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 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.LocationType 27 import com.android.tools.lint.detector.api.Scope 28 import com.android.tools.lint.detector.api.Severity 29 import com.android.tools.lint.detector.api.isKotlin 30 import com.intellij.psi.PsiComment 31 import org.jetbrains.uast.UAnonymousClass 32 import org.jetbrains.uast.UDeclaration 33 import org.jetbrains.uast.UMethod 34 import org.jetbrains.uast.UastVisibility 35 import org.jetbrains.uast.getContainingUClass 36 37 class DeprecationMismatchDetector : Detector(), Detector.UastScanner { 38 getApplicableUastTypesnull39 override fun getApplicableUastTypes() = listOf(UDeclaration::class.java) 40 41 override fun createUastHandler(context: JavaContext): UElementHandler { 42 return DeprecationChecker(context) 43 } 44 45 private inner class DeprecationChecker(val context: JavaContext) : UElementHandler() { visitDeclarationnull46 override fun visitDeclaration(node: UDeclaration) { 47 // This check is only applicable for Java, the Kotlin @Deprecated has a message field 48 if (isKotlin(node.lang)) return 49 50 // This check is for API elements, not anonymous class declarations 51 if (node is UAnonymousClass) return 52 if (node is UMethod && node.name == "<anon-init>") return 53 54 // Not necessary if the element isn't public API 55 if (!applicableVisibilities.contains(node.visibility)) return 56 57 // Check if @deprecated and @Deprecated don't match 58 val hasDeprecatedDocTag = node.comments.any { it.text.contains("@deprecated") } 59 val hasDeprecatedAnnotation = node.hasAnnotation(DEPRECATED_ANNOTATION) 60 if (hasDeprecatedDocTag == hasDeprecatedAnnotation) return 61 62 // Proto-generated files are not part of the public API surface 63 if ( 64 node.containingFile.children.filterIsInstance<PsiComment>().any { 65 it.text.contains("Generated by the protocol buffer compiler. DO NOT EDIT!") 66 } 67 ) 68 return 69 70 // Methods that override deprecated methods can inherit docs from the original method 71 if ( 72 node is UMethod && 73 node.hasAnnotation(OVERRIDE_ANNOTATION) && 74 (node.comments.isEmpty() || 75 node.comments.any { it.text.contains("@inheritDoc") }) 76 ) 77 return 78 79 // @RestrictTo elements aren't part of the public API surface 80 if ( 81 node.hasAnnotation(RESTRICT_TO_ANNOTATION) || 82 node.getContainingUClass()?.hasAnnotation(RESTRICT_TO_ANNOTATION) == true 83 ) 84 return 85 86 // The mismatch is in a public API, report the error 87 val baseIncident = 88 Incident(context) 89 .issue(ISSUE) 90 .location(context.getLocation(node, LocationType.NAME)) 91 .scope(node) 92 93 val incident = 94 if (hasDeprecatedAnnotation) { 95 // No auto-fix for this case since developers should write a comment with 96 // details 97 baseIncident.message( 98 "Items annotated with @Deprecated must have a @deprecated doc tag" 99 ) 100 } else { 101 val fix = 102 fix() 103 .name("Annotate with @Deprecated") 104 .annotate(DEPRECATED_ANNOTATION, context, node) 105 .autoFix() 106 .build() 107 108 baseIncident 109 .fix(fix) 110 .message( 111 "Items with a @deprecated doc tag must be annotated with @Deprecated" 112 ) 113 } 114 115 context.report(incident) 116 } 117 } 118 119 companion object { 120 val ISSUE = 121 Issue.create( 122 "DeprecationMismatch", 123 "@Deprecated (annotation) and @deprecated (doc tag) must go together", 124 "A deprecated API should both be annotated with @Deprecated and have a " + 125 "@deprecated doc tag.", 126 Category.CORRECTNESS, 127 5, 128 Severity.ERROR, 129 Implementation(DeprecationMismatchDetector::class.java, Scope.JAVA_FILE_SCOPE) 130 ) 131 132 private const val DEPRECATED_ANNOTATION = "java.lang.Deprecated" 133 private const val RESTRICT_TO_ANNOTATION = "androidx.annotation.RestrictTo" 134 private const val OVERRIDE_ANNOTATION = "java.lang.Override" 135 private val applicableVisibilities = listOf(UastVisibility.PUBLIC, UastVisibility.PROTECTED) 136 } 137 } 138