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