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