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