blob: 992c919d1cf3c3a6a4cf58e1458081f9afb7ab96 [file] [log] [blame]
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.lint.checks
import com.android.tools.lint.detector.api.Category
import com.android.tools.lint.detector.api.Detector
import com.android.tools.lint.detector.api.Implementation
import com.android.tools.lint.detector.api.Issue
import com.android.tools.lint.detector.api.JavaContext
import com.android.tools.lint.detector.api.Scope
import com.android.tools.lint.detector.api.Severity
import com.android.tools.lint.detector.api.SourceCodeScanner
import com.android.tools.lint.detector.api.getMethodName
import com.intellij.psi.PsiLocalVariable
import com.intellij.psi.PsiMethod
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.UElement
import org.jetbrains.uast.ULocalVariable
import org.jetbrains.uast.UMethod
import org.jetbrains.uast.UQualifiedReferenceExpression
import org.jetbrains.uast.getParentOfType
import org.jetbrains.uast.tryResolve
/**
* Some lint checks around WorkManager usage
*/
class WorkManagerDetector : Detector(), SourceCodeScanner {
companion object {
private val IMPLEMENTATION = Implementation(
WorkManagerDetector::class.java,
Scope.JAVA_FILE_SCOPE
)
/** Problems with enqueueing work manager continuations */
@JvmField
val ISSUE = Issue.create(
id = "EnqueueWork",
briefDescription = "WorkManager Enqueue",
explanation = """
`WorkContinuations` cannot be enqueued automatically. You must call `enqueue()` \
on a `WorkContinuation` to have it and its parent continuations enqueued inside \
`WorkManager`.
""",
category = Category.CORRECTNESS,
priority = 6,
severity = Severity.WARNING,
androidSpecific = true,
implementation = IMPLEMENTATION
)
private const val CLASS_WORK_MANAGER = "androidx.work.WorkManager"
private const val CLASS_WORK_CONTINUATION = "androidx.work.WorkContinuation"
private const val METHOD_BEGIN_WITH = "beginWith"
private const val METHOD_BEGIN_UNIQUE_WORK = "beginUniqueWork"
private const val METHOD_ENQUEUE = "enqueue"
private const val METHOD_ENQUEUE_SYNC = "enqueueSync"
private const val METHOD_ENQUEUE_UNIQUE = "enqueueUniquePeriodicWork"
private const val METHOD_THEN = "then"
private const val METHOD_COMBINE = "combine"
}
override fun getApplicableMethodNames(): List<String>? {
return listOf(METHOD_BEGIN_WITH, METHOD_BEGIN_UNIQUE_WORK, METHOD_THEN, METHOD_COMBINE)
}
override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
if (!context.evaluator.isMemberInClass(method, CLASS_WORK_MANAGER) &&
!context.evaluator.isMemberInClass(method, CLASS_WORK_CONTINUATION)
) {
return
}
val surrounding = node.getParentOfType<UMethod>(UMethod::class.java, true) ?: return
if (surrounding.returnType?.canonicalText == CLASS_WORK_CONTINUATION) {
return
}
var enqueued = false
surrounding.accept(object : DataFlowAnalyzer(listOf(node)) {
override fun receiver(call: UCallExpression) {
val methodName = getMethodName(call)
if (methodName == METHOD_ENQUEUE ||
methodName == METHOD_ENQUEUE_SYNC ||
methodName == METHOD_ENQUEUE_UNIQUE
) {
// TODO: check that it's called on the WorkContinuation?
enqueued = true
} else if (methodName == METHOD_THEN || methodName == METHOD_COMBINE) {
// Implicitly enqueued by the then/combine methods on the WorkContinuation
enqueued = true
}
}
override fun argument(call: UCallExpression, reference: UElement) {
val methodName = getMethodName(call)
if (methodName == METHOD_COMBINE) {
enqueued = true
} else {
// Used in a list etc: start to track the list
val parent = call.uastParent
if (parent is UQualifiedReferenceExpression) {
val listVariable = parent.receiver.tryResolve()
if (listVariable is PsiLocalVariable) {
references.add(listVariable)
} else {
// List factory method?
val parentParent = parent.uastParent
if (parentParent is ULocalVariable) {
addVariableReference(parentParent)
}
}
} else if (parent is ULocalVariable) {
// Some direct list construction call, such as listOf() in Kotlin
addVariableReference(parent)
}
}
}
override fun returnsSelf(call: UCallExpression): Boolean {
val methodName = getMethodName(call)
if (methodName == "synchronous") {
return true
}
return super.returnsSelf(call)
}
})
if (!enqueued) {
val name = (node.uastParent?.uastParent as? ULocalVariable)?.name
val nameString = if (name != null) "`$name` " else ""
context.report(
ISSUE, node, context.getLocation(node),
"WorkContinuation ${nameString}not enqueued: did you forget to call `enqueue()`?"
)
}
}
}