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