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