blob: f3fdbd318f2688d606fb32a4e087830c20d7b2fb [file] [log] [blame]
/*
* 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<Issue> = 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<Class<out UElement>>? {
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
}
}