Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add rule to warn against using scopes with assisted injection #171

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.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 @@
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)
}
}
Loading