Skip to content

Commit

Permalink
Support qualifiers for subcomponents
Browse files Browse the repository at this point in the history
Support qualifiers on the factory functions when contributing subcomponents and don't silently drop them.

Fixes #58
  • Loading branch information
vRallev committed Oct 25, 2024
1 parent 37329d8 commit 2b42afd
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import com.google.devtools.ksp.symbol.KSFunctionDeclaration
import com.google.devtools.ksp.symbol.KSNode
import com.google.devtools.ksp.symbol.KSType
import com.google.devtools.ksp.symbol.Visibility
import me.tatarka.inject.annotations.Qualifier
import me.tatarka.inject.annotations.Scope
import software.amazon.lastmile.kotlin.inject.anvil.internal.Origin
import kotlin.reflect.KClass
Expand All @@ -23,6 +24,7 @@ internal interface ContextAware {
val logger: KSPLogger

private val scopeFqName get() = Scope::class.requireQualifiedName()
private val qualifierFqName get() = Qualifier::class.requireQualifiedName()

fun <T : Any> requireNotNull(
value: T?,
Expand Down Expand Up @@ -93,6 +95,16 @@ internal interface ContextAware {
}
}

fun KSAnnotation.isKotlinInjectQualifierAnnotation(): Boolean {
return annotationType.resolve().isKotlinInjectQualifierAnnotation()
}

private fun KSType.isKotlinInjectQualifierAnnotation(): Boolean {
return declaration.annotations.any {
it.annotationType.resolve().declaration.requireQualifiedName() == qualifierFqName
}
}

private fun KSAnnotation.hasScopeParameter(): Boolean {
return (annotationType.resolve().declaration as? KSClassDeclaration)
?.primaryConstructor?.parameters?.firstOrNull()?.name?.asString() == "scope"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.google.devtools.ksp.KspExperimental
import com.google.devtools.ksp.isAnnotationPresent
import com.google.devtools.ksp.processing.CodeGenerator
import com.google.devtools.ksp.processing.KSPLogger
import com.google.devtools.ksp.symbol.KSAnnotation
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.ClassName
Expand All @@ -12,6 +13,7 @@ import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.ParameterSpec
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.ksp.toAnnotationSpec
import com.squareup.kotlinpoet.ksp.toClassName
Expand Down Expand Up @@ -121,8 +123,14 @@ internal class ContributesSubcomponentProcessor(

val function = factoryInterface.factoryFunctions().single()

val parameters = function.parameters.map {
requireNotNull(it.name?.asString()) to it.type.toTypeName()
val parameters = function.parameters.map { valueParameter ->
FactoryParameter(
name = requireNotNull(valueParameter.name?.asString()),
typeName = valueParameter.type.toTypeName(),
qualifier = valueParameter.annotations
.filter { it.isKotlinInjectQualifierAnnotation() }
.singleOrNull(),
)
}

val finalComponentClassName = ClassName(
Expand Down Expand Up @@ -159,14 +167,27 @@ internal class ContributesSubcomponentProcessor(
.build(),
)
.addParameters(
parameters.map { (name, type) ->
parameters.map { parameter ->
ParameterSpec
.builder(name, type)
.builder(parameter.name, parameter.typeName)
.addAnnotation(
AnnotationSpec.builder(Provides::class)
.useSiteTarget(AnnotationSpec.UseSiteTarget.GET)
.build(),
)
.apply {
if (parameter.qualifier != null) {
addAnnotation(
parameter.qualifier
.toAnnotationSpec()
.toBuilder()
.useSiteTarget(
AnnotationSpec.UseSiteTarget.GET,
)
.build(),
)
}
}
.build()
},
)
Expand All @@ -178,9 +199,9 @@ internal class ContributesSubcomponentProcessor(
.build(),
)
.addProperties(
parameters.map { (name, type) ->
PropertySpec.builder(name, type)
.initializer(name)
parameters.map { parameter ->
PropertySpec.builder(parameter.name, parameter.typeName)
.initializer(parameter.name)
.build()
},
)
Expand All @@ -198,9 +219,9 @@ internal class ContributesSubcomponentProcessor(
FunSpec.builder(function.simpleName.asString())
.addModifiers(KModifier.OVERRIDE)
.addParameters(
parameters.map { (name, type) ->
parameters.map { parameter ->
ParameterSpec
.builder(name, type)
.builder(parameter.name, parameter.typeName)
.build()
},
)
Expand All @@ -213,7 +234,7 @@ internal class ContributesSubcomponentProcessor(
separator = ", ",
prefix = ", ",
) {
it.first
it.name
}
}

Expand Down Expand Up @@ -244,4 +265,10 @@ internal class ContributesSubcomponentProcessor(

return finalComponentClassName.nestedClass("Factory")
}

private data class FactoryParameter(
val name: String,
val typeName: TypeName,
val qualifier: KSAnnotation?,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,76 @@ class ContributesSubcomponentProcessorTest {
}
}

@Test
fun `the factory function accepts parameters with qualifier annotations and the parameters are bound in the component`() {
compile(
"""
package software.amazon.test
import software.amazon.lastmile.kotlin.inject.anvil.AppScope
import software.amazon.lastmile.kotlin.inject.anvil.ContributesSubcomponent
import software.amazon.lastmile.kotlin.inject.anvil.ContributesTo
import software.amazon.lastmile.kotlin.inject.anvil.ForScope
import software.amazon.lastmile.kotlin.inject.anvil.MergeComponent
import software.amazon.lastmile.kotlin.inject.anvil.SingleIn
import me.tatarka.inject.annotations.Component
import me.tatarka.inject.annotations.Qualifier
import me.tatarka.inject.annotations.Provides
@Qualifier
annotation class QualifiedString
@Component
@MergeComponent(AppScope::class)
@SingleIn(AppScope::class)
interface ComponentInterface : ComponentInterfaceMerged
@ContributesSubcomponent(LoggedInScope::class)
@SingleIn(LoggedInScope::class)
interface OtherComponent {
@ContributesSubcomponent.Factory(AppScope::class)
interface Parent {
fun otherComponent(
@QualifiedString stringArg: String,
@ForScope(LoggedInScope::class) intArg: Int,
): OtherComponent
}
}
@ContributesTo(LoggedInScope::class)
interface ChildComponent {
@QualifiedString
val string: String
@ForScope(LoggedInScope::class)
val int: Int
}
""",
scopesSource,
// TODO: Enable KSP2 once this bug is solved:
// https://github.com/evant/kotlin-inject/issues/447
useKsp2 = false,
) {
val component = componentInterface.newComponent<Any>()
val childComponent = component::class.java.methods
.single { it.name == "otherComponent" }
.invoke(component, "some string", 5)

assertThat(childComponent).isNotNull()

assertThat(
childComponent::class.java.methods
.single { it.name == "getString" }
.invoke(childComponent),
).isEqualTo("some string")
assertThat(
childComponent::class.java.methods
.single { it.name == "getInt" }
.invoke(childComponent),
).isEqualTo(5)
}
}

@Test
fun `abstract classes are disallowed`() {
compile(
Expand Down

0 comments on commit 2b42afd

Please sign in to comment.