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