Skip to content

Commit

Permalink
Support excluding custom contributions
Browse files Browse the repository at this point in the history
Support excluding classes in the merge phase that use a custom annotations. Usually, custom symbol processors generate intermediate interfaces. If they preserve the `@Origin` annotation, then we can check the entire chain if any class was excluded.
  • Loading branch information
vRallev committed Oct 7, 2024
1 parent a2b850e commit 270778f
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 7 deletions.
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

0 comments on commit 270778f

Please sign in to comment.