1 /*
<lambda>null2  * Copyright 2022 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.foundation.lint
20 
21 import androidx.compose.lint.Names
22 import androidx.compose.lint.inheritsFrom
23 import androidx.compose.lint.isInPackageName
24 import androidx.compose.lint.toKmFunction
25 import com.android.tools.lint.detector.api.Category
26 import com.android.tools.lint.detector.api.Detector
27 import com.android.tools.lint.detector.api.Implementation
28 import com.android.tools.lint.detector.api.Issue
29 import com.android.tools.lint.detector.api.JavaContext
30 import com.android.tools.lint.detector.api.Scope
31 import com.android.tools.lint.detector.api.Severity
32 import com.android.tools.lint.detector.api.SourceCodeScanner
33 import com.android.tools.lint.detector.api.UastLintUtils.Companion.tryResolveUDeclaration
34 import com.intellij.psi.PsiMethod
35 import java.util.EnumSet
36 import kotlin.metadata.KmClassifier
37 import org.jetbrains.kotlin.psi.KtProperty
38 import org.jetbrains.uast.UCallExpression
39 import org.jetbrains.uast.UDeclaration
40 import org.jetbrains.uast.UExpression
41 import org.jetbrains.uast.ULocalVariable
42 import org.jetbrains.uast.UMethod
43 import org.jetbrains.uast.USimpleNameReferenceExpression
44 import org.jetbrains.uast.UVariable
45 import org.jetbrains.uast.skipParenthesizedExprDown
46 import org.jetbrains.uast.toUElement
47 import org.jetbrains.uast.visitor.AbstractUastVisitor
48 
49 /**
50  * [Detector] that checks calls to Modifier.offset that use a non-lambda overload but read from
51  * dynamic/state variables. It is recommended to use the lambda overload in those cases for
52  * performance improvements
53  */
54 class NonLambdaOffsetModifierDetector : Detector(), SourceCodeScanner {
55 
56     override fun getApplicableMethodNames(): List<String> =
57         listOf(
58             FoundationNames.Layout.Offset.shortName,
59             FoundationNames.Layout.AbsoluteOffset.shortName
60         )
61 
62     override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
63         // Non Modifier Offset
64         if (!method.isInPackageName(FoundationNames.Layout.PackageName)) return
65 
66         if (method.isDesiredOffsetOverload() && hasStateBackedArguments(node)) {
67             context.report(
68                 UseOfNonLambdaOverload,
69                 node,
70                 context.getNameLocation(node),
71                 ReportMainMessage
72             )
73         }
74     }
75 
76     /**
77      * For the form of `Modifier.<method-name>(Dp, Dp): Modifier`.
78      *
79      * Note that method-name is already handled by [getApplicableMethodNames].
80      */
81     private fun PsiMethod.isDesiredOffsetOverload(): Boolean {
82         val kmFunction = this.toKmFunction() ?: return false
83         val receiverClassifier = kmFunction.receiverParameterType?.classifier ?: return false
84         val returnTypeClassifier = kmFunction.returnType.classifier
85 
86         if (receiverClassifier != ModifierClassifier) {
87             return false
88         }
89         if (returnTypeClassifier != ModifierClassifier) {
90             return false
91         }
92 
93         val valueParameters = kmFunction.valueParameters
94         if (valueParameters.size != 2) {
95             return false
96         }
97         return valueParameters.all { it.type.classifier == DpClassifier }
98     }
99 
100     private fun hasStateBackedArguments(node: UCallExpression): Boolean {
101         var dynamicArguments = false
102 
103         node.valueArguments.forEach { expression ->
104             expression.accept(
105                 object : AbstractUastVisitor() {
106                     override fun visitSimpleNameReferenceExpression(
107                         node: USimpleNameReferenceExpression
108                     ): Boolean {
109                         val declaration = node.tryResolveUDeclaration() ?: return false
110                         dynamicArguments = dynamicArguments || declaration.isCompositionAwareType()
111                         return dynamicArguments
112                     }
113                 }
114             )
115         }
116 
117         return dynamicArguments
118     }
119 
120     companion object {
121         const val ReportMainMessage =
122             "State backed values should use the lambda overload of Modifier.offset"
123 
124         const val IssueId = "UseOfNonLambdaOffsetOverload"
125 
126         val UseOfNonLambdaOverload =
127             Issue.create(
128                 IssueId,
129                 "Modifier.offset{ } is preferred over Modifier.offset() for " +
130                     "`State` backed arguments.",
131                 "`Modifier.offset()` is recommended to be used with static arguments only to " +
132                     "avoid unnecessary recompositions. `Modifier.offset{ }` is " +
133                     "preferred in the cases where the arguments are backed by a `State`.",
134                 Category.PERFORMANCE,
135                 3,
136                 Severity.WARNING,
137                 Implementation(
138                     NonLambdaOffsetModifierDetector::class.java,
139                     EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
140                 )
141             )
142     }
143 }
144 
isCompositionAwareTypenull145 private fun UDeclaration.isCompositionAwareType(): Boolean {
146     return isDelegateOfState() || isMethodFromStateOrAnimatable() || isStateOrAnimatableVariable()
147 }
148 
isStateOrAnimatableVariablenull149 private fun UDeclaration.isStateOrAnimatableVariable(): Boolean {
150     return (this is UVariable) &&
151         (type.inheritsFrom(Names.Runtime.State) ||
152             type.inheritsFrom(Names.Animation.Core.Animatable))
153 }
154 
155 /** Special handling of implicit receiver types */
UDeclarationnull156 private fun UDeclaration.isMethodFromStateOrAnimatable(): Boolean {
157     val argument = this as? UMethod
158     val containingClass = argument?.containingClass ?: return false
159 
160     return containingClass.inheritsFrom(Names.Runtime.State) ||
161         containingClass.inheritsFrom(Names.Animation.Core.Animatable)
162 }
163 
isDelegateOfStatenull164 private fun UDeclaration.isDelegateOfState(): Boolean {
165     val localVariable = this as? ULocalVariable
166     val ktProperty = localVariable?.sourcePsi as? KtProperty ?: return false
167     val delegateExpression =
168         ktProperty.delegate?.expression.toUElement() as? UExpression ?: return false
169     val expressionType = delegateExpression.skipParenthesizedExprDown().getExpressionType()
170     return expressionType?.inheritsFrom(Names.Runtime.State) ?: false
171 }
172 
173 private val ModifierClassifier = KmClassifier.Class(Names.Ui.Modifier.kmClassName)
174 
175 private val DpClassifier = KmClassifier.Class(Names.Ui.Unit.Dp.kmClassName)
176