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 @file:Suppress("UnstableApiUsage")
18 
19 package androidx.compose.ui.lint
20 
21 import androidx.compose.lint.Names
22 import androidx.compose.lint.inheritsFrom
23 import androidx.compose.lint.isInPackageName
24 import com.android.tools.lint.detector.api.Category
25 import com.android.tools.lint.detector.api.Detector
26 import com.android.tools.lint.detector.api.Implementation
27 import com.android.tools.lint.detector.api.Issue
28 import com.android.tools.lint.detector.api.JavaContext
29 import com.android.tools.lint.detector.api.Scope
30 import com.android.tools.lint.detector.api.Severity
31 import com.android.tools.lint.detector.api.SourceCodeScanner
32 import com.android.tools.lint.detector.api.isBelow
33 import com.intellij.psi.PsiElement
34 import com.intellij.psi.PsiMethod
35 import java.util.EnumSet
36 import org.jetbrains.kotlin.analysis.api.KtAnalysisSession
37 import org.jetbrains.kotlin.analysis.api.analyze
38 import org.jetbrains.kotlin.analysis.api.calls.KtCall
39 import org.jetbrains.kotlin.analysis.api.calls.KtCallableMemberCall
40 import org.jetbrains.kotlin.analysis.api.calls.KtCompoundAccessCall
41 import org.jetbrains.kotlin.analysis.api.calls.KtImplicitReceiverValue
42 import org.jetbrains.kotlin.analysis.api.calls.singleCallOrNull
43 import org.jetbrains.kotlin.analysis.api.symbols.KtClassOrObjectSymbol
44 import org.jetbrains.kotlin.analysis.api.symbols.KtReceiverParameterSymbol
45 import org.jetbrains.kotlin.psi.KtCallExpression
46 import org.jetbrains.kotlin.psi.KtExpression
47 import org.jetbrains.uast.UCallExpression
48 import org.jetbrains.uast.visitor.AbstractUastVisitor
49 
50 /**
51  * [Detector] that checks calls to Modifier.then to make sure the parameter does not contain a
52  * Modifier factory function called with an receiver, as this will cause duplicate modifiers in the
53  * chain. E.g. this.then(foo()), will result in this.then(this.then(foo)), as foo() internally will
54  * call this.then(FooModifier).
55  */
56 class SuspiciousModifierThenDetector : Detector(), SourceCodeScanner {
getApplicableMethodNamesnull57     override fun getApplicableMethodNames(): List<String> = listOf(ThenName)
58 
59     override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
60         if (!method.isInPackageName(Names.Ui.PackageName)) return
61 
62         val otherModifierArgument = node.valueArguments.firstOrNull() ?: return
63         val otherModifierArgumentSource = otherModifierArgument.sourcePsi ?: return
64 
65         otherModifierArgument.accept(
66             object : AbstractUastVisitor() {
67                 /**
68                  * Visit all calls to look for calls to a Modifier factory with implicit receiver
69                  */
70                 override fun visitCallExpression(node: UCallExpression): Boolean {
71                     val hasModifierReceiverType =
72                         node.receiverType?.inheritsFrom(Names.Ui.Modifier) == true
73                     val usesImplicitThis = node.receiver == null
74 
75                     if (!hasModifierReceiverType || !usesImplicitThis) {
76                         return false
77                     }
78 
79                     val ktCallExpression = node.sourcePsi as? KtCallExpression ?: return false
80                     // Resolve the implicit `this` to its source, if possible.
81                     val implicitReceiver =
82                         analyze(ktCallExpression) {
83                             getImplicitReceiverValue(ktCallExpression)?.getImplicitReceiverPsi()
84                         }
85 
86                     // The receiver used by the modifier function is defined within the then() call,
87                     // such as then(Modifier.composed { otherModifierFactory() }). We don't know
88                     // what
89                     // the value of this receiver will be, so we ignore this case.
90                     if (implicitReceiver.isBelow(otherModifierArgumentSource)) {
91                         return false
92                     }
93 
94                     context.report(
95                         SuspiciousModifierThen,
96                         node,
97                         context.getNameLocation(node),
98                         "Using Modifier.then with a Modifier factory function with an implicit receiver"
99                     )
100 
101                     // Keep on searching for more errors
102                     return false
103                 }
104             }
105         )
106     }
107 
108     companion object {
109         val SuspiciousModifierThen =
110             Issue.create(
111                 "SuspiciousModifierThen",
112                 "Using Modifier.then with a Modifier factory function with an implicit receiver",
113                 "Calling a Modifier factory function with an implicit receiver inside " +
114                     "Modifier.then will result in the receiver (`this`) being added twice to the " +
115                     "chain. For example, fun Modifier.myModifier() = this.then(otherModifier()) - " +
116                     "the implementation of factory functions such as Modifier.otherModifier() will " +
117                     "internally call this.then(...) to chain the provided modifier with their " +
118                     "implementation. When you expand this.then(otherModifier()), it becomes: " +
119                     "this.then(this.then(OtherModifierImplementation)) - so you can see that `this` " +
120                     "is included twice in the chain, which results in modifiers such as padding " +
121                     "being applied twice, for example. Instead, you should either remove the then() " +
122                     "and directly chain the factory function on the receiver, this.otherModifier(), " +
123                     "or add the empty Modifier as the receiver for the factory, such as " +
124                     "this.then(Modifier.otherModifier())",
125                 Category.CORRECTNESS,
126                 3,
127                 Severity.ERROR,
128                 Implementation(
129                     SuspiciousModifierThenDetector::class.java,
130                     EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
131                 )
132             )
133     }
134 }
135 
136 private const val ThenName = "then"
137 
138 // Below functions taken from AnalysisApiLintUtils.kt
139 
140 /**
141  * Returns the PSI for [this], which will be the owning lambda expression or the surrounding class.
142  */
KtImplicitReceiverValuenull143 private fun KtImplicitReceiverValue.getImplicitReceiverPsi(): PsiElement? {
144     return when (val receiverParameterSymbol = this.symbol) {
145         // the owning lambda expression
146         is KtReceiverParameterSymbol -> receiverParameterSymbol.owningCallableSymbol.psi
147         // the class that we are in, calling a method
148         is KtClassOrObjectSymbol -> receiverParameterSymbol.psi
149         else -> null
150     }
151 }
152 
153 /**
154  * Returns the implicit receiver value of the call-like expression [ktExpression] (can include
155  * property accesses, for example).
156  */
getImplicitReceiverValuenull157 private fun KtAnalysisSession.getImplicitReceiverValue(
158     ktExpression: KtExpression
159 ): KtImplicitReceiverValue? {
160     val partiallyAppliedSymbol =
161         when (val call = ktExpression.resolveCall()?.singleCallOrNull<KtCall>()) {
162             is KtCompoundAccessCall -> call.compoundAccess.operationPartiallyAppliedSymbol
163             is KtCallableMemberCall<*, *> -> call.partiallyAppliedSymbol
164             else -> null
165         } ?: return null
166 
167     return partiallyAppliedSymbol.extensionReceiver as? KtImplicitReceiverValue
168         ?: partiallyAppliedSymbol.dispatchReceiver as? KtImplicitReceiverValue
169 }
170