• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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  *      https://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("FunctionName")
18 
19 package com.google.accompanist.permissions.lint.util
20 
21 import com.intellij.lang.java.JavaLanguage
22 import com.intellij.psi.PsiMethod
23 import com.intellij.psi.PsiParameter
24 import com.intellij.psi.impl.compiled.ClsParameterImpl
25 import com.intellij.psi.impl.light.LightParameter
26 import kotlinx.metadata.jvm.annotations
27 import org.jetbrains.kotlin.psi.KtAnnotated
28 import org.jetbrains.kotlin.psi.KtFunction
29 import org.jetbrains.kotlin.psi.KtProperty
30 import org.jetbrains.kotlin.psi.KtTypeReference
31 import org.jetbrains.kotlin.psi.psiUtil.getParentOfType
32 import org.jetbrains.uast.UAnnotation
33 import org.jetbrains.uast.UAnonymousClass
34 import org.jetbrains.uast.UCallExpression
35 import org.jetbrains.uast.UDeclaration
36 import org.jetbrains.uast.UElement
37 import org.jetbrains.uast.ULambdaExpression
38 import org.jetbrains.uast.UMethod
39 import org.jetbrains.uast.UParameter
40 import org.jetbrains.uast.UTypeReferenceExpression
41 import org.jetbrains.uast.UVariable
42 import org.jetbrains.uast.getContainingDeclaration
43 import org.jetbrains.uast.getContainingUClass
44 import org.jetbrains.uast.getParameterForArgument
45 import org.jetbrains.uast.toUElement
46 import org.jetbrains.uast.withContainingElements
47 
48 // FILE COPIED FROM:
49 // https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/lint/common/src/main/java/androidx/compose/lint/ComposableUtils.kt
50 
51 /**
52  * Returns whether this [UCallExpression] is invoked within the body of a Composable function or
53  * lambda.
54  *
55  * This searches parent declarations until we find a lambda expression or a function, and looks
56  * to see if these are Composable.
57  */
isInvokedWithinComposablenull58 public fun UCallExpression.isInvokedWithinComposable(): Boolean {
59     return ComposableBodyVisitor(this).isComposable()
60 }
61 
62 // TODO: https://youtrack.jetbrains.com/issue/KT-45406
63 // KotlinUMethodWithFakeLightDelegate.hasAnnotation() (for reified functions for example)
64 // doesn't find annotations, so just look at the annotations directly.
65 /**
66  * Returns whether this method is @Composable or not
67  */
68 public val PsiMethod.isComposable: Boolean
<lambda>null69     get() = annotations.any { it.qualifiedName == Composable.javaFqn }
70 
71 /**
72  * Returns whether this variable's type is @Composable or not
73  */
74 public val UVariable.isComposable: Boolean
75     get() {
76         // Annotation on the lambda
77         val annotationOnLambda = when (val initializer = uastInitializer) {
78             is ULambdaExpression -> {
79                 val source = initializer.sourcePsi
80                 if (source is KtFunction) {
81                     // Anonymous function, val foo = @Composable fun() {}
82                     source.hasComposableAnnotation
83                 } else {
84                     // Lambda, val foo = @Composable {}
85                     initializer.findAnnotation(Composable.javaFqn) != null
86                 }
87             }
88             else -> false
89         }
90         // Annotation on the type, foo: @Composable () -> Unit = { }
91         val annotationOnType = typeReference?.isComposable == true
92         return annotationOnLambda || annotationOnType
93     }
94 
95 /**
96  * Returns whether this parameter's type is @Composable or not
97  */
98 private val PsiParameter.isComposable: Boolean
99     get() = when {
100         // The parameter is in a class file. Currently type annotations aren't currently added to
101         // the underlying type (https://youtrack.jetbrains.com/issue/KT-45307), so instead we use
102         // the metadata annotation.
103         this is ClsParameterImpl ||
104             // In some cases when a method is defined in bytecode and the call fails to resolve
105             // to the ClsMethodImpl, we will instead get a LightParameter. Note that some Kotlin
106             // declarations too will also appear as a LightParameter, so we can check to see if
107             // the source language is Java, which means that this is a LightParameter for
108             // bytecode, as opposed to for a Kotlin declaration.
109             // https://youtrack.jetbrains.com/issue/KT-46883
110             (this is LightParameter && this.language is JavaLanguage) -> {
111             // Find the containing method, so we can get metadata from the containing class
112             val containingMethod = getParentOfType<PsiMethod>(true)
113             val kmFunction = containingMethod!!.toKmFunction()
114 
<lambda>null115             val kmValueParameter = kmFunction?.valueParameters?.find {
116                 it.name == name
117             }
118 
<lambda>null119             kmValueParameter?.type?.annotations?.find {
120                 it.className == Composable.kmClassName
121             } != null
122         }
123         // The parameter is in a source declaration
124         else -> (toUElement() as UParameter).typeReference!!.isComposable
125     }
126 
127 /**
128  * Returns whether this lambda expression is @Composable or not
129  */
130 public val ULambdaExpression.isComposable: Boolean
131     get() = when (val lambdaParent = uastParent) {
132         // Function call with a lambda parameter
133         is UCallExpression -> {
134             val parameter = lambdaParent.getParameterForArgument(this)
135             parameter?.isComposable == true
136         }
137         // A local / non-local lambda variable
138         is UVariable -> {
139             lambdaParent.isComposable
140         }
141         // Either a new UAST type we haven't handled, or non-Kotlin declarations
142         else -> false
143     }
144 
145 /**
146  * Helper class that visits parent declarations above the provided [callExpression], until it
147  * finds a lambda or method. This 'boundary' is used as the indicator for whether this
148  * [callExpression] can be considered to be inside a Composable body or not.
149  *
150  * @see isComposable
151  * @see parentUElements
152  */
153 private class ComposableBodyVisitor(
154     private val callExpression: UCallExpression
155 ) {
156     /**
157      * @return whether the body can be considered Composable or not
158      */
isComposablenull159     fun isComposable(): Boolean = when (val element = parentUElements.last()) {
160         is UMethod -> element.isComposable
161         is ULambdaExpression -> element.isComposable
162         else -> false
163     }
164 
165     /**
166      * Returns all parent [UElement]s until and including the boundary lambda / method.
167      */
parentUElementsnull168     fun parentUElements() = parentUElements
169 
170     /**
171      * The outermost UElement that corresponds to the surrounding UDeclaration that contains
172      * [callExpression], with the following special cases:
173      *
174      * - if the containing UDeclaration is a local property, we ignore it and search above as
175      * it still could be created in the context of a Composable body
176      * - if the containing UDeclaration is an anonymous class (object { }), we ignore it and
177      * search above as it still could be created in the context of a Composable body
178      */
179     private val boundaryUElement by lazy {
180         // The nearest property / function / etc declaration that contains this call expression
181         var containingDeclaration = callExpression.getContainingDeclaration()
182 
183         fun UDeclaration.isLocalProperty() = (sourcePsi as? KtProperty)?.isLocal == true
184         fun UDeclaration.isAnonymousClass() = this is UAnonymousClass
185         fun UDeclaration.isPropertyInsideAnonymousClass() =
186             getContainingUClass()?.isAnonymousClass() == true
187 
188         while (
189             containingDeclaration != null &&
190             (
191                 containingDeclaration.isLocalProperty() ||
192                     containingDeclaration.isAnonymousClass() ||
193                     containingDeclaration.isPropertyInsideAnonymousClass()
194                 )
195         ) {
196             containingDeclaration = containingDeclaration.getContainingDeclaration()
197         }
198 
199         containingDeclaration
200     }
201 
<lambda>null202     private val parentUElements by lazy {
203         val elements = mutableListOf<UElement>()
204 
205         // Look through containing elements until we find a lambda or a method
206         for (element in callExpression.withContainingElements) {
207             elements += element
208             when (element) {
209                 // TODO: consider handling the case of a lambda inside an inline function call,
210                 //  such as `apply` or `forEach`. These calls don't really change the
211                 //  'composability' here, but there may be other inline function calls that
212                 //  capture the lambda and invoke it elsewhere, so we might need to look for
213                 //  a callsInPlace contract in the metadata for the function, or the body of the
214                 //  source definition.
215                 is ULambdaExpression -> break
216                 is UMethod -> break
217                 // Stop when we reach the parent declaration to avoid escaping the scope.
218                 boundaryUElement -> break
219             }
220         }
221         elements
222     }
223 }
224 
225 /**
226  * Returns whether this type reference is @Composable or not
227  */
228 private val UTypeReferenceExpression.isComposable: Boolean
229     get() {
230         if (type.hasAnnotation(Composable.javaFqn)) return true
231 
232         // Annotations on the types of local properties (val foo: @Composable () -> Unit = {})
233         // are currently not present on the PsiType, we so need to manually check the underlying
234         // type reference. (https://youtrack.jetbrains.com/issue/KTIJ-18821)
235         return (sourcePsi as? KtTypeReference)?.hasComposableAnnotation == true
236     }
237 
238 /**
239  * Returns whether this annotated declaration has a Composable annotation
240  */
241 private val KtAnnotated.hasComposableAnnotation: Boolean
<lambda>null242     get() = annotationEntries.any {
243         (it.toUElement() as UAnnotation).qualifiedName == Composable.javaFqn
244     }
245 
246 private val RuntimePackageName = Package("androidx.compose.runtime")
247 private val Composable = Name(RuntimePackageName, "Composable")
248 
249 /**
250  * @return a [PackageName] with a Java-style (separated with `.`) [packageName].
251  */
Packagenull252 internal fun Package(packageName: String): PackageName =
253     PackageName(packageName.split("."))
254 
255 /**
256  * @return a [Name] with the provided [pkg] and Java-style (separated with `.`) [shortName].
257  */
258 internal fun Name(pkg: PackageName, shortName: String): Name =
259     Name(pkg, shortName.split("."))
260 
261 /**
262  * Represents a qualified package
263  *
264  * @property segments the segments representing the package
265  */
266 internal class PackageName internal constructor(internal val segments: List<String>) {
267     /**
268      * The Java-style package name for this [Name], separated with `.`
269      */
270     val javaPackageName: String
271         get() = segments.joinToString(".")
272 }
273 
274 /**
275  * Represents the qualified name for an element
276  *
277  * @property pkg the package for this element
278  * @property nameSegments the segments representing the element - there can be multiple in the
279  * case of nested classes.
280  */
281 internal class Name internal constructor(
282     private val pkg: PackageName,
283     private val nameSegments: List<String>
284 ) {
285     /**
286      * The short name for this [Name]
287      */
288     val shortName: String
289         get() = nameSegments.last()
290 
291     /**
292      * The Java-style fully qualified name for this [Name], separated with `.`
293      */
294     val javaFqn: String
295         get() = pkg.segments.joinToString(".", postfix = ".") +
296             nameSegments.joinToString(".")
297 
298     /**
299      * The [ClassName] for use with kotlinx.metadata. Note that in kotlinx.metadata the actual
300      * type might be different from the underlying JVM type, for example:
301      * kotlin/Int -> java/lang/Integer
302      */
303     val kmClassName: ClassName
304         get() = pkg.segments.joinToString("/", postfix = "/") +
305             nameSegments.joinToString(".")
306 }
307 
308 private typealias ClassName = String
309