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