Skip to content

Commit

Permalink
Merge pull request #60 from amzn/rwo/58
Browse files Browse the repository at this point in the history
Support qualifiers for subcomponents
  • Loading branch information
vRallev authored Oct 25, 2024
2 parents afdfcd1 + dd8a5a3 commit 59bb531
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 11 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ jobs:
path: ./**/build/reports/lint-results.html

detekt:
runs-on: ubuntu-latest
runs-on: macos-latest
timeout-minutes: 25

steps:
Expand Down
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,77 @@ class ContributesSubcomponentProcessorTest {
}
}

@Test
@Suppress("ForbiddenComment")
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 59bb531

Please sign in to comment.