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