| /* |
| * 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 |
| } |
| } |