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