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 @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.LintFix
29 import com.android.tools.lint.detector.api.Scope
30 import com.android.tools.lint.detector.api.Severity
31 import com.intellij.psi.PsiMethod
32 import java.util.EnumSet
33 import org.jetbrains.uast.UAnnotation
34 import org.jetbrains.uast.UClass
35 
36 /** Checks for usages of @org.junit.Ignore at the class level. */
37 class IgnoreClassLevelDetector : Detector(), Detector.UastScanner {
38 
getApplicableUastTypesnull39     override fun getApplicableUastTypes() = listOf(UAnnotation::class.java)
40 
41     override fun createUastHandler(context: JavaContext): UElementHandler {
42         return AnnotationChecker(context)
43     }
44 
45     private inner class AnnotationChecker(val context: JavaContext) : UElementHandler() {
visitAnnotationnull46         override fun visitAnnotation(node: UAnnotation) {
47             if (node.qualifiedName == "org.junit.Ignore" && node.uastParent is UClass) {
48                 val incident =
49                     Incident(context)
50                         .issue(ISSUE)
51                         .location(context.getNameLocation(node))
52                         .message(
53                             "@Ignore should not be used at the class level. Move the annotation " +
54                                 "to each test individually."
55                         )
56                         .scope(node)
57                 context.report(incident)
58             }
59         }
60     }
61 
62     /**
63      * Creates a LintFix which removes the @Ignore annotation from the class and adds it to each
64      * individual test method.
65      *
66      * TODO(b/235340679): This is currently unused because of issues described in the method body.
67      */
68     @Suppress("unused")
createFixnull69     private fun createFix(testClass: UClass, context: JavaContext, annotation: String): LintFix {
70         val fix =
71             fix()
72                 .name("Annotate each test method and remove the class-level annotation")
73                 .composite()
74 
75         for (method in testClass.allMethods) {
76             if (method.isTestMethod()) {
77                 val methodFix =
78                     fix()
79                         // The replace param on annotate doesn't work: if @Ignore is already present
80                         // on
81                         // the method, the annotation is added again instead of being replaced.
82                         .annotate("org.junit.Ignore", context, method, true)
83                         .build()
84                 fix.add(methodFix)
85             }
86         }
87 
88         val classFix =
89             fix()
90                 .replace()
91                 // This requires the exact text of the class annotation to be passed to this
92                 // function.
93                 // This can be gotten with `node.sourcePsi?.node?.text!!`, but `text`'s doc says
94                 // using
95                 // it should be avoided, so this isn't the best solution.
96                 .text(annotation)
97                 .with("")
98                 .reformat(true)
99                 .build()
100         fix.add(classFix)
101 
102         return fix.build()
103     }
104 
105     /** Checks if this PsiMethod has a @org.junit.Test annotation */
PsiMethodnull106     private fun PsiMethod.isTestMethod(): Boolean {
107         for (annotation in this.annotations) {
108             if (annotation.qualifiedName == "org.junit.Test") {
109                 return true
110             }
111         }
112         return false
113     }
114 
115     companion object {
116         val ISSUE =
117             Issue.create(
118                 "IgnoreClassLevelDetector",
119                 "@Ignore should not be used at the class level.",
120                 "Using @Ignore at the class level instead of annotating each individual " +
121                     "test causes errors in Android Test Hub.",
122                 Category.CORRECTNESS,
123                 5,
124                 Severity.ERROR,
125                 Implementation(
126                     IgnoreClassLevelDetector::class.java,
127                     EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
128                 )
129             )
130     }
131 }
132