xref: /aosp_15_r20/external/dagger2/java/dagger/lint/DaggerKotlinIssueDetector.kt (revision f585d8a307d0621d6060bd7e80091fdcbf94fe27)
1 /*
<lambda>null2  * Copyright (C) 2020 The Dagger Authors.
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 dagger.lint
17 
18 import com.android.tools.lint.client.api.JavaEvaluator
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.LintFix
26 import com.android.tools.lint.detector.api.Scope
27 import com.android.tools.lint.detector.api.Severity
28 import com.android.tools.lint.detector.api.SourceCodeScanner
29 import com.android.tools.lint.detector.api.TextFormat
30 import com.android.tools.lint.detector.api.isKotlin
31 import dagger.lint.DaggerKotlinIssueDetector.Companion.ISSUE_FIELD_SITE_TARGET_ON_QUALIFIER_ANNOTATION
32 import dagger.lint.DaggerKotlinIssueDetector.Companion.ISSUE_JVM_STATIC_PROVIDES_IN_OBJECT
33 import dagger.lint.DaggerKotlinIssueDetector.Companion.ISSUE_MODULE_COMPANION_OBJECTS
34 import dagger.lint.DaggerKotlinIssueDetector.Companion.ISSUE_MODULE_COMPANION_OBJECTS_NOT_IN_MODULE_PARENT
35 import java.util.EnumSet
36 import org.jetbrains.kotlin.descriptors.annotations.AnnotationUseSiteTarget
37 import org.jetbrains.kotlin.lexer.KtTokens
38 import org.jetbrains.kotlin.psi.KtAnnotationEntry
39 import org.jetbrains.kotlin.psi.KtObjectDeclaration
40 import org.jetbrains.uast.UClass
41 import org.jetbrains.uast.UElement
42 import org.jetbrains.uast.UField
43 import org.jetbrains.uast.UMethod
44 import org.jetbrains.uast.getUastParentOfType
45 import org.jetbrains.uast.toUElement
46 
47 /**
48  * This is a simple lint check to catch common Dagger+Kotlin usage issues.
49  *
50  * - [ISSUE_FIELD_SITE_TARGET_ON_QUALIFIER_ANNOTATION] covers using `field:` site targets for member
51  * injections, which are redundant as of Dagger 2.25.
52  * - [ISSUE_JVM_STATIC_PROVIDES_IN_OBJECT] covers using `@JvmStatic` for object
53  * `@Provides`-annotated functions, which are redundant as of Dagger 2.25. @JvmStatic on companion
54  * object functions are redundant as of Dagger 2.26.
55  * - [ISSUE_MODULE_COMPANION_OBJECTS] covers annotating companion objects with `@Module`, as they
56  * are now part of the enclosing module class's API in Dagger 2.26. This will also error if the
57  * enclosing class is _not_ in a `@Module`-annotated class, as this object just should be moved to a
58  * top-level object to avoid confusion.
59  * - [ISSUE_MODULE_COMPANION_OBJECTS_NOT_IN_MODULE_PARENT] covers annotating companion objects with
60  * `@Module` when the parent class is _not_ also annotated with `@Module`. While technically legal,
61  * these should be moved up to top-level objects to avoid confusion.
62  */
63 @Suppress(
64   "UnstableApiUsage" // Lots of Lint APIs are marked with @Beta.
65 )
66 class DaggerKotlinIssueDetector : Detector(), SourceCodeScanner {
67 
68   companion object {
69     // We use the overloaded constructor that takes a varargs of `Scope` as the last param.
70     // This is to enable on-the-fly IDE checks. We are telling lint to run on both
71     // JAVA and TEST_SOURCES in the `scope` parameter but by providing the `analysisScopes`
72     // params, we're indicating that this check can run on either JAVA or TEST_SOURCES and
73     // doesn't require both of them together.
74     // From discussion on lint-dev https://groups.google.com/d/msg/lint-dev/ULQMzW1ZlP0/1dG4Vj3-AQAJ
75     // This was supposed to be fixed in AS 3.4 but still required as recently as 3.6.
76     private val SCOPES = Implementation(
77       DaggerKotlinIssueDetector::class.java,
78       EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES),
79       EnumSet.of(Scope.JAVA_FILE),
80       EnumSet.of(Scope.TEST_SOURCES)
81     )
82 
83     private val ISSUE_JVM_STATIC_PROVIDES_IN_OBJECT: Issue = Issue.create(
84       id = "JvmStaticProvidesInObjectDetector",
85       briefDescription = "@JvmStatic used for @Provides function in an object class",
86       explanation =
87         """
88         It's redundant to annotate @Provides functions in object classes with @JvmStatic.
89         """,
90       category = Category.CORRECTNESS,
91       priority = 5,
92       severity = Severity.WARNING,
93       implementation = SCOPES
94     )
95 
96     private val ISSUE_FIELD_SITE_TARGET_ON_QUALIFIER_ANNOTATION: Issue = Issue.create(
97       id = "FieldSiteTargetOnQualifierAnnotation",
98       briefDescription = "Redundant 'field:' used for Dagger qualifier annotation.",
99       explanation =
100         """
101         It's redundant to use 'field:' site-targets for qualifier annotations.
102         """,
103       category = Category.CORRECTNESS,
104       priority = 5,
105       severity = Severity.WARNING,
106       implementation = SCOPES
107     )
108 
109     private val ISSUE_MODULE_COMPANION_OBJECTS: Issue = Issue.create(
110       id = "ModuleCompanionObjects",
111       briefDescription = "Module companion objects should not be annotated with @Module.",
112       explanation =
113         """
114         Companion objects in @Module-annotated classes are considered part of the API.
115         """,
116       category = Category.CORRECTNESS,
117       priority = 5,
118       severity = Severity.WARNING,
119       implementation = SCOPES
120     )
121 
122     private val ISSUE_MODULE_COMPANION_OBJECTS_NOT_IN_MODULE_PARENT: Issue = Issue.create(
123       id = "ModuleCompanionObjectsNotInModuleParent",
124       briefDescription = "Companion objects should not be annotated with @Module.",
125       explanation =
126         """
127         Companion objects in @Module-annotated classes are considered part of the API. This
128         companion object is not a companion to an @Module-annotated class though, and should be
129         moved to a top-level object declaration instead otherwise Dagger will ignore companion
130         object.
131         """,
132       category = Category.CORRECTNESS,
133       priority = 5,
134       severity = Severity.WARNING,
135       implementation = SCOPES
136     )
137 
138     private const val PROVIDES_ANNOTATION = "dagger.Provides"
139     private const val JVM_STATIC_ANNOTATION = "kotlin.jvm.JvmStatic"
140     private const val INJECT_ANNOTATION = "javax.inject.Inject"
141     private const val QUALIFIER_ANNOTATION = "javax.inject.Qualifier"
142     private const val MODULE_ANNOTATION = "dagger.Module"
143 
144     val issues: List<Issue> = listOf(
145       ISSUE_JVM_STATIC_PROVIDES_IN_OBJECT,
146       ISSUE_FIELD_SITE_TARGET_ON_QUALIFIER_ANNOTATION,
147       ISSUE_MODULE_COMPANION_OBJECTS,
148       ISSUE_MODULE_COMPANION_OBJECTS_NOT_IN_MODULE_PARENT
149     )
150   }
151 
152   override fun getApplicableUastTypes(): List<Class<out UElement>>? {
153     return listOf(UMethod::class.java, UField::class.java, UClass::class.java)
154   }
155 
156   override fun createUastHandler(context: JavaContext): UElementHandler? {
157     if (!isKotlin(context.psiFile)) {
158       // This is only relevant for Kotlin files.
159       return null
160     }
161     return object : UElementHandler() {
162       override fun visitField(node: UField) {
163         if (!context.evaluator.isLateInit(node)) {
164           return
165         }
166         // Can't use hasAnnotation because it doesn't capture all annotations!
167         val injectAnnotation =
168           node.uAnnotations.find { it.qualifiedName == INJECT_ANNOTATION } ?: return
169         // Look for qualifier annotations
170         node.uAnnotations.forEach { annotation ->
171           if (annotation === injectAnnotation) {
172             // Skip the inject annotation
173             return@forEach
174           }
175           // Check if it's a FIELD site target
176           val sourcePsi = annotation.sourcePsi
177           if (sourcePsi is KtAnnotationEntry &&
178             sourcePsi.useSiteTarget?.getAnnotationUseSiteTarget() == AnnotationUseSiteTarget.FIELD
179           ) {
180             // Check if this annotation is a qualifier annotation
181             if (annotation.resolve()?.hasAnnotation(QUALIFIER_ANNOTATION) == true) {
182               context.report(
183                 ISSUE_FIELD_SITE_TARGET_ON_QUALIFIER_ANNOTATION,
184                 context.getLocation(annotation),
185                 ISSUE_FIELD_SITE_TARGET_ON_QUALIFIER_ANNOTATION
186                   .getBriefDescription(TextFormat.TEXT),
187                 LintFix.create()
188                   .name("Remove 'field:'")
189                   .replace()
190                   .text("field:")
191                   .with("")
192                   .autoFix()
193                   .build()
194               )
195             }
196           }
197         }
198       }
199 
200       override fun visitMethod(node: UMethod) {
201         if (!node.isConstructor &&
202           node.hasAnnotation(PROVIDES_ANNOTATION) &&
203           node.hasAnnotation(JVM_STATIC_ANNOTATION)
204         ) {
205           val containingClass = node.containingClass?.toUElement(UClass::class.java) ?: return
206           if (containingClass.isObject()) {
207             val annotation = node.findAnnotation(JVM_STATIC_ANNOTATION)
208               ?: node.javaPsi.modifierList.findAnnotation(JVM_STATIC_ANNOTATION)!!
209             context.report(
210               ISSUE_JVM_STATIC_PROVIDES_IN_OBJECT,
211               context.getLocation(annotation),
212               ISSUE_JVM_STATIC_PROVIDES_IN_OBJECT.getBriefDescription(TextFormat.TEXT),
213               LintFix.create()
214                 .name("Remove @JvmStatic")
215                 .replace()
216                 .pattern("(@(kotlin\\.jvm\\.)?JvmStatic)")
217                 .with("")
218                 .autoFix()
219                 .build()
220             )
221           }
222         }
223       }
224 
225       override fun visitClass(node: UClass) {
226         if (node.hasAnnotation(MODULE_ANNOTATION) && node.isCompanionObject(context.evaluator)) {
227           val parent = node.getUastParentOfType(UClass::class.java, false)!!
228           if (parent.hasAnnotation(MODULE_ANNOTATION)) {
229             context.report(
230               ISSUE_MODULE_COMPANION_OBJECTS,
231               context.getLocation(node as UElement),
232               ISSUE_MODULE_COMPANION_OBJECTS.getBriefDescription(TextFormat.TEXT),
233               LintFix.create()
234                 .name("Remove @Module")
235                 .replace()
236                 .pattern("(@(dagger\\.)?Module)")
237                 .with("")
238                 .autoFix()
239                 .build()
240 
241             )
242           } else {
243             context.report(
244               ISSUE_MODULE_COMPANION_OBJECTS_NOT_IN_MODULE_PARENT,
245               context.getLocation(node as UElement),
246               ISSUE_MODULE_COMPANION_OBJECTS_NOT_IN_MODULE_PARENT
247                 .getBriefDescription(TextFormat.TEXT)
248             )
249           }
250         }
251       }
252     }
253   }
254 
255   /** @return whether or not the [this] is a Kotlin `companion object` type. */
256   private fun UClass.isCompanionObject(evaluator: JavaEvaluator): Boolean {
257     return isObject() && evaluator.hasModifier(this, KtTokens.COMPANION_KEYWORD)
258   }
259 
260   /** @return whether or not the [this] is a Kotlin `object` type. */
261   private fun UClass.isObject(): Boolean {
262     return sourcePsi is KtObjectDeclaration
263   }
264 }
265