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