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