1 /*
<lambda>null2  * Copyright 2023 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 package androidx.compose.foundation.lint
18 
19 import androidx.compose.lint.inheritsFrom
20 import androidx.compose.lint.isInPackageName
21 import com.android.tools.lint.detector.api.Category
22 import com.android.tools.lint.detector.api.Detector
23 import com.android.tools.lint.detector.api.Implementation
24 import com.android.tools.lint.detector.api.Issue
25 import com.android.tools.lint.detector.api.JavaContext
26 import com.android.tools.lint.detector.api.Scope
27 import com.android.tools.lint.detector.api.Severity
28 import com.android.tools.lint.detector.api.SourceCodeScanner
29 import com.android.tools.lint.detector.api.computeKotlinArgumentMapping
30 import com.intellij.psi.PsiMethod
31 import com.intellij.psi.PsiWildcardType
32 import com.intellij.psi.impl.source.PsiClassReferenceType
33 import java.util.EnumSet
34 import org.jetbrains.uast.UCallExpression
35 import org.jetbrains.uast.ULambdaExpression
36 import org.jetbrains.uast.USimpleNameReferenceExpression
37 import org.jetbrains.uast.UThisExpression
38 import org.jetbrains.uast.tryResolve
39 import org.jetbrains.uast.visitor.AbstractUastVisitor
40 
41 class BoxWithConstraintsDetector : Detector(), SourceCodeScanner {
42     override fun getApplicableMethodNames(): List<String> =
43         listOf(FoundationNames.Layout.BoxWithConstraints.shortName)
44 
45     override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
46         if (method.isInPackageName(FoundationNames.Layout.PackageName)) {
47             val contentArgument =
48                 computeKotlinArgumentMapping(node, method)
49                     .orEmpty()
50                     .filter { (_, parameter) -> parameter.name == "content" }
51                     .keys
52                     .filterIsInstance<ULambdaExpression>()
53                     .firstOrNull() ?: return
54 
55             var foundValidReference = false
56             contentArgument.accept(
57                 object : AbstractUastVisitor() {
58                     // Check for references to any property of BoxWithConstraintsScope
59                     override fun visitSimpleNameReferenceExpression(
60                         node: USimpleNameReferenceExpression
61                     ): Boolean {
62                         val reference =
63                             (node.tryResolve() as? PsiMethod)
64                                 ?: return foundValidReference // No need to continue if already
65                         // found
66                         if (
67                             reference.isInPackageName(FoundationNames.Layout.PackageName) &&
68                                 reference.containingClass?.name ==
69                                     FoundationNames.Layout.BoxWithConstraintsScope.shortName
70                         ) {
71                             foundValidReference = true
72                         }
73 
74                         // Check if reference is an extension property on BoxWithConstraintsScope
75                         if (
76                             reference.hierarchicalMethodSignature.parameterTypes
77                                 .firstOrNull()
78                                 ?.inheritsFrom(FoundationNames.Layout.BoxWithConstraintsScope) ==
79                                 true
80                         ) {
81                             foundValidReference = true
82                         }
83                         return foundValidReference
84                     }
85 
86                     // If this is referenced in the content lambda then consider
87                     // the constraints used.
88                     override fun visitThisExpression(node: UThisExpression): Boolean {
89                         foundValidReference = true
90                         return foundValidReference
91                     }
92 
93                     // Check function calls inside the content lambda to see if they
94                     // are using BoxWithConstraintsScope
95                     override fun visitCallExpression(node: UCallExpression): Boolean {
96                         val receiverType = node.receiverType ?: return foundValidReference
97 
98                         // Check for function calls with a BoxWithConstraintsScope receiver type
99                         if (
100                             receiverType.inheritsFrom(
101                                 FoundationNames.Layout.BoxWithConstraintsScope
102                             )
103                         ) {
104                             foundValidReference = true
105                             return foundValidReference
106                         }
107 
108                         // Check for calls to a lambda with a BoxWithConstraintsScope receiver type
109                         // e.g. BoxWithConstraintsScope.() -> Unit
110                         val firstChildReceiverType =
111                             (receiverType as? PsiClassReferenceType)
112                                 ?.reference
113                                 ?.typeParameters
114                                 ?.firstOrNull() ?: return foundValidReference
115 
116                         val resolvedWildcardType =
117                             (firstChildReceiverType as? PsiWildcardType)?.bound
118                         if (
119                             resolvedWildcardType?.inheritsFrom(
120                                 FoundationNames.Layout.BoxWithConstraintsScope
121                             ) == true
122                         ) {
123                             foundValidReference = true
124                         }
125 
126                         return foundValidReference
127                     }
128                 }
129             )
130             if (!foundValidReference) {
131                 context.report(
132                     UnusedConstraintsParameter,
133                     node,
134                     context.getLocation(contentArgument),
135                     "BoxWithConstraints scope is not used"
136                 )
137             }
138         }
139     }
140 
141     companion object {
142         val UnusedConstraintsParameter =
143             Issue.create(
144                 "UnusedBoxWithConstraintsScope",
145                 "BoxWithConstraints content should use the constraints provided " +
146                     "via BoxWithConstraintsScope",
147                 "The `content` lambda in BoxWithConstraints has a scope " +
148                     "which will include the incoming constraints. If this " +
149                     "scope is ignored, then the cost of subcomposition is being wasted and " +
150                     "this BoxWithConstraints should be replaced with a Box.",
151                 Category.CORRECTNESS,
152                 3,
153                 Severity.ERROR,
154                 Implementation(
155                     BoxWithConstraintsDetector::class.java,
156                     EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
157                 )
158             )
159     }
160 }
161