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 excluding custom contributions #47

Merged
merged 1 commit into from
Oct 7, 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
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ internal interface ContextAware {
}
}

private fun KSClassDeclaration.scopeOrNull(): MergeScope? {
fun KSClassDeclaration.scopeOrNull(): MergeScope? {
val annotationsWithScopeParameter = annotations.filter { it.hasScopeParameter() }
.toList()
.ifEmpty { return null }
Expand Down Expand Up @@ -129,15 +129,21 @@ internal interface ContextAware {
?.let { it.value as? KSType }
}

fun KSClassDeclaration.origin(): KSClassDeclaration {
val annotation = findAnnotation(Origin::class)
fun KSClassDeclaration.originOrNull(): KSClassDeclaration? {
val annotation = findAnnotations(Origin::class).singleOrNull() ?: return null

val argument = annotation.arguments.firstOrNull { it.name?.asString() == "value" }
?: annotation.arguments.first()

return (argument.value as KSType).declaration as KSClassDeclaration
}

fun KSClassDeclaration.origin(): KSClassDeclaration {
return requireNotNull(originOrNull(), this) {
"Origin annotation not found."
}
}

fun KSClassDeclaration.contributedSubcomponent(): KSClassDeclaration {
return origin().parentDeclaration as KSClassDeclaration
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,22 @@ internal class MergeComponentProcessor(
val componentInterfaces = resolver.getDeclarationsFromPackage(LOOKUP_PACKAGE)
.filterIsInstance<KSClassDeclaration>()
.filter { contributedInterface ->
val origin = contributedInterface.origin()
origin.scope() == scope &&
val originChain = contributedInterface.originChain().toList()

// Check that at least one of the scopes in the chain is matching the target
// scope.
val isSameScope = originChain.any { origin ->
origin.scopeOrNull() == scope
}
if (!isSameScope) {
return@filter false
}

// The scope matches, now check that none of the classes in the chain were
// excluded.
originChain.all { origin ->
origin.requireQualifiedName() !in excludeNames
}
}
.filter {
!it.isAnnotationPresent(Subcomponent::class) ||
Expand Down Expand Up @@ -215,4 +228,10 @@ internal class MergeComponentProcessor(
?.map { it.declaration as KSClassDeclaration }
?: emptyList()
}

private fun KSClassDeclaration.originChain(): Sequence<KSClassDeclaration> {
return generateSequence(origin()) { clazz ->
clazz.originOrNull()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@ import com.squareup.kotlinpoet.ksp.writeTo
import com.tschuchort.compiletesting.KotlinCompilation.ExitCode.OK
import me.tatarka.inject.annotations.Provides
import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource
import software.amazon.lastmile.kotlin.inject.anvil.Compilation
import software.amazon.lastmile.kotlin.inject.anvil.ContributesTo
import software.amazon.lastmile.kotlin.inject.anvil.OPTION_CONTRIBUTING_ANNOTATIONS
import software.amazon.lastmile.kotlin.inject.anvil.SingleIn
import software.amazon.lastmile.kotlin.inject.anvil.addOriginAnnotation
import software.amazon.lastmile.kotlin.inject.anvil.compile
import software.amazon.lastmile.kotlin.inject.anvil.componentInterface
import software.amazon.lastmile.kotlin.inject.anvil.mergedComponent
Expand Down Expand Up @@ -153,6 +155,55 @@ class CustomSymbolProcessorTest {
}
}

@Test
fun `custom contribution can be excluded in the merge component`() {
// The custom symbol processor will generate a component interface that is contributed
// to the final component. The generated component interface has a provider method
// for the type String.
//
// Notice that we contribute two renderers, which would result in duplicate bindings:
// e: Error occurred in KSP, check log for detail
// e: [ksp] /var/folders/rs/q_sbtnln4xzb4h17_tdwq2g00000gr/T/Kotlin-Compilation15827920485744140694/ksp/sources/kotlin/software/amazon/test/Renderer2Component.kt:14: Cannot provide: String
// e: [ksp] /var/folders/rs/q_sbtnln4xzb4h17_tdwq2g00000gr/T/Kotlin-Compilation15827920485744140694/ksp/sources/kotlin/software/amazon/test/Renderer1Component.kt:14: as it is already provided
//
// But since one renderer is excluded the duplicate binding doesn't happen.
Compilation()
.configureKotlinInjectAnvilProcessor(
symbolProcessorProviders = setOf(symbolProcessorProvider),
)
.compile(
"""
package software.amazon.test

import me.tatarka.inject.annotations.Component
import software.amazon.lastmile.kotlin.inject.anvil.extend.ContributingAnnotation
import software.amazon.lastmile.kotlin.inject.anvil.MergeComponent
import software.amazon.lastmile.kotlin.inject.anvil.SingleIn
import kotlin.annotation.AnnotationTarget.CLASS

@ContributingAnnotation
annotation class ContributesRenderer

@ContributesRenderer
class Renderer1

@ContributesRenderer
class Renderer2

@Component
@MergeComponent(Unit::class, exclude = [Renderer2::class])
@SingleIn(Unit::class)
interface ComponentInterface : ComponentInterfaceMerged {
val string: String
}
""",
)
.run {
assertThat(exitCode).isEqualTo(OK)
assertThat(componentInterface.mergedComponent).isNotNull()
}
}

private val symbolProcessorProvider = object : SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
return object : SymbolProcessor {
Expand All @@ -161,15 +212,17 @@ class CustomSymbolProcessorTest {
.getSymbolsWithAnnotation("software.amazon.test.ContributesRenderer")
.filterIsInstance<KSClassDeclaration>()
.forEach { clazz ->
val key = clazz.simpleName.asString()
val componentClassName = ClassName(
"software.amazon.test",
"RendererComponent",
"${key}Component",
)
val fileSpec = FileSpec.builder(componentClassName)
.addType(
TypeSpec
.interfaceBuilder(componentClassName)
.addOriginatingKSFile(clazz.containingFile!!)
.addOriginAnnotation(clazz)
.addAnnotation(
AnnotationSpec.builder(ContributesTo::class)
.addMember("Unit::class")
Expand All @@ -182,7 +235,7 @@ class CustomSymbolProcessorTest {
)
.addFunction(
FunSpec
.builder("provideString")
.builder("provideString$key")
.addAnnotation(Provides::class)
.returns(String::class)
.addCode("return \"renderer\"")
Expand Down