1 /*
2  * Copyright (C) 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 com.android.internal.systemui.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.Issue
24 import com.android.tools.lint.detector.api.JavaContext
25 import com.android.tools.lint.detector.api.Scope
26 import com.android.tools.lint.detector.api.Severity
27 import com.android.tools.lint.detector.api.SourceCodeScanner
28 import java.util.EnumSet
29 import java.util.regex.Pattern
30 import org.jetbrains.uast.UAnnotation
31 import org.jetbrains.uast.UElement
32 
33 @Suppress("UnstableApiUsage") // For linter api
34 class DemotingTestWithoutBugDetector : Detector(), SourceCodeScanner {
35     override fun getApplicableUastTypes(): List<Class<out UElement>> {
36         return listOf(UAnnotation::class.java)
37     }
38 
39     override fun createUastHandler(context: JavaContext): UElementHandler {
40         return object : UElementHandler() {
41             override fun visitAnnotation(node: UAnnotation) {
42                 // Annotations having int bugId field
43                 if (node.qualifiedName in DEMOTING_ANNOTATION_BUG_ID) {
44                     if (!containsBugId(node)) {
45                         val location = context.getLocation(node)
46                         val message =
47                             """Please attach a bug id to track demoted test, """ +
48                                 """e.g. @FlakyTest(bugId = 123)"""
49                         context.report(ISSUE, node, location, message)
50                     }
51                 }
52                 // @Ignore has a String field for specifying reasons
53                 if (node.qualifiedName == DEMOTING_ANNOTATION_IGNORE) {
54                     if (!containsBugString(node)) {
55                         val location = context.getLocation(node)
56                         val message =
57                             """Please attach a bug to track demoted test, e.g. @Ignore("b/123")"""
58                         context.report(ISSUE, node, location, message)
59                     }
60                 }
61             }
62         }
63     }
64 
65     private fun containsBugId(node: UAnnotation): Boolean {
66         val bugId = node.findAttributeValue("bugId")?.evaluate() as Int?
67         return bugId != null && bugId > 0
68     }
69 
70     private fun containsBugString(node: UAnnotation): Boolean {
71         val reason = node.findAttributeValue("value")?.evaluate() as String?
72         val bugPattern = Pattern.compile("b/\\d+")
73         return reason != null && bugPattern.matcher(reason).find()
74     }
75 
76     companion object {
77         val DEMOTING_ANNOTATION_BUG_ID =
78             listOf(
79                 "androidx.test.filters.FlakyTest",
80                 "android.platform.test.annotations.FlakyTest",
81                 "android.platform.test.rule.PlatinumRule.Platinum",
82             )
83 
84         const val DEMOTING_ANNOTATION_IGNORE = "org.junit.Ignore"
85 
86         @JvmField
87         val ISSUE: Issue =
88             Issue.create(
89                 id = "DemotingTestWithoutBug",
90                 briefDescription = "Demoting a test without attaching a bug.",
91                 explanation =
92                     """
93                     Annotations (`@FlakyTest`) demote tests to an unmonitored \
94                     test suite. Please set the `bugId` field in such annotations to track \
95                     the test status.
96                     """,
97                 category = Category.TESTING,
98                 priority = 8,
99                 severity = Severity.WARNING,
100                 implementation =
101                     Implementation(
102                         DemotingTestWithoutBugDetector::class.java,
103                         EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
104                     )
105             )
106     }
107 }
108