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