1 /*
<lambda>null2  * Copyright (C) 2024 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 android.adservices.lint.test
18 
19 import android.adservices.lint.test.ErrorLogUtilMockingUsageDetector.Constants.ERROR_LOG_UTIL
20 import android.adservices.lint.test.ErrorLogUtilMockingUsageDetector.Constants.INVALID_ANNOTATION_MESSAGE
21 import android.adservices.lint.test.ErrorLogUtilMockingUsageDetector.Constants.INVALID_MOCKING_MESSAGE
22 import android.adservices.lint.test.ErrorLogUtilMockingUsageDetector.Constants.MOCK_STATIC_ANNOTATION
23 import android.adservices.lint.test.ErrorLogUtilMockingUsageDetector.Constants.SPY_STATIC_ANNOTATION
24 import android.adservices.lint.test.ErrorLogUtilMockingUsageDetector.Constants.SUPERCLASS
25 import com.android.tools.lint.client.api.UElementHandler
26 import com.android.tools.lint.detector.api.*
27 import com.intellij.psi.PsiMethod
28 import org.jetbrains.uast.*
29 
30 class ErrorLogUtilMockingUsageDetector : Detector(), SourceCodeScanner {
31     object Constants {
32         const val MOCK_STATIC_ANNOTATION = "MockStatic"
33         const val SPY_STATIC_ANNOTATION = "SpyStatic"
34         const val ERROR_LOG_UTIL = "ErrorLogUtil"
35         const val INVALID_ANNOTATION_MESSAGE = "Do not spy or mock ErrorLogUtil"
36         const val INVALID_MOCKING_MESSAGE = "Do not mock ErrorLogUtil behavior"
37 
38         // Only allow ErrorLogUtil spy/mock annotation in the superclass
39         const val SUPERCLASS = "AdServicesExtendedMockitoTestCase"
40     }
41 
42     override fun getApplicableUastTypes() = listOf(UAnnotation::class.java)
43 
44     override fun getApplicableMethodNames(): List<String> {
45         return listOf("doNothingOnErrorLogUtilError", "when")
46     }
47 
48     override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
49         if (method.name == "when" && !isMockingErrorLogUtilInvocation(node)) {
50             return
51         }
52 
53         if (isSubclassOfAdServicesExtendedMockitoTestCase(findUClass(node))) {
54             context.report(
55                 MOCKING_INVOCATION_ISSUE,
56                 node,
57                 context.getLocation(node),
58                 INVALID_MOCKING_MESSAGE
59             )
60         }
61     }
62 
63     override fun createUastHandler(context: JavaContext): UElementHandler? {
64         return object : UElementHandler() {
65             override fun visitAnnotation(node: UAnnotation) {
66                 if (isMockOrSpyStaticAnnotation(node)) {
67                     val annotationArgs = node.attributeValues
68                     annotationArgs.forEach { arg ->
69                         val argValue = arg.expression.asSourceString()
70                         if (
71                             argValue.contains(ERROR_LOG_UTIL) &&
72                                 findUClass(node)?.name != SUPERCLASS
73                         ) {
74                             context.report(
75                                 INVALID_ANNOTATION_ISSUE,
76                                 node,
77                                 context.getLocation(node),
78                                 INVALID_ANNOTATION_MESSAGE
79                             )
80                         }
81                     }
82                 }
83             }
84         }
85     }
86 
87     private fun isMockOrSpyStaticAnnotation(node: UAnnotation): Boolean {
88         val annotation = node.qualifiedName?.substringAfterLast('.')
89         return annotation == MOCK_STATIC_ANNOTATION || annotation == SPY_STATIC_ANNOTATION
90     }
91 
92     private fun findUClass(node: UElement): UClass? {
93         var parent: UElement? = node.uastParent
94         while (parent != null) {
95             if (parent is UClass) {
96                 return parent
97             }
98             parent = parent.uastParent
99         }
100 
101         return null // UClass not found
102     }
103 
104     private fun isSubclassOfAdServicesExtendedMockitoTestCase(node: UClass?): Boolean {
105         var currentClass = node
106         while (currentClass != null) {
107             val superClass = currentClass.javaPsi.superClass ?: break
108             if (superClass.name == SUPERCLASS) {
109                 return true
110             }
111             currentClass =
112                 UastFacade.convertElementWithParent(superClass, UClass::class.java) as? UClass
113         }
114 
115         return false
116     }
117 
118     private fun isMockingErrorLogUtilInvocation(node: UCallExpression): Boolean {
119         return node.valueArguments.size == 1 &&
120             node.valueArguments[0].asSourceString().contains(ERROR_LOG_UTIL)
121     }
122 
123     companion object Issues {
124         val INVALID_ANNOTATION_ISSUE =
125             Issue.create(
126                 id = "DoNotMockOrSpyErrorLogUtil",
127                 briefDescription =
128                     "Do not define @SpyStatic(ErrorLogUtil.class) or @MockStatic(ErrorLogUtil.class)",
129                 explanation =
130                     """
131                         All subclasses that extend AdServicesExtendedMockitoTestCase are
132                         automatically configured to spy ErrorLogUtil for AdServicesLoggingUsageRule.
133                         Simply use @ExpectErrorLogUtilCall and/or @ExpectErrorLogUtilWithExceptionCall
134                         directly over test methods to verify ErrorLogUtil calls.
135                         """,
136                 moreInfo = "documentation/ErrorLogUtilMockingUsageDetector.md",
137                 category = Category.COMPLIANCE,
138                 severity = Severity.ERROR,
139                 implementation =
140                     Implementation(
141                         ErrorLogUtilMockingUsageDetector::class.java,
142                         Scope.JAVA_FILE_SCOPE
143                     )
144             )
145 
146         val MOCKING_INVOCATION_ISSUE =
147             Issue.create(
148                 id = "DoNotMockErrorLogUtilBehavior",
149                 briefDescription = "Do not mock ErrorLogUtil invocation behavior explicitly",
150                 explanation =
151                     """
152                         All subclasses that extend AdServicesExtendedMockitoTestCase are
153                         automatically configured to spy ErrorLogUtil for AdServicesLoggingUsageRule.
154                         ErrorLogUtil invocation behavior is already mocked in a specific way for the
155                         rule to work; do not override it by mocking ErrorLogUtil behavior. Simply use
156                         @ExpectErrorLogUtilCall and/or @ExpectErrorLogUtilWithExceptionCall directly
157                         over test methods to verify ErrorLogUtil calls.
158                         """,
159                 moreInfo = "documentation/ErrorLogUtilMockingUsageDetector.md",
160                 category = Category.COMPLIANCE,
161                 severity = Severity.ERROR,
162                 implementation =
163                     Implementation(
164                         ErrorLogUtilMockingUsageDetector::class.java,
165                         Scope.JAVA_FILE_SCOPE
166                     )
167             )
168     }
169 }
170