• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 package android.adservices.lint.prod
17 
18 import com.android.tools.lint.client.api.UElementHandler
19 import com.android.tools.lint.detector.api.Category
20 import com.android.tools.lint.detector.api.Detector
21 import com.android.tools.lint.detector.api.Implementation
22 import com.android.tools.lint.detector.api.Issue
23 import com.android.tools.lint.detector.api.SourceCodeScanner
24 import com.android.tools.lint.detector.api.Scope
25 import com.android.tools.lint.detector.api.Severity
26 import com.android.tools.lint.detector.api.JavaContext
27 import com.intellij.psi.PsiMethod;
28 import org.jetbrains.uast.UBinaryExpression
29 import org.jetbrains.uast.UCallExpression
30 import org.jetbrains.uast.UClass
31 import org.jetbrains.uast.UElement
32 import org.jetbrains.uast.UExpression
33 import org.jetbrains.uast.ULiteralExpression
34 import org.jetbrains.uast.UPolyadicExpression
35 import org.jetbrains.uast.UReferenceExpression
36 
37 /** Lint check for detecting invalid AdServices JobService */
38 class BackCompatJobServiceDetector : Detector(), SourceCodeScanner {
39 
40     companion object {
41         private const val JOB_SERVICE_CLASS = "android.app.job.JobService"
42         private const val ISSUE_DESCRIPTION =
43                 "Avoid using new classes in AdServices JobService field Initializers. Due to the " +
44                         "fact that ExtServices can OTA to any AdServices build, JobServices code " +
45                         "needs to be properly gated to avoid NoClassDefFoundError. " +
46                         "NoClassDefFoundError can happen when new class is used in ExtServices " +
47                         "build, and the error happens when the device OTA to old AdServices " +
48                         "build on T which does not contain the new class definition."
49 
50         // The following list of classes can be safely used in adservices JobService field
51         // initialiser
52         private val SAFE_CLASSES = listOf(
53                 "com.android.adservices.concurrency.AdServicesExecutors",
54                 "com.android.adservices.spe.AdservicesJobInfo"
55         )
56 
57         val ISSUE = Issue.create(
58                 id = "InvalidAdServicesJobService",
59                 briefDescription = "The Back Compat JobService parser detected an error",
60                 explanation = """Rubidium Back Compat design introduces additional constraints
61                     | on how JobServices can be used in AdServices codebase. Check the error
62                     | message to find out the specific constraint not met""".trimMargin(),
63                 moreInfo = "documentation/BackCompatJobServiceDetector.md",
64                 category = Category.CORRECTNESS,
65                 severity = Severity.WARNING,
66                 implementation = Implementation(
67                         BackCompatJobServiceDetector::class.java,
68                         Scope.JAVA_FILE_SCOPE
69                 ),
70                 androidSpecific = true
71         )
72     }
73 
getApplicableUastTypesnull74     override fun getApplicableUastTypes(): List<Class<out UElement>> {
75         return listOf(UClass::class.java)
76     }
77 
createUastHandlernull78     override fun createUastHandler(context: JavaContext): UElementHandler? {
79         return object : UElementHandler() {
80             override fun visitClass(node: UClass) {
81                 if (isJobServiceSubclass(node)) {
82                     checkJobServiceAttributes(node, context)
83                 }
84             }
85         }
86     }
87 
isJobServiceSubclassnull88     private fun isJobServiceSubclass(node: UClass): Boolean {
89         val superClass = node.javaPsi.superClass
90         return superClass?.qualifiedName == JOB_SERVICE_CLASS
91     }
92 
checkJobServiceAttributesnull93     private fun checkJobServiceAttributes(node: UClass, context: JavaContext) {
94         for (field in node.fields) {
95             val initializer = field.uastInitializer
96             if (initializer != null && !isInitializerValid(initializer)) {
97                 val location = context.getLocation(initializer)
98                 context.report(
99                         ISSUE,
100                         initializer,
101                         location,
102                         ISSUE_DESCRIPTION
103                 )
104             }
105         }
106     }
107 
isInitializerValidnull108     private fun isInitializerValid(initializer: UExpression): Boolean {
109         return when (initializer) {
110             // literal is always valid, ex, "abc", 123
111             is ULiteralExpression -> true
112             // Check whether unsafe classes are used in reference
113             is UReferenceExpression -> {
114                 val resolved = initializer.resolve()
115                 if (resolved != null && resolved is PsiMethod) {
116                     val typeName = resolved.getContainingClass()?.getQualifiedName()
117                     SAFE_CLASSES.contains(typeName)
118                 } else {
119                     true
120                 }
121             }
122             // Check whether unsafe classes are used in function call
123             is UCallExpression -> {
124                 val resolved = initializer.resolve()
125                 if (resolved != null && resolved is PsiMethod) {
126                     val typeName = resolved.getContainingClass()?.getQualifiedName()
127                     SAFE_CLASSES.contains(typeName)
128                 } else {
129                     true
130                 }
131             }
132             // Recursively check whether BinaryExpression is allowed
133             is UBinaryExpression -> isInitializerValid(initializer.leftOperand) && isInitializerValid(initializer.rightOperand)
134             // Recursively check whether PolyadicExpression is allowed
135             is UPolyadicExpression -> initializer.operands.all { isInitializerValid(it) }
136             // Other expressions are always allowed
137             else -> true
138         }
139     }
140 }