1 /*
2  * Copyright 2020 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.startup.lint
20 
21 import com.android.SdkConstants.ANDROID_URI
22 import com.android.SdkConstants.ATTR_NAME
23 import com.android.SdkConstants.ATTR_VALUE
24 import com.android.tools.lint.detector.api.Category
25 import com.android.tools.lint.detector.api.Context
26 import com.android.tools.lint.detector.api.Detector
27 import com.android.tools.lint.detector.api.Implementation
28 import com.android.tools.lint.detector.api.Issue
29 import com.android.tools.lint.detector.api.JavaContext
30 import com.android.tools.lint.detector.api.Location
31 import com.android.tools.lint.detector.api.Scope
32 import com.android.tools.lint.detector.api.Severity
33 import com.android.tools.lint.detector.api.SourceCodeScanner
34 import com.android.tools.lint.detector.api.XmlContext
35 import com.android.tools.lint.detector.api.XmlScanner
36 import com.intellij.psi.impl.source.PsiClassReferenceType
37 import java.util.EnumSet
38 import org.jetbrains.uast.UClass
39 import org.jetbrains.uast.UClassLiteralExpression
40 import org.jetbrains.uast.visitor.AbstractUastVisitor
41 import org.w3c.dom.Element
42 
43 /**
44  * A [Detector] which ensures that every `ComponentInitializer` is accompanied by a corresponding
45  * entry in the `AndroidManifest.xml`.
46  */
47 class EnsureInitializerMetadataDetector : Detector(), SourceCodeScanner, XmlScanner {
48     // all declared components
49     private val components = mutableMapOf<UClass, Location>()
50     // all reachable components
51     // Synthetic access
52     val reachable = mutableSetOf<String>()
53 
54     companion object {
55         private const val DESCRIPTION =
56             "Every Initializer needs to be accompanied by a " +
57                 "corresponding <meta-data> entry in the AndroidManifest.xml file."
58 
59         val ISSUE =
60             Issue.create(
61                 id = "EnsureInitializerMetadata",
62                 briefDescription = DESCRIPTION,
63                 explanation =
64                     """
65                 When a library defines a Initializer, it needs to be accompanied by a \
66                 corresponding <meta-data> entry in the AndroidManifest.xml file.
67             """,
68                 androidSpecific = true,
69                 category = Category.CORRECTNESS,
70                 severity = Severity.FATAL,
71                 implementation =
72                     Implementation(
73                         EnsureInitializerMetadataDetector::class.java,
74                         EnumSet.of(Scope.JAVA_FILE, Scope.MANIFEST)
75                     )
76             )
77     }
78 
applicableSuperClassesnull79     override fun applicableSuperClasses() = listOf("androidx.startup.Initializer")
80 
81     override fun getApplicableElements() = listOf("meta-data")
82 
83     override fun visitClass(context: JavaContext, declaration: UClass) {
84         val name = declaration.qualifiedName
85 
86         if (name == "androidx.startup.Initializer") {
87             // This is the component initializer itself.
88             return
89         }
90 
91         if (!declaration.isInterface) {
92             val location = context.getLocation(declaration.javaPsi)
93             components[declaration] = location
94         }
95 
96         // Check every dependencies() method for reachable Initializer's
97         val method =
98             declaration.methods.first { it.name == "dependencies" && it.parameterList.isEmpty }
99         val visitor =
100             object : AbstractUastVisitor() {
101                 override fun visitClassLiteralExpression(node: UClassLiteralExpression): Boolean {
102                     val qualifiedName =
103                         (node.type as? PsiClassReferenceType)?.resolve()?.qualifiedName
104                     if (qualifiedName != null) {
105                         reachable += qualifiedName
106                     }
107                     return true
108                 }
109             }
110 
111         method.accept(visitor)
112     }
113 
visitElementnull114     override fun visitElement(context: XmlContext, element: Element) {
115         // Track all <meta-data> elements with value androidx.startup
116         val name = element.getAttributeNS(ANDROID_URI, ATTR_NAME)
117         val value = element.getAttributeNS(ANDROID_URI, ATTR_VALUE)
118         // There does not seem to be a way to evaluate resources defined in the manifest.
119         // Figure out if there is a better way.
120         if (value == "androidx.startup" || value == "@string/androidx_startup") {
121             reachable += name
122         }
123     }
124 
afterCheckRootProjectnull125     override fun afterCheckRootProject(context: Context) {
126         for ((declaration, location) in components) {
127             if (declaration.qualifiedName !in reachable) {
128                 context.report(issue = ISSUE, location = location, message = DESCRIPTION)
129             }
130         }
131     }
132 }
133