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