1 /*
2 * Copyright 2021 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.intellij.lang.java.JavaLanguage
20 import com.intellij.psi.PsiMethod
21 import com.intellij.psi.PsiParameter
22 import com.intellij.psi.impl.compiled.ClsParameterImpl
23 import com.intellij.psi.impl.light.LightParameter
24 import kotlin.metadata.jvm.annotations
25 import org.jetbrains.kotlin.psi.KtAnnotated
26 import org.jetbrains.kotlin.psi.KtFunction
27 import org.jetbrains.kotlin.psi.KtProperty
28 import org.jetbrains.kotlin.psi.KtTypeReference
29 import org.jetbrains.kotlin.psi.psiUtil.getParentOfType
30 import org.jetbrains.uast.UAnnotation
31 import org.jetbrains.uast.UAnonymousClass
32 import org.jetbrains.uast.UCallExpression
33 import org.jetbrains.uast.UDeclaration
34 import org.jetbrains.uast.UElement
35 import org.jetbrains.uast.UExpression
36 import org.jetbrains.uast.ULambdaExpression
37 import org.jetbrains.uast.UMethod
38 import org.jetbrains.uast.UParameter
39 import org.jetbrains.uast.UTypeReferenceExpression
40 import org.jetbrains.uast.UVariable
41 import org.jetbrains.uast.getContainingDeclaration
42 import org.jetbrains.uast.getContainingUClass
43 import org.jetbrains.uast.getParameterForArgument
44 import org.jetbrains.uast.toUElement
45 import org.jetbrains.uast.withContainingElements
46
47 /**
48 * Returns whether this [UExpression] is directly invoked within the body of a Composable function
49 * or lambda without being `remember`ed.
50 */
UExpressionnull51 fun UExpression.isNotRemembered(): Boolean = isNotRememberedWithKeys()
52
53 /**
54 * Returns whether this [UExpression] is directly invoked within the body of a Composable function
55 * or lambda without being `remember`ed, or whether it is invoked inside a `remember call without
56 * the provided [keys][keyClassNames].
57 * - Returns true if this [UExpression] is directly invoked inside a Composable function or lambda
58 * without being `remember`ed
59 * - Returns true if this [UExpression] is invoked inside a call to `remember`, but without all of
60 * the provided [keys][keyClassNames] being used as key parameters to `remember`
61 * - Returns false if this [UExpression] is correctly `remember`ed with the provided
62 * [keys][keyClassNames], or is not called inside a `remember` block, and is not called inside a
63 * Composable function or lambda
64 *
65 * @param keyClassNames [Name]s representing the expected classes that should be used as a key
66 * parameter to the `remember` call
67 */
68 fun UExpression.isNotRememberedWithKeys(vararg keyClassNames: Name): Boolean {
69 val visitor = ComposableBodyVisitor(this)
70 // The nearest method or lambda expression that contains this call expression
71 val boundaryElement = visitor.parentUElements().last()
72 // Check if the nearest lambda expression is actually a call to remember
73 val rememberCall: UCallExpression? =
74 (boundaryElement.uastParent as? UCallExpression)?.takeIf {
75 it.methodName == Names.Runtime.Remember.shortName &&
76 it.resolve()?.isInPackageName(Names.Runtime.PackageName) == true
77 }
78 return if (rememberCall == null) {
79 visitor.isComposable()
80 } else {
81 val parameterTypes =
82 rememberCall.valueArguments.mapNotNull { it.getExpressionType()?.canonicalText }
83 !keyClassNames.all { parameterTypes.contains(it.javaFqn) }
84 }
85 }
86
87 /**
88 * Returns whether this [UExpression] is invoked within the body of a Composable function or lambda.
89 *
90 * This searches parent declarations until we find a lambda expression or a function, and looks to
91 * see if these are Composable.
92 */
UExpressionnull93 fun UExpression.isInvokedWithinComposable(): Boolean {
94 return ComposableBodyVisitor(this).isComposable()
95 }
96
97 // TODO: https://youtrack.jetbrains.com/issue/KT-45406
98 // KotlinUMethodWithFakeLightDelegate.hasAnnotation() (for reified functions for example)
99 // doesn't find annotations, so just look at the annotations directly.
100 /** Returns whether this method is @Composable or not */
101 val PsiMethod.isComposable
<lambda>null102 get() = annotations.any { it.qualifiedName == Names.Runtime.Composable.javaFqn }
103
104 /** Returns whether this variable's type is @Composable or not */
105 val UVariable.isComposable: Boolean
106 get() {
107 // Annotation on the lambda
108 val annotationOnLambda =
109 when (val initializer = uastInitializer) {
110 is ULambdaExpression -> {
111 val source = initializer.sourcePsi
112 if (source is KtFunction) {
113 // Anonymous function, val foo = @Composable fun() {}
114 source.hasComposableAnnotation
115 } else {
116 // Lambda, val foo = @Composable {}
117 initializer.findAnnotation(Names.Runtime.Composable.javaFqn) != null
118 }
119 }
120 else -> false
121 }
122 // Annotation on the type, foo: @Composable () -> Unit = { }
123 val annotationOnType = typeReference?.isComposable == true
124 return annotationOnLambda || annotationOnType
125 }
126
127 /** Returns whether this parameter's type is @Composable or not */
128 private val PsiParameter.isComposable: Boolean
129 get() =
130 when {
131 // The parameter is in a class file. Currently type annotations aren't currently added
132 // to
133 // the underlying type (https://youtrack.jetbrains.com/issue/KT-45307), so instead we
134 // use
135 // the metadata annotation.
136 this is ClsParameterImpl ||
137 // In some cases when a method is defined in bytecode and the call fails to resolve
138 // to the ClsMethodImpl, we will instead get a LightParameter. Note that some Kotlin
139 // declarations too will also appear as a LightParameter, so we can check to see if
140 // the source language is Java, which means that this is a LightParameter for
141 // bytecode, as opposed to for a Kotlin declaration.
142 // https://youtrack.jetbrains.com/issue/KT-46883
143 (this is LightParameter && this.language is JavaLanguage) -> {
144 // Find the containing method, so we can get metadata from the containing class
145 val containingMethod = getParentOfType<PsiMethod>(true)
146 val kmFunction = containingMethod!!.toKmFunction()
147
<lambda>null148 val kmValueParameter = kmFunction?.valueParameters?.find { it.name == name }
149
<lambda>null150 kmValueParameter?.type?.annotations?.find {
151 it.className == Names.Runtime.Composable.kmClassName
152 } != null
153 }
154 // The parameter is in a source declaration
155 else -> (toUElement() as? UParameter)?.typeReference?.isComposable == true
156 }
157
158 /** Returns whether this lambda expression is @Composable or not */
159 val ULambdaExpression.isComposable: Boolean
160 get() =
161 when (val lambdaParent = uastParent) {
162 // Function call with a lambda parameter
163 is UCallExpression -> {
164 val parameter = lambdaParent.getParameterForArgument(this)
165 parameter?.isComposable == true
166 }
167 // A local / non-local lambda variable
168 is UVariable -> {
169 lambdaParent.isComposable
170 }
171 // Either a new UAST type we haven't handled, or non-Kotlin declarations
172 else -> false
173 }
174
175 /**
176 * Helper class that visits parent declarations above the provided [expression], until it finds a
177 * lambda or method. This 'boundary' is used as the indicator for whether this [expression] can be
178 * considered to be inside a Composable body or not.
179 *
180 * @see isComposable
181 * @see parentUElements
182 */
183 private class ComposableBodyVisitor(private val expression: UExpression) {
184 /** @return whether the body can be considered Composable or not */
isComposablenull185 fun isComposable(): Boolean =
186 when (val element = parentUElements.last()) {
187 is UMethod -> element.isComposable
188 is ULambdaExpression -> element.isComposable
189 else -> false
190 }
191
192 /** Returns all parent [UElement]s until and including the boundary lambda / method. */
parentUElementsnull193 fun parentUElements() = parentUElements
194
195 /**
196 * The outermost UElement that corresponds to the surrounding UDeclaration that contains
197 * [expression], with the following special cases:
198 * - if the containing UDeclaration is a local property, we ignore it and search above as it
199 * still could be created in the context of a Composable body
200 * - if the containing UDeclaration is an anonymous class (object { }), we ignore it and search
201 * above as it still could be created in the context of a Composable body
202 */
203 private val boundaryUElement by lazy {
204 // The nearest property / function / etc declaration that contains this call expression
205 var containingDeclaration = expression.getContainingDeclaration()
206
207 fun UDeclaration.isLocalProperty() = (sourcePsi as? KtProperty)?.isLocal == true
208 fun UDeclaration.isAnonymousClass() = this is UAnonymousClass
209 fun UDeclaration.isPropertyInsideAnonymousClass() =
210 getContainingUClass()?.isAnonymousClass() == true
211
212 while (
213 containingDeclaration != null &&
214 (containingDeclaration.isLocalProperty() ||
215 containingDeclaration.isAnonymousClass() ||
216 containingDeclaration.isPropertyInsideAnonymousClass())
217 ) {
218 containingDeclaration = containingDeclaration.getContainingDeclaration()
219 }
220
221 containingDeclaration
222 }
223
<lambda>null224 private val parentUElements by lazy {
225 val elements = mutableListOf<UElement>()
226
227 // Look through containing elements until we find a lambda or a method
228 for (element in expression.withContainingElements) {
229 elements += element
230 when (element) {
231 // TODO: consider handling the case of a lambda inside an inline function call,
232 // such as `apply` or `forEach`. These calls don't really change the
233 // 'composability' here, but there may be other inline function calls that
234 // capture the lambda and invoke it elsewhere, so we might need to look for
235 // a callsInPlace contract in the metadata for the function, or the body of the
236 // source definition.
237 is ULambdaExpression -> break
238 is UMethod -> break
239 // Stop when we reach the parent declaration to avoid escaping the scope.
240 boundaryUElement -> break
241 }
242 }
243 elements
244 }
245 }
246
247 /** Returns whether this type reference is @Composable or not */
248 val UTypeReferenceExpression.isComposable: Boolean
249 get() {
250 if (type.hasAnnotation(Names.Runtime.Composable.javaFqn)) return true
251
252 // Annotations on the types of local properties (val foo: @Composable () -> Unit = {})
253 // are currently not present on the PsiType, we so need to manually check the underlying
254 // type reference. (https://youtrack.jetbrains.com/issue/KTIJ-18821)
255 return (sourcePsi as? KtTypeReference)?.hasComposableAnnotation == true
256 }
257
258 /** Returns whether this annotated declaration has a Composable annotation */
259 private val KtAnnotated.hasComposableAnnotation: Boolean
260 get() =
<lambda>null261 annotationEntries.any {
262 (it.toUElement() as UAnnotation).qualifiedName == Names.Runtime.Composable.javaFqn
263 }
264