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 }