/* * Copyright (C) 2020 The Dagger Authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package dagger.lint import com.android.tools.lint.client.api.JavaEvaluator import com.android.tools.lint.client.api.UElementHandler import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Implementation import com.android.tools.lint.detector.api.Issue import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.LintFix import com.android.tools.lint.detector.api.Scope import com.android.tools.lint.detector.api.Severity import com.android.tools.lint.detector.api.SourceCodeScanner import com.android.tools.lint.detector.api.TextFormat import com.android.tools.lint.detector.api.isKotlin import dagger.lint.DaggerKotlinIssueDetector.Companion.ISSUE_FIELD_SITE_TARGET_ON_QUALIFIER_ANNOTATION import dagger.lint.DaggerKotlinIssueDetector.Companion.ISSUE_JVM_STATIC_PROVIDES_IN_OBJECT import dagger.lint.DaggerKotlinIssueDetector.Companion.ISSUE_MODULE_COMPANION_OBJECTS import dagger.lint.DaggerKotlinIssueDetector.Companion.ISSUE_MODULE_COMPANION_OBJECTS_NOT_IN_MODULE_PARENT import java.util.EnumSet import org.jetbrains.kotlin.descriptors.annotations.AnnotationUseSiteTarget import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.psi.KtAnnotationEntry import org.jetbrains.kotlin.psi.KtObjectDeclaration import org.jetbrains.uast.UClass import org.jetbrains.uast.UElement import org.jetbrains.uast.UField import org.jetbrains.uast.UMethod import org.jetbrains.uast.getUastParentOfType import org.jetbrains.uast.kotlin.KotlinUClass import org.jetbrains.uast.toUElement /** * This is a simple lint check to catch common Dagger+Kotlin usage issues. * * - [ISSUE_FIELD_SITE_TARGET_ON_QUALIFIER_ANNOTATION] covers using `field:` site targets for member * injections, which are redundant as of Dagger 2.25. * - [ISSUE_JVM_STATIC_PROVIDES_IN_OBJECT] covers using `@JvmStatic` for object * `@Provides`-annotated functions, which are redundant as of Dagger 2.25. @JvmStatic on companion * object functions are redundant as of Dagger 2.26. * - [ISSUE_MODULE_COMPANION_OBJECTS] covers annotating companion objects with `@Module`, as they * are now part of the enclosing module class's API in Dagger 2.26. This will also error if the * enclosing class is _not_ in a `@Module`-annotated class, as this object just should be moved to a * top-level object to avoid confusion. * - [ISSUE_MODULE_COMPANION_OBJECTS_NOT_IN_MODULE_PARENT] covers annotating companion objects with * `@Module` when the parent class is _not_ also annotated with `@Module`. While technically legal, * these should be moved up to top-level objects to avoid confusion. */ @Suppress("UnstableApiUsage") // Lots of Lint APIs are marked with @Beta. class DaggerKotlinIssueDetector : Detector(), SourceCodeScanner { companion object { // We use the overloaded constructor that takes a varargs of `Scope` as the last param. // This is to enable on-the-fly IDE checks. We are telling lint to run on both // JAVA and TEST_SOURCES in the `scope` parameter but by providing the `analysisScopes` // params, we're indicating that this check can run on either JAVA or TEST_SOURCES and // doesn't require both of them together. // From discussion on lint-dev https://groups.google.com/d/msg/lint-dev/ULQMzW1ZlP0/1dG4Vj3-AQAJ // This was supposed to be fixed in AS 3.4 but still required as recently as 3.6. private val SCOPES = Implementation( DaggerKotlinIssueDetector::class.java, EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES), EnumSet.of(Scope.JAVA_FILE), EnumSet.of(Scope.TEST_SOURCES) ) private val ISSUE_JVM_STATIC_PROVIDES_IN_OBJECT: Issue = Issue.create( id = "JvmStaticProvidesInObjectDetector", briefDescription = "@JvmStatic used for @Provides function in an object class", explanation = """ It's redundant to annotate @Provides functions in object classes with @JvmStatic. """, category = Category.CORRECTNESS, priority = 5, severity = Severity.WARNING, implementation = SCOPES ) private val ISSUE_FIELD_SITE_TARGET_ON_QUALIFIER_ANNOTATION: Issue = Issue.create( id = "FieldSiteTargetOnQualifierAnnotation", briefDescription = "Redundant 'field:' used for Dagger qualifier annotation.", explanation = """ It's redundant to use 'field:' site-targets for qualifier annotations. """, category = Category.CORRECTNESS, priority = 5, severity = Severity.WARNING, implementation = SCOPES ) private val ISSUE_MODULE_COMPANION_OBJECTS: Issue = Issue.create( id = "ModuleCompanionObjects", briefDescription = "Module companion objects should not be annotated with @Module.", explanation = """ Companion objects in @Module-annotated classes are considered part of the API. """, category = Category.CORRECTNESS, priority = 5, severity = Severity.WARNING, implementation = SCOPES ) private val ISSUE_MODULE_COMPANION_OBJECTS_NOT_IN_MODULE_PARENT: Issue = Issue.create( id = "ModuleCompanionObjectsNotInModuleParent", briefDescription = "Companion objects should not be annotated with @Module.", explanation = """ Companion objects in @Module-annotated classes are considered part of the API. This companion object is not a companion to an @Module-annotated class though, and should be moved to a top-level object declaration instead otherwise Dagger will ignore companion object. """, category = Category.CORRECTNESS, priority = 5, severity = Severity.WARNING, implementation = SCOPES ) private const val PROVIDES_ANNOTATION = "dagger.Provides" private const val JVM_STATIC_ANNOTATION = "kotlin.jvm.JvmStatic" private const val INJECT_ANNOTATION = "javax.inject.Inject" private const val QUALIFIER_ANNOTATION = "javax.inject.Qualifier" private const val MODULE_ANNOTATION = "dagger.Module" val issues: List = listOf( ISSUE_JVM_STATIC_PROVIDES_IN_OBJECT, ISSUE_FIELD_SITE_TARGET_ON_QUALIFIER_ANNOTATION, ISSUE_MODULE_COMPANION_OBJECTS, ISSUE_MODULE_COMPANION_OBJECTS_NOT_IN_MODULE_PARENT ) } override fun getApplicableUastTypes(): List>? { return listOf(UMethod::class.java, UField::class.java, UClass::class.java) } override fun createUastHandler(context: JavaContext): UElementHandler? { if (!isKotlin(context.psiFile)) { // This is only relevant for Kotlin files. return null } return object : UElementHandler() { override fun visitField(node: UField) { if (!context.evaluator.isLateInit(node)) { return } // Can't use hasAnnotation because it doesn't capture all annotations! val injectAnnotation = node.annotations.find { it.qualifiedName == INJECT_ANNOTATION } ?: return // Look for qualifier annotations node.annotations.forEach { annotation -> if (annotation === injectAnnotation) { // Skip the inject annotation return@forEach } // Check if it's a FIELD site target val sourcePsi = annotation.sourcePsi if (sourcePsi is KtAnnotationEntry && sourcePsi.useSiteTarget?.getAnnotationUseSiteTarget() == AnnotationUseSiteTarget.FIELD ) { // Check if this annotation is a qualifier annotation if (annotation.resolve()?.hasAnnotation(QUALIFIER_ANNOTATION) == true) { context.report( ISSUE_FIELD_SITE_TARGET_ON_QUALIFIER_ANNOTATION, context.getLocation(annotation), ISSUE_FIELD_SITE_TARGET_ON_QUALIFIER_ANNOTATION .getBriefDescription(TextFormat.TEXT), LintFix.create() .name("Remove 'field:'") .replace() .text("field:") .with("") .autoFix() .build() ) } } } } override fun visitMethod(node: UMethod) { if (!node.isConstructor && node.hasAnnotation(PROVIDES_ANNOTATION) && node.hasAnnotation(JVM_STATIC_ANNOTATION) ) { val containingClass = node.containingClass?.toUElement(UClass::class.java) ?: return if (containingClass.isObject()) { val annotation = node.findAnnotation(JVM_STATIC_ANNOTATION)!! context.report( ISSUE_JVM_STATIC_PROVIDES_IN_OBJECT, context.getLocation(annotation), ISSUE_JVM_STATIC_PROVIDES_IN_OBJECT.getBriefDescription(TextFormat.TEXT), LintFix.create() .name("Remove @JvmStatic") .replace() .pattern("(@(kotlin\\.jvm\\.)?JvmStatic)") .with("") .autoFix() .build() ) } } } override fun visitClass(node: UClass) { if (node.hasAnnotation(MODULE_ANNOTATION) && node.isCompanionObject(context.evaluator)) { val parent = node.getUastParentOfType(UClass::class.java, false)!! if (parent.hasAnnotation(MODULE_ANNOTATION)) { context.report( ISSUE_MODULE_COMPANION_OBJECTS, context.getLocation(node as UElement), ISSUE_MODULE_COMPANION_OBJECTS.getBriefDescription(TextFormat.TEXT), LintFix.create() .name("Remove @Module") .replace() .pattern("(@(dagger\\.)?Module)") .with("") .autoFix() .build() ) } else { context.report( ISSUE_MODULE_COMPANION_OBJECTS_NOT_IN_MODULE_PARENT, context.getLocation(node as UElement), ISSUE_MODULE_COMPANION_OBJECTS_NOT_IN_MODULE_PARENT .getBriefDescription(TextFormat.TEXT) ) } } } } } /** @return whether or not the [this] is a Kotlin `companion object` type. */ private fun UClass.isCompanionObject(evaluator: JavaEvaluator): Boolean { return isObject() && evaluator.hasModifier(this, KtTokens.COMPANION_KEYWORD) } /** @return whether or not the [this] is a Kotlin `object` type. */ private fun UClass.isObject(): Boolean { return this is KotlinUClass && ktClass is KtObjectDeclaration } }