1 /* 2 * Copyright 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.compose.lint 18 19 import com.android.tools.lint.client.api.UElementHandler 20 import com.android.tools.lint.detector.api.Category 21 import com.android.tools.lint.detector.api.Detector 22 import com.android.tools.lint.detector.api.Implementation 23 import com.android.tools.lint.detector.api.Issue 24 import com.android.tools.lint.detector.api.JavaContext 25 import com.android.tools.lint.detector.api.LintFix 26 import com.android.tools.lint.detector.api.Scope 27 import com.android.tools.lint.detector.api.Severity 28 import com.android.tools.lint.detector.api.SourceCodeScanner 29 import java.util.EnumSet 30 import org.jetbrains.kotlin.analysis.api.analyze 31 import org.jetbrains.kotlin.psi.KtExpression 32 import org.jetbrains.uast.UBinaryExpression 33 import org.jetbrains.uast.UCallExpression 34 import org.jetbrains.uast.UastBinaryOperator 35 36 /** 37 * Lint [Detector] to ensure that lambda instances are compared referentially, instead of 38 * structurally. 39 * 40 * This is needed as function references (::lambda) do not consider their capture scope in their 41 * equals implementation. This means that structural equality can return true, even if the lambdas 42 * are different references with a different capture scope. Instead, lambdas should be compared 43 * referentially (=== or !==) to avoid this issue. 44 */ 45 class LambdaStructuralEqualityDetector : Detector(), SourceCodeScanner { getApplicableUastTypesnull46 override fun getApplicableUastTypes() = 47 listOf(UBinaryExpression::class.java, UCallExpression::class.java) 48 49 override fun createUastHandler(context: JavaContext) = 50 object : UElementHandler() { 51 override fun visitBinaryExpression(node: UBinaryExpression) { 52 val op = node.operator 53 if (op == UastBinaryOperator.EQUALS || op == UastBinaryOperator.NOT_EQUALS) { 54 val left = node.leftOperand.sourcePsi as? KtExpression ?: return 55 val right = node.rightOperand.sourcePsi as? KtExpression ?: return 56 if (left.isFunctionType() && right.isFunctionType()) { 57 val replacement = if (op == UastBinaryOperator.EQUALS) "===" else "!==" 58 context.report( 59 ISSUE, 60 node.operatorIdentifier, 61 context.getNameLocation(node.operatorIdentifier ?: node), 62 BriefDescription, 63 LintFix.create() 64 .replace() 65 .name("Change to $replacement") 66 .text(op.text) 67 .with(replacement) 68 .autoFix() 69 .build() 70 ) 71 } 72 } 73 } 74 75 override fun visitCallExpression(node: UCallExpression) { 76 if (node.methodName == "equals") { 77 val left = node.receiver?.sourcePsi as? KtExpression ?: return 78 val right = 79 node.valueArguments.firstOrNull()?.sourcePsi as? KtExpression ?: return 80 if (left.isFunctionType() && right.isFunctionType()) { 81 context.report(ISSUE, node, context.getNameLocation(node), BriefDescription) 82 } 83 } 84 } 85 } 86 isFunctionTypenull87 private fun KtExpression.isFunctionType(): Boolean = 88 analyze(this) { expressionType?.isFunctionType == true } 89 90 companion object { 91 private const val BriefDescription = 92 "Checking lambdas for structural equality, instead " + 93 "of checking for referential equality" 94 private const val Explanation = 95 "Checking structural equality on lambdas can lead to issues, as function references " + 96 "(::lambda) do not consider their capture scope in their equals implementation. " + 97 "This means that structural equality can return true, even if the lambdas are " + 98 "different references with a different capture scope. Instead, lambdas should be" + 99 "compared referentially (=== or !==) to avoid this issue." 100 101 val ISSUE = 102 Issue.create( 103 "LambdaStructuralEquality", 104 BriefDescription, 105 Explanation, 106 Category.CORRECTNESS, 107 5, 108 Severity.ERROR, 109 Implementation( 110 LambdaStructuralEqualityDetector::class.java, 111 EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES) 112 ) 113 ) 114 } 115 } 116