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 @file:Suppress("UnstableApiUsage")
18 
19 package androidx.compose.ui.lint
20 
21 import androidx.compose.lint.Names
22 import androidx.compose.lint.isComposable
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.intellij.psi.PsiMethod
33 import java.util.EnumSet
34 import org.jetbrains.uast.UCallExpression
35 import org.jetbrains.uast.USimpleNameReferenceExpression
36 import org.jetbrains.uast.getParameterForArgument
37 import org.jetbrains.uast.tryResolve
38 import org.jetbrains.uast.visitor.AbstractUastVisitor
39 
40 /**
41  * [Detector] that checks calls to Modifier.composed to make sure they actually reference a
42  * Composable function inside - otherwise there is no reason to use Modifier.composed, and since the
43  * resulting Modifier is not skippable, it will cause worse performance.
44  */
45 class ComposedModifierDetector : Detector(), SourceCodeScanner {
getApplicableMethodNamesnull46     override fun getApplicableMethodNames(): List<String> = listOf(Names.Ui.Composed.shortName)
47 
48     override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
49         if (!method.isInPackageName(Names.Ui.PackageName)) return
50 
51         val factoryLambda =
52             node.valueArguments.find { node.getParameterForArgument(it)?.name == "factory" }
53                 ?: return
54 
55         var hasComposableCall = false
56         factoryLambda.accept(
57             object : AbstractUastVisitor() {
58                 /** Visit function calls to see if the functions are composable */
59                 override fun visitCallExpression(node: UCallExpression): Boolean =
60                     (node.tryResolve() as? PsiMethod).hasComposableCall()
61 
62                 /**
63                  * Visit any simple name reference expressions and see if they resolve to a
64                  * composable function - for example if referencing a property with a composable
65                  * getter, such as CompositionLocal.current.
66                  */
67                 override fun visitSimpleNameReferenceExpression(
68                     node: USimpleNameReferenceExpression
69                 ): Boolean = (node.tryResolve() as? PsiMethod).hasComposableCall()
70 
71                 private fun PsiMethod?.hasComposableCall(): Boolean {
72                     if (this?.isComposable == true) {
73                         hasComposableCall = true
74                     }
75                     return hasComposableCall
76                 }
77             }
78         )
79 
80         if (!hasComposableCall) {
81             context.report(
82                 UnnecessaryComposedModifier,
83                 node,
84                 context.getNameLocation(node),
85                 "Unnecessary use of Modifier.composed"
86             )
87         }
88     }
89 
90     companion object {
91         val UnnecessaryComposedModifier =
92             Issue.create(
93                 "UnnecessaryComposedModifier",
94                 "Modifier.composed should only be used for modifiers that invoke @Composable functions",
95                 "`Modifier.composed` allows invoking @Composable functions when creating a `Modifier`" +
96                     " instance - for example, using `remember` to have instance-specific state, " +
97                     "allowing the same `Modifier` object to be safely used in multiple places. Using " +
98                     "`Modifier.composed` without calling any @Composable functions inside is " +
99                     "unnecessary, and since the Modifier is no longer skippable, this can cause a lot" +
100                     " of extra work inside the composed body, leading to worse performance.",
101                 Category.CORRECTNESS,
102                 3,
103                 Severity.WARNING,
104                 Implementation(
105                     ComposedModifierDetector::class.java,
106                     EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
107                 )
108             )
109     }
110 }
111