-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add rule to warn against using scopes with assisted injection
- Loading branch information
1 parent
8cd332d
commit 52b9587
Showing
4 changed files
with
324 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
95 changes: 95 additions & 0 deletions
95
...agger/src/main/java/dev/whosnickdoglio/dagger/detectors/ScopedAssistedInjectedDetector.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
/* | ||
* Copyright (C) 2023 Nicholas Doglio | ||
* SPDX-License-Identifier: MIT | ||
*/ | ||
package dev.whosnickdoglio.dagger.detectors | ||
|
||
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.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 dev.whosnickdoglio.lint.shared.ASSISTED_INJECT | ||
import dev.whosnickdoglio.lint.shared.SCOPE | ||
import org.jetbrains.uast.UAnnotated | ||
import org.jetbrains.uast.UClass | ||
import org.jetbrains.uast.UElement | ||
import org.jetbrains.uast.resolveToUElement | ||
|
||
/** | ||
* A Lint rule that warns if a class is annotated with any scope annotation but does not have a | ||
* `@Inject` annotation on any constructor that it will not be added to the Dagger graph. | ||
*/ | ||
internal class ScopedAssistedInjectedDetector : Detector(), SourceCodeScanner { | ||
|
||
override fun getApplicableUastTypes(): List<Class<out UElement>> = listOf(UClass::class.java) | ||
|
||
override fun createUastHandler(context: JavaContext): UElementHandler = | ||
object : UElementHandler() { | ||
override fun visitClass(node: UClass) { | ||
val sourceScopeAnnotations = | ||
node.uAnnotations | ||
.map { annotation -> annotation.resolveToUElement() } | ||
.filterIsInstance<UAnnotated>() | ||
.filter { annotated -> | ||
annotated.uAnnotations.any { annotation -> | ||
annotation.qualifiedName == SCOPE | ||
} | ||
} | ||
.filterIsInstance<UClass>() | ||
|
||
val scopeAnnotationsOnCurrentClass = | ||
node.uAnnotations.filter { annotation -> | ||
sourceScopeAnnotations.any { scope -> | ||
scope.qualifiedName == annotation.qualifiedName | ||
} | ||
} | ||
|
||
val usesAssistedInjection = | ||
node.constructors.any { constructor -> | ||
constructor.hasAnnotation(ASSISTED_INJECT) | ||
} | ||
|
||
if (scopeAnnotationsOnCurrentClass.isNotEmpty() && usesAssistedInjection) { | ||
scopeAnnotationsOnCurrentClass.forEach { scopeAnnotation -> | ||
context.report( | ||
issue = ISSUE, | ||
location = context.getLocation(scopeAnnotation), | ||
message = ISSUE.getExplanation(TextFormat.RAW), | ||
quickfixData = | ||
fix() | ||
.name("Remove scope annotation") | ||
.replace() | ||
.pattern( | ||
"(?i)(.*${scopeAnnotation.qualifiedName?.substringAfterLast(".")})", | ||
) | ||
.reformat(true) | ||
.with("") | ||
.build(), | ||
) | ||
} | ||
} | ||
} | ||
} | ||
|
||
companion object { | ||
private val implementation = | ||
Implementation(ScopedAssistedInjectedDetector::class.java, Scope.JAVA_FILE_SCOPE) | ||
|
||
internal val ISSUE = | ||
Issue.create( | ||
id = "ScopedAssistedInject", | ||
briefDescription = "Classes using assisted inject cannot be scoped", | ||
explanation = "Classes using assisted inject cannot be scoped", | ||
category = Category.CORRECTNESS, | ||
priority = 5, | ||
severity = Severity.ERROR, | ||
implementation = implementation, | ||
) | ||
} | ||
} |
222 changes: 222 additions & 0 deletions
222
...r/src/test/java/dev/whosnickdoglio/dagger/detectors/ScopedAssistedInjectedDetectorTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,222 @@ | ||
/* | ||
* Copyright (C) 2023 Nicholas Doglio | ||
* SPDX-License-Identifier: MIT | ||
*/ | ||
package dev.whosnickdoglio.dagger.detectors | ||
|
||
import com.android.tools.lint.checks.infrastructure.TestFiles | ||
import com.android.tools.lint.checks.infrastructure.TestLintTask | ||
import dev.whosnickdoglio.stubs.daggerAnnotations | ||
import dev.whosnickdoglio.stubs.daggerAssistedAnnotations | ||
import dev.whosnickdoglio.stubs.javaxAnnotations | ||
import org.junit.Test | ||
|
||
class ScopedAssistedInjectedDetectorTest { | ||
|
||
private val myScope = | ||
TestFiles.kotlin( | ||
""" | ||
import javax.inject.Scope | ||
@Scope annotation class MyScope | ||
""" | ||
.trimIndent(), | ||
) | ||
|
||
@Test | ||
fun `scoped kotlin class using @AssistedInject triggers error`() { | ||
TestLintTask.lint() | ||
.files( | ||
daggerAnnotations, | ||
daggerAssistedAnnotations, | ||
javaxAnnotations, | ||
myScope, | ||
TestFiles.kotlin( | ||
""" | ||
import dagger.assisted.AssistedInject | ||
import dagger.assisted.Assisted | ||
@MyScope class MyAssistedClass @AssistedInject constructor( | ||
private val myInt: Int, | ||
@Assisted private val something: String | ||
) | ||
""", | ||
) | ||
.indented(), | ||
) | ||
.issues(ScopedAssistedInjectedDetector.ISSUE) | ||
.run() | ||
.expect( | ||
""" | ||
src/MyAssistedClass.kt:4: Error: Classes using assisted inject cannot be scoped [ScopedAssistedInject] | ||
@MyScope class MyAssistedClass @AssistedInject constructor( | ||
~~~~~~~~ | ||
1 errors, 0 warnings | ||
""" | ||
.trimIndent(), | ||
) | ||
.expectErrorCount(1) | ||
.expectFixDiffs( | ||
""" | ||
Fix for src/MyAssistedClass.kt line 4: Remove scope annotation: | ||
@@ -4 +4 | ||
- @MyScope class MyAssistedClass @AssistedInject constructor( | ||
+ class MyAssistedClass @AssistedInject constructor( | ||
""" | ||
.trimIndent(), | ||
) | ||
} | ||
|
||
@Test | ||
fun `scoped java class using @AssistedInject triggers error`() { | ||
TestLintTask.lint() | ||
.files( | ||
daggerAnnotations, | ||
daggerAssistedAnnotations, | ||
javaxAnnotations, | ||
myScope, | ||
TestFiles.java( | ||
""" | ||
import dagger.assisted.AssistedInject; | ||
import dagger.assisted.Assisted; | ||
@MyScope class MyAssistedClass { | ||
@AssistedInject MyAssistedClass( | ||
String something, | ||
@Assisted Boolean somethingElse | ||
) {} | ||
} | ||
""", | ||
) | ||
.indented(), | ||
) | ||
.issues(ScopedAssistedInjectedDetector.ISSUE) | ||
.run() | ||
.expect( | ||
""" | ||
src/MyAssistedClass.java:4: Error: Classes using assisted inject cannot be scoped [ScopedAssistedInject] | ||
@MyScope class MyAssistedClass { | ||
~~~~~~~~ | ||
1 errors, 0 warnings | ||
""" | ||
.trimIndent(), | ||
) | ||
.expectErrorCount(1) | ||
.expectFixDiffs( | ||
""" | ||
Fix for src/MyAssistedClass.java line 4: Remove scope annotation: | ||
@@ -4 +4 | ||
- @MyScope class MyAssistedClass { | ||
+ class MyAssistedClass { | ||
""" | ||
.trimIndent(), | ||
) | ||
} | ||
|
||
@Test | ||
fun `kotlin class without scope using @AssistedInject does not triggers error`() { | ||
TestLintTask.lint() | ||
.files( | ||
daggerAnnotations, | ||
daggerAssistedAnnotations, | ||
javaxAnnotations, | ||
TestFiles.kotlin( | ||
""" | ||
import dagger.assisted.AssistedInject | ||
import dagger.assisted.Assisted | ||
class MyAssistedClass @AssistedInject constructor( | ||
private val myInt: Int, | ||
@Assisted private val something: String | ||
) | ||
""", | ||
) | ||
.indented(), | ||
) | ||
.issues(ScopedAssistedInjectedDetector.ISSUE) | ||
.run() | ||
.expectClean() | ||
.expectErrorCount(0) | ||
} | ||
|
||
@Test | ||
fun `java class without scope using @AssistedInject does not triggers error`() { | ||
TestLintTask.lint() | ||
.files( | ||
daggerAnnotations, | ||
daggerAssistedAnnotations, | ||
javaxAnnotations, | ||
TestFiles.java( | ||
""" | ||
import dagger.assisted.AssistedInject; | ||
import dagger.assisted.Assisted; | ||
class MyAssistedClass { | ||
@AssistedInject MyAssistedClass( | ||
String something, | ||
@Assisted Boolean somethingElse | ||
) {} | ||
} | ||
""", | ||
) | ||
.indented(), | ||
) | ||
.issues(ScopedAssistedInjectedDetector.ISSUE) | ||
.run() | ||
.expectClean() | ||
.expectErrorCount(0) | ||
} | ||
|
||
@Test | ||
fun `scoped kotlin class not using @AssistedInject does not triggers error`() { | ||
TestLintTask.lint() | ||
.files( | ||
daggerAnnotations, | ||
daggerAssistedAnnotations, | ||
javaxAnnotations, | ||
myScope, | ||
TestFiles.kotlin( | ||
""" | ||
import javax.inject.Inject | ||
@MyScope class MyAssistedClass @Inject constructor(something: String) | ||
""", | ||
) | ||
.indented(), | ||
) | ||
.issues(ScopedAssistedInjectedDetector.ISSUE) | ||
.run() | ||
.expectClean() | ||
.expectErrorCount(0) | ||
} | ||
|
||
@Test | ||
fun `scoped java class not using @AssistedInject does not triggers error`() { | ||
TestLintTask.lint() | ||
.files( | ||
daggerAnnotations, | ||
daggerAssistedAnnotations, | ||
javaxAnnotations, | ||
myScope, | ||
TestFiles.java( | ||
""" | ||
import javax.inject.Inject; | ||
@MyScope class MyAssistedClass { | ||
@Inject MyAssistedClass( | ||
String something, | ||
Boolean somethingElse | ||
) {} | ||
} | ||
""", | ||
) | ||
.indented(), | ||
) | ||
.issues(ScopedAssistedInjectedDetector.ISSUE) | ||
.run() | ||
.expectClean() | ||
.expectErrorCount(0) | ||
} | ||
} |