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