1 /*
2  * Copyright 2021 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.build.lint
20 
21 import com.android.tools.lint.client.api.UElementHandler
22 import com.android.tools.lint.detector.api.Category
23 import com.android.tools.lint.detector.api.Detector
24 import com.android.tools.lint.detector.api.Implementation
25 import com.android.tools.lint.detector.api.Incident
26 import com.android.tools.lint.detector.api.Issue
27 import com.android.tools.lint.detector.api.JavaContext
28 import com.android.tools.lint.detector.api.Scope
29 import com.android.tools.lint.detector.api.Severity
30 import com.android.tools.lint.detector.api.SourceCodeScanner
31 import org.jetbrains.uast.UClass
32 import org.jetbrains.uast.UDeclaration
33 import org.jetbrains.uast.UElement
34 import org.jetbrains.uast.UFile
35 import org.jetbrains.uast.ULiteralExpression
36 import org.jetbrains.uast.UMethod
37 import org.jetbrains.uast.UVariable
38 
39 /**
40  * Detects usages of IntelliJ's per-line suppression, which is only valid within IntelliJ-based
41  * tools, and suggests replacement with the Java-compatible `@SuppressWarnings` annotation.
42  *
43  * Adapted from Android Studio's `TerminologyDetector` lint check.
44  */
45 class IdeaSuppressionDetector : Detector(), SourceCodeScanner {
46 
getApplicableUastTypesnull47     override fun getApplicableUastTypes(): List<Class<out UElement?>> {
48         // Everything that we'd expect to see a suppression on.
49         return listOf(
50             UFile::class.java,
51             UVariable::class.java,
52             UMethod::class.java,
53             UClass::class.java,
54             ULiteralExpression::class.java
55         )
56     }
57 
createUastHandlernull58     override fun createUastHandler(context: JavaContext): UElementHandler {
59         // We're using a UAST visitor here instead of just visiting the file
60         // as raw text since we'd like to only visit declarations, comments and strings,
61         // not for example class, method and field *references* to APIs outside of
62         // our control
63         return object : UElementHandler() {
64             // There's some duplication in comments between UFile#allCommentsInFile
65             // and the comments returned for each declaration, but unfortunately each
66             // one is missing some from the other so we need to check both and just
67             // keep track of the ones we've checked so we don't report errors multiple
68             // times
69             private val checkedComments = mutableSetOf<String>()
70 
71             override fun visitFile(node: UFile) {
72                 checkedComments.clear()
73                 for (comment in node.allCommentsInFile) {
74                     if (comment.uastParent is UDeclaration) { // handled in checkDeclaration
75                         continue
76                     }
77                     val contents = comment.text
78                     checkedComments.add(contents)
79                     visitComment(context, comment, contents)
80                 }
81             }
82 
83             override fun visitVariable(node: UVariable) {
84                 checkDeclaration(node, node.name)
85             }
86 
87             override fun visitMethod(node: UMethod) {
88                 checkDeclaration(node, node.name)
89             }
90 
91             override fun visitClass(node: UClass) {
92                 checkDeclaration(node, node.name)
93             }
94 
95             private fun checkDeclaration(node: UDeclaration, name: String?) {
96                 name ?: return
97                 visitComment(context, node, name)
98                 for (comment in node.comments) {
99                     val contents = comment.text
100                     if (checkedComments.add(contents)) {
101                         visitComment(context, comment, contents)
102                     }
103                 }
104             }
105 
106             override fun visitLiteralExpression(node: ULiteralExpression) {
107                 if (node.isString) {
108                     val string = node.value as? String ?: return
109                     visitComment(context, node, string)
110                 }
111             }
112         }
113     }
114 
115     /**
116      * Checks the text in [source].
117      *
118      * If it finds matches in the string, it will report errors into the given context. The
119      * associated AST [element] is used to look look up suppress annotations and to find the right
120      * error range.
121      */
visitCommentnull122     private fun visitComment(
123         context: JavaContext,
124         element: UElement,
125         source: CharSequence,
126     ) {
127         if (source.startsWith("//noinspection ")) {
128             val warnings = source.split(" ").drop(1).filter { JAVA_WARNINGS.contains(it) }
129             if (warnings.isNotEmpty()) {
130                 val args = warnings.joinToString(", ") { "\"$it\"" }
131                 val incident =
132                     Incident(context)
133                         .issue(ISSUE)
134                         .location(context.getNameLocation(element))
135                         .message(
136                             "Uses IntelliJ-specific suppression, should use" +
137                                 " `@SuppressWarnings($args)`"
138                         )
139                         .scope(element)
140                 context.report(incident)
141             }
142         }
143     }
144 
145     companion object {
146         // Warnings that the Java compiler cares about and should not be suppressed inline.
147         private val JAVA_WARNINGS = listOf("deprecation")
148 
149         val ISSUE =
150             Issue.create(
151                 "IdeaSuppression",
152                 "Suppression using `//noinspection` is not supported by the Java compiler",
153                 "Per-line suppression using `//noinspection` is not supported by the Java compiler " +
154                     "and will not suppress build-time warnings. Instead, use the `@SuppressWarnings` " +
155                     "annotation on the containing method or class.",
156                 Category.CORRECTNESS,
157                 5,
158                 Severity.ERROR,
159                 Implementation(IdeaSuppressionDetector::class.java, Scope.JAVA_FILE_SCOPE),
160             )
161     }
162 }
163