Skip to content

Commit

Permalink
Add rule to warn against using scopes with assisted injection
Browse files Browse the repository at this point in the history
  • Loading branch information
WhosNickDoglio committed May 28, 2024
1 parent b0db042 commit c511b5e
Show file tree
Hide file tree
Showing 4 changed files with 324 additions and 0 deletions.
5 changes: 5 additions & 0 deletions docs/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,11 @@ interface AppComponent
@Singleton MyOtherClass @Inject constructor()
```

### Classes that use `@AssistedInject` cannot be scoped

[//]: # (TODO)


## Anvil Rules

### Prefer using `@ContributesBinding` over `@Binds`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import dev.whosnickdoglio.dagger.detectors.ConstructorInjectionOverFieldInjectio
import dev.whosnickdoglio.dagger.detectors.CorrectBindsUsageDetector
import dev.whosnickdoglio.dagger.detectors.MissingModuleAnnotationDetector
import dev.whosnickdoglio.dagger.detectors.MultipleScopesDetector
import dev.whosnickdoglio.dagger.detectors.ScopedAssistedInjectedDetector
import dev.whosnickdoglio.dagger.detectors.ScopedWithoutInjectAnnotationDetector
import dev.whosnickdoglio.dagger.detectors.StaticProvidesDetector

Expand All @@ -28,6 +29,7 @@ public class DaggerRulesIssueRegistry : IssueRegistry() {
MissingModuleAnnotationDetector.ISSUE,
MultipleScopesDetector.ISSUE,
StaticProvidesDetector.ISSUE,
ScopedAssistedInjectedDetector.ISSUE,

Check warning on line 32 in lint/dagger/src/main/java/dev/whosnickdoglio/dagger/DaggerRulesIssueRegistry.kt

View check run for this annotation

Codecov / codecov/patch

lint/dagger/src/main/java/dev/whosnickdoglio/dagger/DaggerRulesIssueRegistry.kt#L32

Added line #L32 was not covered by tests
ScopedWithoutInjectAnnotationDetector.ISSUE,
)

Expand Down
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,
)
}
}
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)
}
}

0 comments on commit c511b5e

Please sign in to comment.