1 /* 2 * 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 package androidx.build.lint 18 19 import com.android.tools.lint.client.api.UElementHandler 20 import com.android.tools.lint.detector.api.Category 21 import com.android.tools.lint.detector.api.Detector 22 import com.android.tools.lint.detector.api.Implementation 23 import com.android.tools.lint.detector.api.Incident 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.LintFix 27 import com.android.tools.lint.detector.api.Scope 28 import com.android.tools.lint.detector.api.Severity 29 import com.intellij.psi.PsiElement 30 import org.jetbrains.kotlin.asJava.toLightSetter 31 import org.jetbrains.kotlin.descriptors.annotations.AnnotationUseSiteTarget 32 import org.jetbrains.kotlin.lexer.KtTokens 33 import org.jetbrains.kotlin.psi.KtAnnotated 34 import org.jetbrains.kotlin.psi.KtAnnotationEntry 35 import org.jetbrains.kotlin.psi.KtClass 36 import org.jetbrains.kotlin.psi.KtClassBody 37 import org.jetbrains.kotlin.psi.KtConstructor 38 import org.jetbrains.kotlin.psi.KtFile 39 import org.jetbrains.kotlin.psi.KtModifierListOwner 40 import org.jetbrains.kotlin.psi.KtParameter 41 import org.jetbrains.kotlin.psi.KtProperty 42 import org.jetbrains.kotlin.psi.psiUtil.getParentOfType 43 import org.jetbrains.kotlin.psi.psiUtil.isPrivate 44 import org.jetbrains.kotlin.psi.psiUtil.isPropertyParameter 45 import org.jetbrains.uast.UAnnotation 46 import org.jetbrains.uast.UElement 47 import org.jetbrains.uast.UParameter 48 import org.jetbrains.uast.toUElement 49 50 class ExperimentalPropertyAnnotationDetector : Detector(), Detector.UastScanner { 51 getApplicableUastTypesnull52 override fun getApplicableUastTypes(): List<Class<out UElement>> = 53 listOf(UAnnotation::class.java, UParameter::class.java) 54 55 override fun createUastHandler(context: JavaContext): UElementHandler = 56 object : UElementHandler() { 57 /** 58 * Work around for b/406850340: annotations with the `property` use site target on 59 * parameters aren't visited directly with [visitAnnotation]. This finds those 60 * annotations and calls [visitAnnotation] on them. 61 */ 62 override fun visitParameter(node: UParameter) { 63 val ktParameter = node.sourcePsi as? KtParameter ?: return 64 val propertyAnnotations = 65 ktParameter.annotationEntries 66 .filter { 67 it.useSiteTarget?.getAnnotationUseSiteTarget() == 68 AnnotationUseSiteTarget.PROPERTY 69 } 70 .map { it.toUElement() } 71 .filterIsInstance<UAnnotation>() 72 for (propertyAnnotation in propertyAnnotations) { 73 visitAnnotation(propertyAnnotation) 74 } 75 } 76 77 override fun visitAnnotation(node: UAnnotation) { 78 val neededTargets = 79 mutableSetOf( 80 AnnotationUseSiteTarget.PROPERTY, 81 AnnotationUseSiteTarget.PROPERTY_GETTER, 82 AnnotationUseSiteTarget.PROPERTY_SETTER 83 ) 84 85 // If this annotation is not annotated with an experimental annotation, return 86 val resolved = node.resolve() 87 if ( 88 BanInappropriateExperimentalUsage.APPLICABLE_ANNOTATIONS.all { 89 context.evaluator.getAnnotation(resolved, it) == null 90 } 91 ) { 92 return 93 } 94 95 val type = node.qualifiedName ?: return 96 val source = node.sourcePsi as? KtAnnotationEntry ?: return 97 98 // Check that the annotation is applied to a property. Properties can also be 99 // defined as constructor parameters. 100 val parent = source.parent?.parent 101 when (parent) { 102 // Check if this check shouldn't apply to the property/parameter. 103 is KtProperty -> if (!appliesToProperty(parent)) return 104 is KtParameter -> if (!appliesToParameter(parent)) return 105 else -> return 106 } 107 val propertyParent = parent.parent 108 109 // Don't apply the lint to private properties 110 // parent is either a KtProperty or KtParameter, both are KtModifierListOwner 111 if ((parent as KtModifierListOwner).isPrivate()) return 112 113 // Don't apply the lint to properties in private classes 114 if (propertyParent.getParentOfType<KtClass>(true)?.isPrivate() == true) return 115 116 // Annotation on setter is only needed for mutable property with non-private setter 117 // Getter annotation is needed because the getter can't be private if the property 118 // isn't 119 if ( 120 !((parent as? KtProperty)?.hasVisibleSetter() 121 ?: (parent as KtParameter).hasSetter()) 122 ) { 123 neededTargets.remove(AnnotationUseSiteTarget.PROPERTY_SETTER) 124 } 125 126 // Find all usages of this annotation on the property 127 // parent is either a KtProperty or KtParameter, both are KtAnnotated 128 val existingTargets = 129 (parent as KtAnnotated) 130 .annotationEntries 131 .filter { type.endsWith(it.shortName?.identifier ?: "") } 132 .map { it.useSiteTarget?.getAnnotationUseSiteTarget() } 133 134 val existingTargetSet = 135 existingTargets 136 // A null target means the default, which is the property target 137 // Note this is true for parameters because experimental annotations don't 138 // apply to params. 139 .map { it ?: AnnotationUseSiteTarget.PROPERTY } 140 .toSet() 141 val missingTargets = neededTargets - existingTargetSet 142 143 if (missingTargets.isEmpty()) return 144 145 // If not all annotations are present but more than one is, only report the error on 146 // the first annotation to prevent duplicate errors 147 val target = source.useSiteTarget?.getAnnotationUseSiteTarget() 148 if (existingTargets.size > 1 && existingTargets.indexOf(target) != 0) return 149 150 val fix = createFix(type, parent, missingTargets) 151 val message = 152 "This property does not have all required annotations to correctly mark" + 153 " it as experimental." 154 val location = context.getLocation(node) 155 val incident = Incident(ISSUE, node, location, message, fix) 156 context.report(incident) 157 } 158 159 /** 160 * Whether the lint check should apply to [property]. The check only applies to top 161 * level or class properties, and does not apply to const, @JvmField, or delegated 162 * properties. 163 */ 164 fun appliesToProperty(property: KtProperty): Boolean { 165 // Only applies to properties defined at the top level or in classes 166 val propertyParent = property.parent 167 if ((propertyParent !is KtClassBody && propertyParent !is KtFile)) return false 168 169 // Don't apply lint to const properties, because they are static fields in java 170 if (property.modifierList?.node?.findChildByType(KtTokens.CONST_KEYWORD) != null) 171 return false 172 // Don't apply lint to @JvmField properties, because they are fields in java 173 if (property.annotationEntries.any { it.shortName.toString() == "JvmField" }) 174 return false 175 176 // Don't apply lint to delegated properties 177 if (property.delegate != null) return false 178 179 return true 180 } 181 182 /** 183 * Whether the lint check should apply to [parameter]. The check only applies to 184 * constructor property parameters, and should not apply if the constructor is private. 185 */ 186 fun appliesToParameter(parameter: KtParameter): Boolean { 187 if (!parameter.isPropertyParameter()) return false 188 189 // Don't apply to parameters of private constructors 190 if (parameter.getParentOfType<KtConstructor<*>>(true)?.isPrivate() == true) 191 return false 192 193 return true 194 } 195 196 fun KtProperty.hasVisibleSetter() = isVar && setter?.isPrivate() != true 197 198 fun KtParameter.hasSetter() = toLightSetter() != null 199 200 private fun createFix( 201 annotation: String, 202 annotated: PsiElement, 203 missingTargets: Set<AnnotationUseSiteTarget> 204 ): LintFix { 205 val fix = fix().name("Add missing annotations").composite() 206 207 for (target in missingTargets) { 208 // There's a compilation error when an experimental annotation is applied to a 209 // getter: 210 // https://kotlinlang.org/docs/opt-in-requirements.html#mark-api-elements 211 // Add it anyway because metalava needs it and suppress the error 212 if (target == AnnotationUseSiteTarget.PROPERTY_GETTER) { 213 val addSuppression = 214 fix() 215 .annotate( 216 "kotlin.Suppress(\"OPT_IN_MARKER_ON_WRONG_TARGET\")", 217 context, 218 annotated 219 ) 220 .build() 221 fix.add(addSuppression) 222 } 223 224 val addAnnotation = 225 fix() 226 // With replace = true, the existing annotation with a different target 227 // would 228 // be replaced. There shouldn't be an existing annotation with this 229 // target. 230 .annotate( 231 target.renderName + ":" + annotation, 232 context, 233 annotated, 234 replace = false 235 ) 236 .build() 237 fix.add(addAnnotation) 238 } 239 240 return fix.build().autoFix() 241 } 242 } 243 244 companion object { 245 val ISSUE = 246 Issue.create( 247 "ExperimentalPropertyAnnotation", 248 "Experimental properties need to have annotations targeting the" + 249 " property, getter, and (if applicable) setter.", 250 "Annotations on Kotlin properties which don't specify a use-site will " + 251 "only apply to the private backing field itself, and not to the getter or setter " + 252 "(see https://kotlinlang.org/docs/annotations.html#annotation-use-site-targets). " + 253 "Annotating the property use-site is required by the Kotlin compiler, the get " + 254 "use-site is required by Metalava, and the set use-site is required by Java " + 255 "clients, so all use-sites must be annotated.", 256 Category.CORRECTNESS, 257 5, 258 Severity.ERROR, 259 Implementation( 260 ExperimentalPropertyAnnotationDetector::class.java, 261 Scope.JAVA_FILE_SCOPE 262 ) 263 ) 264 } 265 } 266