blob: e5059e1911f78bcabfcbad611863635c39b912a3 [file] [log] [blame]
/*
* Copyright (C) 2021 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.sdklib.SdkVersionInfo
import com.android.tools.lint.checks.VersionChecks.Companion.CHECKS_SDK_INT_AT_LEAST_ANNOTATION
import com.android.tools.lint.client.api.JavaEvaluator
import com.android.tools.lint.client.api.LintClient
import com.android.tools.lint.detector.api.Category
import com.android.tools.lint.detector.api.ConstantEvaluator
import com.android.tools.lint.detector.api.Context
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.LintFix
import com.android.tools.lint.detector.api.Location
import com.android.tools.lint.detector.api.PartialResult
import com.android.tools.lint.detector.api.Project
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.intellij.psi.PsiElement
import com.intellij.psi.PsiField
import com.intellij.psi.PsiMethod
import com.intellij.psi.PsiModifierListOwner
import com.intellij.psi.PsiParameter
import com.intellij.psi.PsiParameterList
import org.jetbrains.uast.UAnnotated
import org.jetbrains.uast.UBinaryExpression
import org.jetbrains.uast.UBlockExpression
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.UElement
import org.jetbrains.uast.UExpression
import org.jetbrains.uast.UField
import org.jetbrains.uast.UIfExpression
import org.jetbrains.uast.UMethod
import org.jetbrains.uast.UQualifiedReferenceExpression
import org.jetbrains.uast.UReferenceExpression
import org.jetbrains.uast.UReturnExpression
import org.jetbrains.uast.UastBinaryOperator
import org.jetbrains.uast.getContainingUClass
import org.jetbrains.uast.getParentOfType
import org.jetbrains.uast.tryResolve
/** Looks for SDK_INT checks and suggests annotating these. */
class SdkIntDetector : Detector(), SourceCodeScanner {
override fun getApplicableReferenceNames(): List<String> = listOf("SDK_INT")
override fun getApplicableMethodNames(): List<String> = listOf("getBuildSdkInt")
override fun visitReference(
context: JavaContext,
reference: UReferenceExpression,
referenced: PsiElement
) {
// Make sure it's android.os.Build.VERSION.SDK_INT, though that's highly likely
val evaluator = context.evaluator
if (evaluator.isMemberInClass(referenced as? PsiField, "android.os.Build.VERSION")) {
checkAnnotation(context, reference)
}
}
override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
checkAnnotation(context, node)
}
override fun checkPartialResults(context: Context, partialResults: PartialResult) {
// Nothing to do here; the partial results are consumed in the VersionChecks lookup
// along the way
}
companion object {
private val IMPLEMENTATION = Implementation(
SdkIntDetector::class.java,
Scope.JAVA_FILE_SCOPE
)
/** SDK_INT without @ChecksSdkIntAtLeast. */
@JvmField
val ISSUE = Issue.create(
id = "AnnotateVersionCheck",
briefDescription = "Annotate SDK_INT checks",
explanation = """
Methods which perform `SDK_INT` version checks (or field constants which reflect \
the result of a version check) in libraries should be annotated with \
`@ChecksSdkIntAtLeast`. This makes it possible for lint to correctly \
check calls into the library later to correctly understand that problematic \
code which is wrapped within a call into this library is safe after all.
""",
category = Category.CORRECTNESS,
priority = 6,
severity = Severity.WARNING,
androidSpecific = true,
implementation = IMPLEMENTATION
)
fun checkAnnotation(context: JavaContext, sdkInt: UElement) {
// In app module analysis we always have source access to the
// check method bodies; don't nag users to annotate these.
val project = context.project
if (!project.isLibrary || !project.isAndroidProject) {
return
}
val comparison = sdkInt.getParentOfType(UBinaryExpression::class.java, true)
?: return
val tokenType = comparison.operator
if (tokenType !== UastBinaryOperator.GREATER &&
tokenType !== UastBinaryOperator.GREATER_OR_EQUALS
) {
return
}
val isGreaterOrEquals = tokenType === UastBinaryOperator.GREATER_OR_EQUALS
val parent = comparison.uastParent
if (parent is UField) {
checkField(comparison, context, isGreaterOrEquals, parent)
return
} else if (parent is UReturnExpression) {
val parentParent = parent.uastParent
if (parentParent is UBlockExpression && parentParent.uastParent is UMethod) {
val size = parentParent.expressions.size
if (size == 1) {
val method = parentParent.uastParent as UMethod
checkMethod(comparison, context, isGreaterOrEquals, method)
}
}
} else if (parent is UIfExpression) {
val then = (parent.thenExpression as? UBlockExpression)?.expressions?.firstOrNull()
?: parent.thenExpression
?: return
val receiver = when (then) {
is UQualifiedReferenceExpression -> then.receiver
is UCallExpression -> then.receiver ?: return
else -> return
}
val method: UMethod = if (parent.uastParent is UReturnExpression) {
parent.uastParent?.uastParent as? UMethod
?: parent.uastParent?.uastParent?.uastParent as? UMethod
?: return
} else if (parent.uastParent is UBlockExpression &&
parent.uastParent?.uastParent is UMethod
) {
val block = parent.uastParent as UBlockExpression
val expressions = block.expressions
if (expressions.size == 1 ||
expressions.size == 2 && expressions[1] is UReturnExpression
) {
parent.uastParent?.uastParent as? UMethod
?: parent.uastParent?.uastParent?.uastParent as? UMethod
?: return
} else {
return
}
} else {
return
}
checkMethod(context, method, receiver, comparison, isGreaterOrEquals)
}
}
private fun checkMethod(
context: JavaContext,
method: UMethod,
receiver: UExpression,
comparison: UBinaryExpression,
isGreaterOrEquals: Boolean
) {
val parameter = receiver.tryResolve() as? PsiParameter ?: return
val index = getParameterIndex(parameter)
if (index != -1) {
checkMethod(comparison, context, isGreaterOrEquals, method, index)
}
}
private fun getParameterIndex(parameter: PsiParameter): Int {
val parameterList = parameter.parent as? PsiParameterList ?: return -1
return parameterList.getParameterIndex(parameter)
}
private fun checkMethod(
comparison: UBinaryExpression,
context: JavaContext,
isGreaterOrEquals: Boolean,
method: UMethod,
lambda: Int = -1
) {
val apiOperand = comparison.rightOperand
val apiValue = apiOperand.evaluate() ?: ConstantEvaluator.evaluate(context, apiOperand)
val api = apiValue as? Int
if (api != null) {
val apiAtLeast = if (isGreaterOrEquals) api else api + 1
if (!annotated(context, method, apiAtLeast)) {
val buildCode = getBuildCode(
apiAtLeast,
if (isGreaterOrEquals) apiOperand else null
)
val location = context.getNameLocation(method)
val args = "api=$buildCode${if (lambda != -1) ", lambda=$lambda" else ""}"
val message =
"This method should be annotated with `@ChecksSdkIntAtLeast($args)`"
val fix = createAnnotationFix(context, args, context.getLocation(method))
context.report(ISSUE, method, location, message, fix)
if (!context.isGlobalAnalysis()) {
// Store data for VersionChecks used by for example ApiDetector
val methodDesc = getMethodKey(context.evaluator, method)
val map = context.getPartialResults(ISSUE).map()
// See VersionChecks#isKnownVersionCheck
map.put(
methodDesc,
"api=$apiAtLeast${if (lambda != -1) ",lambda=$lambda" else ""}"
)
}
}
} else if (apiOperand is UReferenceExpression) {
val parameter = apiOperand.resolve()
if (parameter is PsiParameter) {
val index = getParameterIndex(parameter)
if (index != -1 && !annotated(context, method, -1)) {
val args = "parameter=$index${if (lambda != -1) ", lambda=$lambda" else ""}"
val message =
"This method should be annotated with `@ChecksSdkIntAtLeast($args)`"
val location = context.getNameLocation(method)
val fix = createAnnotationFix(context, args, context.getLocation(method))
context.report(ISSUE, method, location, message, fix)
if (!context.isGlobalAnalysis()) {
// Store data for the Version Check, used from ApiDetector etc
val methodDesc = getMethodKey(context.evaluator, method)
val map = context.getPartialResults(ISSUE).map()
map.put(
methodDesc,
"parameter=$index${if (lambda != -1) ",lambda=$lambda" else ""}"
)
}
}
}
}
}
private fun createAnnotationFix(
context: JavaContext,
args: String,
location: Location
): LintFix? {
// if not on classpath (older annotation library) don't suggest annotating
if (context.evaluator.findClass(CHECKS_SDK_INT_AT_LEAST_ANNOTATION) == null) return null
return LintFix.create()
.annotate("$CHECKS_SDK_INT_AT_LEAST_ANNOTATION($args)")
.range(location)
.build()
}
private fun getBuildCode(api: Int, constant: UElement?): String {
val text = (constant as? UReferenceExpression)?.sourcePsi?.text
if (text != null) {
return text
}
val buildCode = SdkVersionInfo.getBuildCode(api) ?: return api.toString()
return "android.os.Build.VERSION_CODES.$buildCode"
}
private fun checkField(
comparison: UBinaryExpression,
context: JavaContext,
isGreaterOrEquals: Boolean,
field: UField
) {
val apiOperand = comparison.rightOperand
val value = apiOperand.evaluate()
?: ConstantEvaluator.evaluate(context, apiOperand)
val api = value as? Int ?: return
val atLeast = if (isGreaterOrEquals) api else api + 1
if (!annotated(context, field, atLeast)) {
val buildCode = getBuildCode(atLeast, if (isGreaterOrEquals) apiOperand else null)
val args = "api=$buildCode"
val message = "This field should be annotated with `ChecksSdkIntAtLeast($args)`"
val location = context.getLocation(field)
val fix = createAnnotationFix(context, args, location)
context.report(ISSUE, field, location, message, fix)
if (!context.isGlobalAnalysis()) {
// Store data for VersionChecks used by for example ApiDetector
val fieldDesc = getFieldKey(context.evaluator, field)
val map = context.getPartialResults(ISSUE).map()
map.put(fieldDesc, "api=$atLeast")
}
}
}
private fun annotated(context: JavaContext, annotated: UAnnotated, api: Int): Boolean {
// TODO: If annotated, warn if it's not set to the correct API level
return context.evaluator.getAllAnnotations(annotated, false).any {
it.qualifiedName == CHECKS_SDK_INT_AT_LEAST_ANNOTATION
}
}
private fun getMethodKey(
evaluator: JavaEvaluator,
method: UMethod
): String {
val desc = evaluator.getMethodDescription(
method,
includeName = false,
includeReturn = false
)
val cls = method.getContainingUClass()?.let { evaluator.getQualifiedName(it) }
return "$cls#${method.name}$desc"
}
private fun getFieldKey(
evaluator: JavaEvaluator,
field: UField
): String {
val cls = field.getContainingUClass()?.let { evaluator.getQualifiedName(it) }
return "$cls#${field.name}"
}
private fun getMethodKey(
evaluator: JavaEvaluator,
method: PsiMethod
): String {
val desc = evaluator.getMethodDescription(
method,
includeName = false,
includeReturn = false
)
val cls = method.containingClass?.let { evaluator.getQualifiedName(it) }
return "$cls#${method.name}$desc"
}
private fun getFieldKey(
evaluator: JavaEvaluator,
field: PsiField
): String {
val cls = field.containingClass?.let { evaluator.getQualifiedName(it) }
return "$cls#${field.name}"
}
fun findSdkIntAnnotation(
client: LintClient,
evaluator: JavaEvaluator,
project: Project,
owner: PsiModifierListOwner
): SdkIntAnnotation? {
val key = when (owner) {
is PsiMethod -> getMethodKey(evaluator, owner)
is PsiField -> getFieldKey(evaluator, owner)
else -> return null
}
val map = client.getPartialResults(project, ISSUE).map()
val args = map[key] ?: return null
val api = findAttribute(args, "api")?.toIntOrNull()
val codename = findAttribute(args, "codename")
val parameter = findAttribute(args, "parameter")?.toIntOrNull()
val lambda = findAttribute(args, "lambda")?.toIntOrNull()
return SdkIntAnnotation(api, codename, parameter, lambda)
}
private fun findAttribute(args: String, name: String): String? {
val key = "$name="
val index = args.indexOf(key)
if (index == -1) {
return null
}
val start = index + key.length
val end = args.indexOf(',', start).let { if (it == -1) args.length else it }
return args.substring(start, end)
}
}
}