Skip to content

Commit

Permalink
Improve error messaging when using assisted inject (#1246)
Browse files Browse the repository at this point in the history
Resolves #477
  • Loading branch information
ZacSweers authored Mar 4, 2024
1 parent be1b3fa commit 93cdbe3
Show file tree
Hide file tree
Showing 2 changed files with 134 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.google.auto.service.AutoService
import com.google.devtools.ksp.KspExperimental
import com.google.devtools.ksp.containingFile
import com.google.devtools.ksp.getAllSuperTypes
import com.google.devtools.ksp.getConstructors
import com.google.devtools.ksp.getVisibility
import com.google.devtools.ksp.isAnnotationPresent
import com.google.devtools.ksp.processing.CodeGenerator
Expand All @@ -20,6 +21,7 @@ import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
import com.google.devtools.ksp.processing.SymbolProcessorProvider
import com.google.devtools.ksp.symbol.ClassKind
import com.google.devtools.ksp.symbol.KSAnnotated
import com.google.devtools.ksp.symbol.KSAnnotation
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSDeclaration
import com.google.devtools.ksp.symbol.KSFunctionDeclaration
Expand All @@ -46,9 +48,11 @@ import com.squareup.kotlinpoet.ksp.toClassName
import com.squareup.kotlinpoet.ksp.toTypeName
import com.squareup.kotlinpoet.ksp.writeTo
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import java.util.Locale
import javax.inject.Inject
import javax.inject.Provider
import kotlin.reflect.KClass

private const val CIRCUIT_RUNTIME_BASE_PACKAGE = "com.slack.circuit.runtime"
private const val DAGGER_PACKAGE = "dagger"
Expand Down Expand Up @@ -170,10 +174,16 @@ private class CircuitSymbolProcessor(
codegenMode: CodegenMode,
) {
val circuitInjectAnnotation =
annotatedElement.annotations.first {
it.annotationType.resolve().declaration.qualifiedName?.asString() ==
CIRCUIT_INJECT_ANNOTATION.canonicalName
annotatedElement.getKSAnnotationsWithLeniency(CIRCUIT_INJECT_ANNOTATION).single()

// If we annotated a class, check that the class isn't using assisted inject. If so, error and
// return
if (instantiationType == InstantiationType.CLASS) {
(annotatedElement as KSClassDeclaration).checkForAssistedInjection {
return
}
}

val screenKSType = circuitInjectAnnotation.arguments[0].value as KSType
val screenIsObject =
screenKSType.declaration.let { it is KSClassDeclaration && it.classKind == ClassKind.OBJECT }
Expand Down Expand Up @@ -255,6 +265,53 @@ private class CircuitSymbolProcessor(
)
}

private fun KSClassDeclaration.findConstructorAnnotatedWith(
annotation: KClass<out Annotation>
): KSFunctionDeclaration? {
return getConstructors().singleOrNull { constructor ->
constructor.isAnnotationPresentWithLeniency(annotation)
}
}

private inline fun KSClassDeclaration.checkForAssistedInjection(exit: () -> Nothing) {
// Check for an AssistedInject constructor
if (findConstructorAnnotatedWith(AssistedInject::class) != null) {
val assistedFactory =
declarations.filterIsInstance<KSClassDeclaration>().find {
it.isAnnotationPresentWithLeniency(AssistedFactory::class)
}
val suffix =
if (assistedFactory != null) " (${assistedFactory.qualifiedName?.asString()})" else ""
logger.error(
"When using @CircuitInject with an @AssistedInject-annotated class, you must " +
"put the @CircuitInject annotation on the @AssistedFactory-annotated nested class$suffix.",
this,
)
exit()
}
}

private fun KSAnnotated.isAnnotationPresentWithLeniency(annotation: KClass<out Annotation>) =
getKSAnnotationsWithLeniency(annotation).any()

private fun KSAnnotated.getKSAnnotationsWithLeniency(annotation: KClass<out Annotation>) =
getKSAnnotationsWithLeniency(annotation.asClassName())

private fun KSAnnotated.getKSAnnotationsWithLeniency(
annotation: ClassName
): Sequence<KSAnnotation> {
val simpleName = annotation.simpleName
return if (lenient) {
annotations.filter { it.shortName.asString() == simpleName }
} else {
val qualifiedName = annotation.canonicalName
this.annotations.filter {
it.shortName.getShortName() == simpleName &&
it.annotationType.resolve().declaration.qualifiedName?.asString() == qualifiedName
}
}
}

private data class FactoryData(
val className: String,
val packageName: String,
Expand Down Expand Up @@ -381,12 +438,7 @@ private class CircuitSymbolProcessor(
declaration.checkVisibility(logger) {
return null
}
val isAssisted =
if (lenient) {
declaration.annotations.any { it.shortName.asString().contains("AssistedFactory") }
} else {
declaration.isAnnotationPresent(AssistedFactory::class)
}
val isAssisted = declaration.isAnnotationPresentWithLeniency(AssistedFactory::class)
val creatorOrConstructor: KSFunctionDeclaration?
val targetClass: KSClassDeclaration
if (isAssisted) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,6 @@ class CircuitSymbolProcessorTest {
import androidx.compose.ui.Modifier
@CircuitInject(FavoritesScreen::class, AppScope::class)
@Composable
class Favorites : Ui<FavoritesScreen.State> {
@Composable
override fun Content(state: FavoritesScreen.State, modifier: Modifier) {
Expand Down Expand Up @@ -398,7 +397,6 @@ class CircuitSymbolProcessorTest {
import javax.inject.Inject
@CircuitInject(FavoritesScreen::class, AppScope::class)
@Composable
class Favorites @Inject constructor() : Ui<FavoritesScreen.State> {
@Composable
override fun Content(state: FavoritesScreen.State, modifier: Modifier) {
Expand Down Expand Up @@ -451,7 +449,6 @@ class CircuitSymbolProcessorTest {
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@Composable
class Favorites @AssistedInject constructor(
@Assisted private val screen: FavoritesScreen,
) : Ui<FavoritesScreen.State> {
Expand Down Expand Up @@ -656,7 +653,6 @@ class CircuitSymbolProcessorTest {
import androidx.compose.runtime.Composable
@CircuitInject(FavoritesScreen::class, AppScope::class)
@Composable
class FavoritesPresenter : Presenter<FavoritesScreen.State> {
@Composable
override fun present(): FavoritesScreen.State {
Expand Down Expand Up @@ -709,7 +705,6 @@ class CircuitSymbolProcessorTest {
import javax.inject.Inject
@CircuitInject(FavoritesScreen::class, AppScope::class)
@Composable
class FavoritesPresenter @Inject constructor() : Presenter<FavoritesScreen.State> {
@Composable
override fun present(): FavoritesScreen.State {
Expand Down Expand Up @@ -767,7 +762,6 @@ class CircuitSymbolProcessorTest {
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@Composable
class FavoritesPresenter @AssistedInject constructor(
@Assisted private val screen: FavoritesScreen,
@Assisted private val navigator: Navigator,
Expand Down Expand Up @@ -835,7 +829,6 @@ class CircuitSymbolProcessorTest {
import dagger.assisted.AssistedInject
import dagger.hilt.components.SingletonComponent
@Composable
class FavoritesPresenter @AssistedInject constructor(
@Assisted private val screen: FavoritesScreen,
@Assisted private val navigator: Navigator,
Expand Down Expand Up @@ -901,7 +894,6 @@ class CircuitSymbolProcessorTest {
import dagger.assisted.AssistedInject
import dagger.hilt.components.SingletonComponent
@Composable
class FavoritesPresenter @AssistedInject constructor(
@Assisted private val screen: FavoritesScreen,
@Assisted private val navigator: Navigator,
Expand Down Expand Up @@ -1020,7 +1012,6 @@ class CircuitSymbolProcessorTest {
}
@Composable
class Favorites @AssistedInject constructor(
@Assisted private val someString: String,
) : Ui<FavoritesScreen.State> {
Expand All @@ -1042,7 +1033,6 @@ class CircuitSymbolProcessorTest {
}
@Composable
class FavoritesPresenter @AssistedInject constructor(
@Assisted private val someString: String,
) : Presenter<FavoritesScreen.State> {
Expand Down Expand Up @@ -1079,7 +1069,6 @@ class CircuitSymbolProcessorTest {
import androidx.compose.ui.Modifier
@CircuitInject(FavoritesScreen::class, AppScope::class)
@Composable
class Favorites {
@Composable
fun Content(state: FavoritesScreen.State, modifier: Modifier) {
Expand Down Expand Up @@ -1146,6 +1135,79 @@ class CircuitSymbolProcessorTest {
}
}

@Test
fun invalidAssistedInjection() {
assertProcessingError(
sourceFile =
kotlin(
"InvalidAssistedInjection.kt",
"""
package test
import com.slack.circuit.codegen.annotations.CircuitInject
import androidx.compose.runtime.Composable
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import dagger.assisted.AssistedFactory
@CircuitInject(FavoritesScreen::class, AppScope::class)
class Favorites @AssistedInject constructor(@Assisted input: String) : Presenter<FavoritesScreen.State> {
@Composable
override fun present(): FavoritesScreen.State {
}
@AssistedFactory
fun interface Factory {
fun create(input: String): Favorites
}
}
"""
.trimIndent(),
)
) { messages ->
assertThat(messages)
.contains(
"When using @CircuitInject with an @AssistedInject-annotated class, you must put " +
"the @CircuitInject annotation on the @AssistedFactory-annotated nested" +
" class (test.Favorites.Factory)."
)
}
}

@Test
fun invalidAssistedInjection_missingFactory() {
assertProcessingError(
sourceFile =
kotlin(
"InvalidAssistedInjection.kt",
"""
package test
import com.slack.circuit.codegen.annotations.CircuitInject
import androidx.compose.runtime.Composable
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
@CircuitInject(FavoritesScreen::class, AppScope::class)
class Favorites @AssistedInject constructor(@Assisted input: String) : Presenter<FavoritesScreen.State> {
@Composable
override fun present(): FavoritesScreen.State {
}
}
"""
.trimIndent(),
)
) { messages ->
assertThat(messages)
.contains(
"When using @CircuitInject with an @AssistedInject-annotated class, you must put " +
"the @CircuitInject annotation on the @AssistedFactory-annotated nested class."
)
}
}

private enum class CodegenMode {
ANVIL,
HILT
Expand Down

0 comments on commit 93cdbe3

Please sign in to comment.