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