blob: 65c93d900580011ecd739d4d0b8fc494834a6e58 [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.SdkConstants.CLASS_VIEW
import com.android.tools.lint.client.api.UElementHandler
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.isJava
import com.intellij.psi.PsiCompiledElement
import com.intellij.psi.PsiField
import com.intellij.psi.PsiLocalVariable
import com.intellij.psi.PsiParameter
import org.jetbrains.uast.UBinaryExpression
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.UCallableReferenceExpression
import org.jetbrains.uast.UElement
import org.jetbrains.uast.UExpression
import org.jetbrains.uast.ULambdaExpression
import org.jetbrains.uast.ULocalVariable
import org.jetbrains.uast.UMethod
import org.jetbrains.uast.UReferenceExpression
import org.jetbrains.uast.USimpleNameReferenceExpression
import org.jetbrains.uast.UastBinaryOperator
import org.jetbrains.uast.getContainingUMethod
import org.jetbrains.uast.toUElement
import org.jetbrains.uast.tryResolve
import org.jetbrains.uast.util.isAssignment
import org.jetbrains.uast.visitor.AbstractUastVisitor
/**
* Looks for bugs around implicit SAM conversions
*/
class SamDetector : Detector(), SourceCodeScanner {
companion object Issues {
/** Improperly handling implicit SAM instances */
@JvmField
val ISSUE = Issue.create(
id = "ImplicitSamInstance",
briefDescription = "Implicit SAM Instances",
explanation = """
Kotlin's support for SAM (single accessor method) interfaces lets you pass \
a lambda to the interface. This will create a new instance on the fly even \
though there is no explicit constructor call. If you pass one of these \
lambdas or method references into a method which (for example) stores or \
compares the object identity, unexpected results may happen.
""",
category = Category.CORRECTNESS,
priority = 6,
severity = Severity.WARNING,
androidSpecific = null,
enabledByDefault = false,
implementation = Implementation(
SamDetector::class.java,
Scope.JAVA_FILE_SCOPE
)
)
private const val HANDLER_CLASS = "android.os.Handler"
private const val DRAWABLE_CALLBACK_CLASS = "android.graphics.drawable.Drawable.Callback"
private const val RUNNABLE_CLASS = "java.lang.Runnable"
}
override fun getApplicableUastTypes(): List<Class<out UElement>>? =
listOf(ULambdaExpression::class.java, UCallableReferenceExpression::class.java)
override fun createUastHandler(context: JavaContext): UElementHandler? {
val psi = context.uastFile?.sourcePsi ?: return null
if (isJava(psi)) {
return null
}
return object : UElementHandler() {
override fun visitLambdaExpression(node: ULambdaExpression) {
val parent = node.uastParent ?: return
if (parent is ULocalVariable) {
val psiVar = parent.sourcePsi as? PsiLocalVariable ?: parent.psi ?: return
checkCalls(context, node, psiVar)
} else if (parent.isAssignment()) {
val v = (parent as UBinaryExpression).leftOperand.tryResolve() ?: return
val psiVar = v as? PsiLocalVariable ?: return
checkCalls(context, node, psiVar)
}
}
override fun visitCallableReferenceExpression(node: UCallableReferenceExpression) {
val call = node.uastParent as? UCallExpression ?: return
checkLambda(context, node, call, node)
}
}
}
private fun checkCalls(
context: JavaContext,
lambda: ULambdaExpression,
variable: PsiLocalVariable
) {
val method = lambda.getContainingUMethod() ?: return
method.accept(object : AbstractUastVisitor() {
override fun visitCallExpression(node: UCallExpression): Boolean {
for (argument in node.valueArguments) {
if (argument is UReferenceExpression && argument.resolve() == variable) {
checkLambda(context, lambda, node, argument)
}
}
return super.visitCallExpression(node)
}
})
}
private fun checkLambda(
context: JavaContext,
lambda: UExpression,
call: UCallExpression,
argument: UReferenceExpression
) {
val psiMethod = call.resolve() ?: return
val evaluator = context.evaluator
if (psiMethod is PsiCompiledElement) {
// The various Runnable methods in Handler operate on Runnable instances
// that are stored. Ditto for View and Drawable.Callback.
val containingClass = psiMethod.containingClass
if (evaluator.isMemberInClass(psiMethod, HANDLER_CLASS) ||
evaluator.inheritsFrom(containingClass, CLASS_VIEW, false) ||
evaluator.inheritsFrom(containingClass, "android.view.ViewTreeObserver", false) ||
evaluator.inheritsFrom(containingClass, DRAWABLE_CALLBACK_CLASS, false)
) {
// idea: only store if temporarily in a variable
val map = evaluator.computeArgumentMapping(call, psiMethod)
val psiParameter = map[lambda] ?: return
val typeString = psiParameter.type.canonicalText
if (typeString == RUNNABLE_CLASS) {
reportError(context, lambda, typeString, argument)
}
}
return
}
if (!isJava(psiMethod)) {
return
}
val map = evaluator.computeArgumentMapping(call, psiMethod)
val psiParameter = map[argument] ?: return
val method = psiMethod.toUElement(UMethod::class.java) ?: return
if (storesLambda(method, psiParameter) &&
!context.driver.isSuppressed(context, ISSUE, method as UElement)
) {
val typeString = psiParameter.type.canonicalText
reportError(context, lambda, typeString, argument)
}
}
private fun reportError(
context: JavaContext,
lambda: UExpression,
type: String,
argument: UReferenceExpression
) {
val location = context.getLocation(argument)
val simpleType = type.substring(type.lastIndexOf('.') + 1)
val range = context.getLocation(lambda)
val fix =
if (lambda is ULambdaExpression) {
fix()
.name("Explicitly create $simpleType instance")
.replace()
.beginning()
.with("$simpleType ")
.range(range)
.build()
} else {
null
}
context.report(
ISSUE, argument, location,
"Implicit new `$simpleType` instance being passed to method which ends up " +
"checking instance equality; this can lead to subtle bugs",
fix
)
}
private fun storesLambda(method: UMethod, parameter: PsiParameter): Boolean {
var storesLambda = false
method.accept(object : AbstractUastVisitor() {
override fun visitSimpleNameReferenceExpression(node: USimpleNameReferenceExpression): Boolean {
val resolved = node.resolve()
if (resolved == parameter) {
val parent = node.uastParent
if (parent is UCallExpression) {
// Decide if we're calling some method which is storing the new instance
val methodName = parent.methodName
if (methodName != null &&
(methodName.startsWith("add") ||
methodName.startsWith("put") ||
methodName.startsWith("set"))
) {
storesLambda = true
}
} else if (parent is UBinaryExpression) {
val kind = parent.operator
if (kind == UastBinaryOperator.IDENTITY_EQUALS ||
kind == UastBinaryOperator.IDENTITY_NOT_EQUALS
) {
storesLambda = true
} else if (kind == UastBinaryOperator.ASSIGN && parent.rightOperand == node) {
val lhs = parent.leftOperand.tryResolve()
if (lhs is PsiField) {
storesLambda = true
}
}
}
// One thing I can try is to let you ONLY invoke methods on these things,
// to see what else I can surface
}
return super.visitSimpleNameReferenceExpression(node)
}
})
return storesLambda
}
}