1 /*
2  * 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 @file:Suppress("UnstableApiUsage")
18 
19 package androidx.compose.ui.lint
20 
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 org.jetbrains.kotlin.psi.KtCallableDeclaration
29 import org.jetbrains.uast.UClass
30 import org.jetbrains.uast.UMethod
31 import org.jetbrains.uast.UTypeReferenceExpression
32 import org.jetbrains.uast.UastFacade
33 import org.jetbrains.uast.convertWithParent
34 import org.jetbrains.uast.toUElement
35 
36 /**
37  * Detects subclasses of ModifierNodeElement that do not override the base `inspectableProperties`
38  * function. Classes that extend from another class which overrides this function satisfy the
39  * inspection.
40  *
41  * We suggest overriding this method to provide more accurate, complete, and consistent data in the
42  * layout inspector. This check reports an error for AndroidX libraries, since we want to ensure
43  * each modifier has full inspection support. Additionally, the base implementation can only work if
44  * kotlin-reflect is in the classpath, which is avoided by having a custom implementation.
45  */
46 class ModifierNodeInspectablePropertiesDetector : Detector(), Detector.UastScanner {
47 
applicableSuperClassesnull48     override fun applicableSuperClasses(): List<String> {
49         return listOf("androidx.compose.ui.node.ModifierNodeElement")
50     }
51 
visitClassnull52     override fun visitClass(context: JavaContext, declaration: UClass) {
53         if (declaration.qualifiedName == ModifierNodeElementFqName) {
54             return
55         }
56 
57         if (declaration.getInspectablePropertiesFunctionOverride() == null) {
58             context.report(
59                 ModifierNodeInspectableProperties,
60                 declaration,
61                 context.getNameLocation(declaration),
62                 "${declaration.name} does not override $InspectionFunName(). The layout " +
63                     "inspector will use the default implementation of this function, which " +
64                     "will attempt to read ${declaration.name}'s properties reflectively. " +
65                     "Override $InspectionFunName() if you'd like to customize this modifier's " +
66                     "presentation in the layout inspector."
67             )
68         }
69     }
70 
71     @Suppress("NO_TAIL_CALLS_FOUND", "NON_TAIL_RECURSIVE_CALL")
getInspectablePropertiesFunctionOverridenull72     private tailrec fun UClass?.getInspectablePropertiesFunctionOverride(): UMethod? {
73         if (this == null || qualifiedName == ModifierNodeElementFqName) {
74             return null
75         }
76 
77         return uastDeclarations.filterIsInstance<UMethod>().firstOrNull {
78             it.hasInspectablePropertiesSignature()
79         }
80             ?: UastFacade.convertWithParent<UClass>(javaPsi.superClass)
81                 ?.getInspectablePropertiesFunctionOverride()
82     }
83 
hasInspectablePropertiesSignaturenull84     private fun UMethod.hasInspectablePropertiesSignature(): Boolean {
85         return name == InspectionFunName &&
86             parameters.size == 1 && // The only argument is the receiver
87             receiverFqType == InspectionFunFqReceiver
88     }
89 
90     private val UMethod.receiverFqType: String?
91         get() {
92             val receiverTypeRef = (sourcePsi as? KtCallableDeclaration)?.receiverTypeReference
93             return (receiverTypeRef?.toUElement() as? UTypeReferenceExpression)?.getQualifiedName()
94         }
95 
96     companion object {
97         private const val ModifierNodeElementFqName = "androidx.compose.ui.node.ModifierNodeElement"
98         private const val InspectionFunName = "inspectableProperties"
99         private const val InspectionFunFqReceiver = "androidx.compose.ui.platform.InspectorInfo"
100 
101         val ModifierNodeInspectableProperties =
102             Issue.create(
103                 "ModifierNodeInspectableProperties",
104                 "ModifierNodeElement missing inspectableProperties",
105                 "ModifierNodeElements may override inspectableProperties() to provide information " +
106                     "about the modifier in the layout inspector. The default implementation attempts " +
107                     "to read all of the properties on the class reflectively, which may not " +
108                     "comprehensively or effectively describe the modifier.",
109                 Category.PRODUCTIVITY,
110                 4,
111                 Severity.INFORMATIONAL,
112                 Implementation(
113                     ModifierNodeInspectablePropertiesDetector::class.java,
114                     Scope.JAVA_FILE_SCOPE
115                 )
116             )
117     }
118 }
119