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

Support qualifiers for subcomponents #60

Merged
merged 1 commit into from
Oct 25, 2024
Merged
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
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