1 /*
2  * 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 com.android.photopicker.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 org.jetbrains.uast.UClass
29 import org.jetbrains.uast.UElement
30 
31 /**
32  * A linter implementation that enforces Hilt dependencies to be injected Lazily in certain core
33  * Photopicker classes.
34  */
35 class LazyInjectionDetector : Detector(), SourceCodeScanner {
36 
37     companion object {
38 
39         const val INVALID_INJECTION_FIELD_ERROR =
40             "Dependencies injected into core classes must be either an allowlisted dependency " +
41                 "or be injected Lazily. Use dagger.Lazy to inject the dependency, to avoid " +
42                 "out-of-order initialization issues."
43 
44         val ISSUE =
45             Issue.create(
46                 id = "LazyInjectionRequired",
47                 briefDescription =
48                     "Hilt dependencies should be injected into primary classes lazily to avoid " +
49                         "out-of-order initialization issues.",
50                 explanation =
51                     "Photopicker's injected classes implementation expects a certain " +
52                         "initialization order, namely that the PhotopickerConfiguration is " +
53                         "stable before other classes are created to reduce error prone issues " +
54                         "related to a configuration update or re-initialization of FeatureManager.",
55                 category = Category.CORRECTNESS,
56                 severity = Severity.ERROR,
57                 implementation =
58                     Implementation(LazyInjectionDetector::class.java, Scope.JAVA_FILE_SCOPE),
59                 androidSpecific = true,
60             )
61 
62         // Core classes this LazyInjectionDetector enforces.
63         val ENFORCED_CLASSES: List<String> =
64             listOf(
65                 "com.android.photopicker.MainActivity",
66                 "com.android.photopicker.core.embedded.Session",
67             )
68 
69         /** The list of class that may be injected without using Lazy<...> */
70         val ALLOWED_NON_LAZY_CLASSES =
71             listOf(
72                 "android.content.ContentResolver",
73                 "android.os.UserHandle",
74                 "com.android.photopicker.core.configuration.ConfigurationManager",
75                 "com.android.photopicker.core.embedded.EmbeddedLifecycle",
76                 "kotlinx.coroutines.CoroutineDispatcher",
77                 "kotlinx.coroutines.CoroutineScope",
78             )
79 
80         // Qualified name of the @Inject annotation.
81         val INJECT_ANNOTATION = "javax.inject.Inject"
82 
83         // Qualified name of the EntryPoint annotation.
84         val ENTRY_POINT_ANNOTATION = "dagger.hilt.EntryPoint"
85 
86         // The qualified name of the Dagger lazy class.
87         val DAGGER_LAZY = "dagger.Lazy"
88     }
89 
getApplicableUastTypesnull90     override fun getApplicableUastTypes(): List<Class<out UElement>> {
91         return listOf(UClass::class.java)
92     }
93 
createUastHandlernull94     override fun createUastHandler(context: JavaContext): UElementHandler? {
95 
96         return object : UElementHandler() {
97 
98             override fun visitClass(node: UClass) {
99 
100                 // If the class being inspected is not one of the enforced classes, skip the class.
101                 if (!ENFORCED_CLASSES.contains(node.qualifiedName)) {
102                     return
103                 }
104 
105                 for (_node in node.getFields()) {
106 
107                     // Quickly skip the field if it is not a lateinit field, all Hilt fields are
108                     // lateinit.
109                     if (!context.evaluator.isLateInit(_node)) {
110                         continue
111                     }
112 
113                     // If the field is not annotated with @Inject then skip it.
114                     _node.uAnnotations.find { it.qualifiedName == INJECT_ANNOTATION } ?: continue
115 
116                     // This is the qualified type signature of the field
117                     val typeQualified = _node.typeReference?.getQualifiedName()
118 
119                     // If the qualified type is either in the allowlist, or a Lazy<*> field,
120                     // it is allowed.
121                     if (
122                         typeQualified == DAGGER_LAZY ||
123                             ALLOWED_NON_LAZY_CLASSES.contains(typeQualified)
124                     ) {
125                         continue
126                     }
127 
128                     // The field is an @Inject non-lazy field that is not in the allow-list.
129                     // Report this as an error as this is not permitted.
130                     context.report(
131                         issue = ISSUE,
132                         location = context.getNameLocation(_node),
133                         message = INVALID_INJECTION_FIELD_ERROR,
134                     )
135                 }
136 
137                 for (clazz in node.getInnerClasses()) {
138 
139                     // Only check inner classes that are marked with an "EntryPoint annotation"
140                     clazz.uAnnotations.find { it.qualifiedName == ENTRY_POINT_ANNOTATION }
141                         ?: continue
142 
143                     // EntryPoints use methods rather than fields, so iterate all the methods in
144                     // the EntryPoint.
145                     for (_method in clazz.getMethods()) {
146 
147                         val typeQualified = _method.returnTypeReference?.getQualifiedName()
148                         // If the qualified type is either in the allowlist, or a Lazy<*> field,
149                         // it is allowed.
150                         if (
151                             typeQualified == DAGGER_LAZY ||
152                                 ALLOWED_NON_LAZY_CLASSES.contains(typeQualified)
153                         ) {
154                             continue
155                         }
156 
157                         // The method is a non-lazy return type that is not in the allow-list.
158                         // Report this as an error as this is not permitted.
159                         context.report(
160                             issue = ISSUE,
161                             location = context.getNameLocation(_method),
162                             message = INVALID_INJECTION_FIELD_ERROR,
163                         )
164                     }
165                 }
166             }
167         }
168     }
169 }
170