diff --git a/compiler/build.gradle b/compiler/build.gradle index 892be49..4f2b35d 100644 --- a/compiler/build.gradle +++ b/compiler/build.gradle @@ -22,6 +22,7 @@ dependencies { // Gives us access to annotations. implementation libs.kotlin.inject.runtime + testImplementation project(':runtime-optional') testImplementation libs.assertk testImplementation libs.kotlin.compile.testing.core testImplementation libs.kotlin.compile.testing.ksp diff --git a/compiler/src/main/kotlin/software/amazon/lastmile/kotlin/inject/anvil/ContextAware.kt b/compiler/src/main/kotlin/software/amazon/lastmile/kotlin/inject/anvil/ContextAware.kt index 1ba6844..d9e3199 100644 --- a/compiler/src/main/kotlin/software/amazon/lastmile/kotlin/inject/anvil/ContextAware.kt +++ b/compiler/src/main/kotlin/software/amazon/lastmile/kotlin/inject/anvil/ContextAware.kt @@ -64,21 +64,97 @@ internal interface ContextAware { check(clazz.classKind == ClassKind.INTERFACE, clazz, lazyMessage) } - fun KSClassDeclaration.scope(): KSAnnotation { + fun checkHasScope(clazz: KSClassDeclaration) { + // Ensures that the value is non-null. + clazz.scope() + } + + fun KSClassDeclaration.scope(): MergeScope { return requireNotNull(scopeOrNull(), this) { "Couldn't find scope annotation for $this." } } - fun KSClassDeclaration.scopeOrNull(): KSAnnotation? = - annotations.firstOrNull { isScopeAnnotation(it) } + private fun KSClassDeclaration.scopeOrNull(): MergeScope? { + val annotationsWithScopeParameter = annotations.filter { + // Avoid scope annotations themselves, e.g. that skips `@SingleIn` and include + // annotations with a "scope" parameter, e.g. `@ContributesTo`. + !isScopeAnnotation(it) && it.hasScopeParameter() + }.toList() + + return if (annotationsWithScopeParameter.isEmpty()) { + annotations.firstOrNull { isScopeAnnotation(it) } + ?.let { MergeScope(this@ContextAware, it) } + } else { + scopeForAnnotationsWithScopeParameters(this, annotationsWithScopeParameter) + } + } + + fun isScopeAnnotation(annotation: KSAnnotation): Boolean { + return isScopeAnnotation(annotation.annotationType.resolve()) + } - private fun isScopeAnnotation(annotation: KSAnnotation): Boolean { - return annotation.annotationType.resolve().declaration.annotations.any { + private fun isScopeAnnotation(type: KSType): Boolean { + return type.declaration.annotations.any { it.annotationType.resolve().declaration.requireQualifiedName() == scopeFqName } } + private fun KSAnnotation.hasScopeParameter(): Boolean { + return (annotationType.resolve().declaration as? KSClassDeclaration) + ?.primaryConstructor?.parameters?.firstOrNull()?.name?.asString() == "scope" + } + + private fun scopeForAnnotationsWithScopeParameters( + clazz: KSClassDeclaration, + annotations: List, + ): MergeScope { + val explicitScopes = annotations.mapNotNull { annotation -> + annotation.scopeParameter(this) + } + + val classScope = clazz.annotations.firstOrNull { isScopeAnnotation(it) } + ?.let { MergeScope(this, it) } + + if (explicitScopes.isNotEmpty()) { + check(explicitScopes.size == annotations.size, clazz) { + "If one annotation has an explicit scope, then all " + + "annotations must specify an explicit scope." + } + + explicitScopes.scan( + explicitScopes.first().declaration.requireQualifiedName(), + ) { previous, next -> + check(previous == next.declaration.requireQualifiedName(), clazz) { + "All explicit scopes on annotations must be the same." + } + previous + } + + val explicitScope = explicitScopes.first() + val explicitScopeIsScope = isScopeAnnotation(explicitScope) + + return if (explicitScopeIsScope) { + MergeScope( + contextAware = this, + annotationType = explicitScope, + markerType = null, + ) + } else { + MergeScope( + contextAware = this, + annotationType = null, + markerType = explicitScope, + ) + } + } + + return requireNotNull(classScope, clazz) { + "Couldn't find scope for ${clazz.simpleName.asString()}. For unscoped " + + "objects it is required to specify the target scope on the annotation." + } + } + fun KSClassDeclaration.origin(): KSClassDeclaration { val annotation = findAnnotation(Origin::class) @@ -98,11 +174,6 @@ internal interface ContextAware { fun KSClassDeclaration.findAnnotations(annotation: KClass): List { val fqName = annotation.requireQualifiedName() return annotations.filter { it.isAnnotation(fqName) }.toList() - .also { - check(it.isNotEmpty(), this) { - "Couldn't find the @${annotation.simpleName} annotation for $this." - } - } } fun KSAnnotation.isAnnotation(fqName: String): Boolean { @@ -117,23 +188,11 @@ internal interface ContextAware { "Containing file was null for $this" } - fun KSDeclaration.requireQualifiedName(): String = - requireNotNull(qualifiedName?.asString(), this) { - "Qualified name was null for $this" - } - - fun KClass<*>.requireQualifiedName(): String = requireNotNull(qualifiedName) { - "Qualified name was null for $this" - } + fun KSDeclaration.requireQualifiedName(): String = requireQualifiedName(this@ContextAware) fun Resolver.getSymbolsWithAnnotation(annotation: KClass<*>): Sequence = getSymbolsWithAnnotation(annotation.requireQualifiedName()) - fun KSAnnotation.isSameAs(other: KSAnnotation): Boolean { - return annotationType.resolve().declaration.requireQualifiedName() == - other.annotationType.resolve().declaration.requireQualifiedName() - } - fun KSDeclaration.innerClassNames(separator: String = ""): String { val classNames = requireQualifiedName().substring(packageName.asString().length + 1) return classNames.replace(".", separator) diff --git a/compiler/src/main/kotlin/software/amazon/lastmile/kotlin/inject/anvil/MergeScope.kt b/compiler/src/main/kotlin/software/amazon/lastmile/kotlin/inject/anvil/MergeScope.kt new file mode 100644 index 0000000..27c5df8 --- /dev/null +++ b/compiler/src/main/kotlin/software/amazon/lastmile/kotlin/inject/anvil/MergeScope.kt @@ -0,0 +1,157 @@ +package software.amazon.lastmile.kotlin.inject.anvil + +import com.google.devtools.ksp.symbol.KSAnnotation +import com.google.devtools.ksp.symbol.KSType +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.ksp.toAnnotationSpec +import com.squareup.kotlinpoet.ksp.toClassName + +/** + * Represents the destination of contributed types and which types should be merged during + * the merge phase. There is complexity to this problem, because `kotlin-inject` didn't + * support parameters for scopes initially and our Anvil extensions added support for that. Later, + * we started supporting parameters, which changed the API. E.g. one could use: + * ``` + * @ContributesTo(AppScope::class) + * interface ContributedComponentInterface + * + * @Component + * @MergeComponent(AppScope::class) + * interface MergedComponent + * ``` + * Or the old way: + * ``` + * @ContributesTo + * @SingleInAppScope + * interface ContributedComponentInterface + * + * @Component + * @MergeComponent + * @SingleInAppScope + * interface MergedComponent + * ``` + */ +internal sealed class MergeScope { + /** + * The fully qualified name of the annotation used as scope, e.g. + * ``` + * @ContributesTo + * @SingleInAppScope + * interface Abc + * ``` + * Note that the annotation itself is annotated with `@Scope`. + * + * The value is `null`, when only a marker is used, e.g. + * ``` + * @ContributesTo(AppScope::class) + * interface Abc + * ``` + * + * If the `scope` parameter is used and the argument is annotated with `@Scope`, then + * this value is non-null, e.g. for this: + * ``` + * @ContributesBinding(scope = SingleInAppScope::class) + * class Binding : SuperType + * ``` + */ + abstract val annotationFqName: String? + + /** + * A marker for a scope that isn't itself annotated with `@Scope`, e.g. + * ``` + * @ContributesTo(AppScope::class) + * interface Abc + * ``` + * + * The value is null, if no marker is used, e.g. + * ``` + * @ContributesTo + * @SingleInAppScope + * interface Abc + * ``` + * + * The value is also null, when the `scope` parameter is used and the argument is annotated + * with `@Scope`, e.g. + * ``` + * @ContributesBinding(scope = SingleInAppScope::class) + * class Binding : SuperType + * ``` + */ + abstract val markerFqName: String? + + /** + * A reference to the scope. + * + * [markerFqName] is preferred, because it allows us to decouple contributions from + * kotlin-inject's scoping mechanism. E.g. imagine someone using `@Singleton` as a scope, and + * they'd like to adopt kotlin-inject-anvil with `@ContributesTo(AppScope::class)`. Because we + * prefer the marker, this would be supported. + */ + val fqName: String get() = requireNotNull(markerFqName ?: annotationFqName) + + abstract fun toAnnotationSpec(): AnnotationSpec + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is MergeScope) return false + + if (fqName != other.fqName) return false + + return true + } + + override fun hashCode(): Int { + return fqName.hashCode() + } + + private class MarkerBasedMergeScope( + override val annotationFqName: String, + override val markerFqName: String?, + private val ksAnnotation: KSAnnotation, + ) : MergeScope() { + override fun toAnnotationSpec(): AnnotationSpec { + return ksAnnotation.toAnnotationSpec() + } + } + + private class AnnotationBasedMergeScope( + override val annotationFqName: String?, + override val markerFqName: String?, + private val ksType: KSType, + ) : MergeScope() { + override fun toAnnotationSpec(): AnnotationSpec { + return AnnotationSpec.builder(ksType.toClassName()).build() + } + } + + companion object { + operator fun invoke( + contextAware: ContextAware, + annotationType: KSType?, + markerType: KSType?, + ): MergeScope { + val nonNullType = contextAware.requireNotNull(markerType ?: annotationType, null) { + "Couldn't determine scope. No scope annotation nor marker found." + } + + return AnnotationBasedMergeScope( + annotationFqName = annotationType?.declaration?.requireQualifiedName(contextAware), + markerFqName = markerType?.declaration?.requireQualifiedName(contextAware), + ksType = nonNullType, + ) + } + + operator fun invoke( + contextAware: ContextAware, + ksAnnotation: KSAnnotation, + ): MergeScope { + return MarkerBasedMergeScope( + annotationFqName = ksAnnotation.annotationType.resolve().declaration + .requireQualifiedName(contextAware), + markerFqName = ksAnnotation.scopeParameter(contextAware)?.declaration + ?.requireQualifiedName(contextAware), + ksAnnotation = ksAnnotation, + ) + } + } +} diff --git a/compiler/src/main/kotlin/software/amazon/lastmile/kotlin/inject/anvil/Util.kt b/compiler/src/main/kotlin/software/amazon/lastmile/kotlin/inject/anvil/Util.kt index 081849c..f392612 100644 --- a/compiler/src/main/kotlin/software/amazon/lastmile/kotlin/inject/anvil/Util.kt +++ b/compiler/src/main/kotlin/software/amazon/lastmile/kotlin/inject/anvil/Util.kt @@ -3,12 +3,15 @@ package software.amazon.lastmile.kotlin.inject.anvil import com.google.devtools.ksp.isDefault import com.google.devtools.ksp.symbol.KSAnnotation import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSDeclaration +import com.google.devtools.ksp.symbol.KSType import com.google.devtools.ksp.symbol.KSValueArgument import com.squareup.kotlinpoet.Annotatable import com.squareup.kotlinpoet.AnnotationSpec import com.squareup.kotlinpoet.ksp.toClassName import software.amazon.lastmile.kotlin.inject.anvil.internal.Origin import java.util.Locale +import kotlin.reflect.KClass /** * The package in which code is generated that should be picked up during the merging phase. @@ -62,3 +65,21 @@ internal fun KSAnnotation.argumentAt(name: String): KSValueArgument? { return arguments.find { it.name?.asString() == name } ?.takeUnless { it.isDefault() } } + +internal fun KSDeclaration.requireQualifiedName(contextAware: ContextAware): String = + contextAware.requireNotNull(qualifiedName?.asString(), this) { + "Qualified name was null for $this" + } + +internal fun KClass<*>.requireQualifiedName(): String = requireNotNull(qualifiedName) { + "Qualified name was null for $this" +} + +internal fun KSAnnotation.scopeParameter(contextAware: ContextAware): KSType? { + return arguments.firstOrNull { it.name?.asString() == "scope" } + ?.let { it.value as? KSType } + ?.takeIf { + it.declaration.requireQualifiedName(contextAware) != + Unit::class.requireQualifiedName() + } +} diff --git a/compiler/src/main/kotlin/software/amazon/lastmile/kotlin/inject/anvil/processor/ContributesBindingProcessor.kt b/compiler/src/main/kotlin/software/amazon/lastmile/kotlin/inject/anvil/processor/ContributesBindingProcessor.kt index 8c8b6bd..b6cb14f 100644 --- a/compiler/src/main/kotlin/software/amazon/lastmile/kotlin/inject/anvil/processor/ContributesBindingProcessor.kt +++ b/compiler/src/main/kotlin/software/amazon/lastmile/kotlin/inject/anvil/processor/ContributesBindingProcessor.kt @@ -24,6 +24,8 @@ import software.amazon.lastmile.kotlin.inject.anvil.LOOKUP_PACKAGE import software.amazon.lastmile.kotlin.inject.anvil.addOriginAnnotation import software.amazon.lastmile.kotlin.inject.anvil.argumentOfTypeAt import software.amazon.lastmile.kotlin.inject.anvil.decapitalize +import software.amazon.lastmile.kotlin.inject.anvil.requireQualifiedName +import kotlin.reflect.KClass /** * Generates the code for [ContributesBinding]. @@ -43,7 +45,6 @@ import software.amazon.lastmile.kotlin.inject.anvil.decapitalize * ``` * package $LOOKUP_PACKAGE * - * @SingleInAppScope * @Origin(ComponentInterface::class) * interface SoftwareAmazonTestRealAuthenticator { * @Provides fun provideRealAuthenticatorAuthenticator( @@ -65,6 +66,7 @@ internal class ContributesBindingProcessor( .filterIsInstance() .onEach { checkIsPublic(it) + checkHasScope(it) } .forEach { generateComponentInterface(it) @@ -77,11 +79,9 @@ internal class ContributesBindingProcessor( private fun generateComponentInterface(clazz: KSClassDeclaration) { val componentClassName = ClassName(LOOKUP_PACKAGE, clazz.safeClassName) - val annotations = clazz.findAnnotations(ContributesBinding::class) + val annotations = clazz.findAnnotationsAtLeastOne(ContributesBinding::class) checkNoDuplicateBoundTypes(clazz, annotations) - val scope = scope(clazz, annotations) - val boundTypes = annotations .map { GeneratedFunction( @@ -96,7 +96,6 @@ internal class ContributesBindingProcessor( TypeSpec .interfaceBuilder(componentClassName) .addOriginatingKSFile(clazz.requireContainingFile()) - .addAnnotation(scope.toClassName()) .addOriginAnnotation(clazz) .addFunctions( boundTypes.map { function -> @@ -158,70 +157,6 @@ internal class ContributesBindingProcessor( } } - private fun scope( - clazz: KSClassDeclaration, - annotations: List, - ): KSType { - val explicitScopes = annotations.mapNotNull { annotation -> - annotation.arguments.firstOrNull { it.name?.asString() == "scope" } - ?.let { it.value as? KSType } - ?.takeIf { - it.declaration.requireQualifiedName() != - Annotation::class.requireQualifiedName() - } - } - - val classScope = clazz.scopeOrNull()?.annotationType?.resolve() - - if (explicitScopes.isNotEmpty()) { - check(explicitScopes.size == annotations.size, clazz) { - "If one @ContributesBinding annotation has an explicit scope, then all " + - "annotations must specify an explicit scope." - } - - explicitScopes.scan( - explicitScopes.first().declaration.requireQualifiedName(), - ) { previous, next -> - check(previous == next.declaration.requireQualifiedName(), clazz) { - "All explicit scopes on @ContributesBinding annotations must be the same." - } - previous - } - - val explicitScope = explicitScopes.first() - - if (classScope != null) { - check( - classScope.declaration.requireQualifiedName() == - explicitScope.declaration.requireQualifiedName(), - clazz, - ) { - "A scope was defined explicitly on the @ContributesBinding annotation " + - "`${explicitScope.declaration.requireQualifiedName()}` and the class " + - "itself is scoped using " + - "`${classScope.declaration.requireQualifiedName()}`. It's not allowed " + - "to mix different scopes." - } - } - - check(classScope == null, clazz) { - "A scope was defined explicitly on the @ContributesBinding annotation " + - "`${explicitScope.declaration.requireQualifiedName()}` and the class itself " + - "is scoped using `${classScope!!.declaration.requireQualifiedName()}`. In " + - "this case the explicit scope on the @ContributesBinding annotation can be " + - "removed." - } - - return explicitScope - } - - return requireNotNull(classScope, clazz) { - "Couldn't find scope for ${clazz.simpleName.asString()}. For unscoped " + - "objects it is required to specify the target scope on the @ContributesBinding " + - "annotation." - } - } - private fun boundTypeFromAnnotation(annotation: KSAnnotation): KSType? { return annotation.arguments.firstOrNull { it.name?.asString() == "boundType" } ?.let { it.value as? KSType } @@ -266,6 +201,16 @@ internal class ContributesBindingProcessor( } } + private fun KSClassDeclaration.findAnnotationsAtLeastOne( + annotation: KClass, + ): List { + return findAnnotations(annotation).also { + check(it.isNotEmpty(), this) { + "Couldn't find the @${annotation.simpleName} annotation for $this." + } + } + } + private inner class GeneratedFunction( boundType: KSType, val multibinding: Boolean, diff --git a/compiler/src/main/kotlin/software/amazon/lastmile/kotlin/inject/anvil/processor/ContributesSubcomponentFactoryProcessor.kt b/compiler/src/main/kotlin/software/amazon/lastmile/kotlin/inject/anvil/processor/ContributesSubcomponentFactoryProcessor.kt index 769ee2c..6a9ec8f 100644 --- a/compiler/src/main/kotlin/software/amazon/lastmile/kotlin/inject/anvil/processor/ContributesSubcomponentFactoryProcessor.kt +++ b/compiler/src/main/kotlin/software/amazon/lastmile/kotlin/inject/anvil/processor/ContributesSubcomponentFactoryProcessor.kt @@ -12,7 +12,6 @@ import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.FileSpec import com.squareup.kotlinpoet.TypeSpec import com.squareup.kotlinpoet.ksp.addOriginatingKSFile -import com.squareup.kotlinpoet.ksp.toAnnotationSpec import com.squareup.kotlinpoet.ksp.toClassName import com.squareup.kotlinpoet.ksp.writeTo import software.amazon.lastmile.kotlin.inject.anvil.ContextAware @@ -46,7 +45,6 @@ import software.amazon.lastmile.kotlin.inject.anvil.internal.Subcomponent * ``` * package $LOOKUP_PACKAGE * - * @ParentScope * @Origin(Subcomponent.Factory::class) * @Subcomponent * interface SoftwareAmazonTestSubcomponentFactory : Subcomponent.Factory @@ -66,6 +64,7 @@ internal class ContributesSubcomponentFactoryProcessor( checkIsPublic(it) checkInnerClass(it) checkSingleFunction(it) + checkHasScope(it) } .forEach { generateComponentInterfaceForFactory(it) @@ -88,14 +87,12 @@ internal class ContributesSubcomponentFactoryProcessor( private fun generateComponentInterfaceForFactory(factory: KSClassDeclaration) { val componentClassName = ClassName(LOOKUP_PACKAGE, factory.safeClassName) - val scope = factory.scope() val fileSpec = FileSpec.builder(componentClassName) .addType( TypeSpec .interfaceBuilder(componentClassName) .addOriginatingKSFile(factory.requireContainingFile()) - .addAnnotation(scope.toAnnotationSpec()) .addOriginAnnotation(factory) .addAnnotation(Subcomponent::class) .addSuperinterface(factory.toClassName()) diff --git a/compiler/src/main/kotlin/software/amazon/lastmile/kotlin/inject/anvil/processor/ContributesSubcomponentProcessor.kt b/compiler/src/main/kotlin/software/amazon/lastmile/kotlin/inject/anvil/processor/ContributesSubcomponentProcessor.kt index 452fce4..3a0b93e 100644 --- a/compiler/src/main/kotlin/software/amazon/lastmile/kotlin/inject/anvil/processor/ContributesSubcomponentProcessor.kt +++ b/compiler/src/main/kotlin/software/amazon/lastmile/kotlin/inject/anvil/processor/ContributesSubcomponentProcessor.kt @@ -13,7 +13,6 @@ import com.squareup.kotlinpoet.KModifier import com.squareup.kotlinpoet.ParameterSpec import com.squareup.kotlinpoet.PropertySpec import com.squareup.kotlinpoet.TypeSpec -import com.squareup.kotlinpoet.ksp.toAnnotationSpec import com.squareup.kotlinpoet.ksp.toClassName import com.squareup.kotlinpoet.ksp.toTypeName import com.squareup.kotlinpoet.ksp.writeTo @@ -23,6 +22,7 @@ import software.amazon.lastmile.kotlin.inject.anvil.ContextAware import software.amazon.lastmile.kotlin.inject.anvil.ContributesSubcomponent import software.amazon.lastmile.kotlin.inject.anvil.LOOKUP_PACKAGE import software.amazon.lastmile.kotlin.inject.anvil.MergeComponent +import software.amazon.lastmile.kotlin.inject.anvil.MergeScope import software.amazon.lastmile.kotlin.inject.anvil.addOriginAnnotation import software.amazon.lastmile.kotlin.inject.anvil.internal.Subcomponent @@ -118,7 +118,15 @@ internal class ContributesSubcomponentProcessor( factoryInterface: KSClassDeclaration, generatedFactoryInterface: KSClassDeclaration, ): ClassName { - val scope = subcomponent.scope() + val scope = requireNotNull( + value = subcomponent.annotations + .firstOrNull { isScopeAnnotation(it) } + ?.let { MergeScope(this, it) }, + symbol = subcomponent, + ) { + "A scope like @SingleIn(Abc::class) is missing." + } + val function = factoryInterface.factoryFunctions().single() val parameters = function.parameters.map { diff --git a/compiler/src/main/kotlin/software/amazon/lastmile/kotlin/inject/anvil/processor/ContributesToProcessor.kt b/compiler/src/main/kotlin/software/amazon/lastmile/kotlin/inject/anvil/processor/ContributesToProcessor.kt index 4c67e6e..ac8dcca 100644 --- a/compiler/src/main/kotlin/software/amazon/lastmile/kotlin/inject/anvil/processor/ContributesToProcessor.kt +++ b/compiler/src/main/kotlin/software/amazon/lastmile/kotlin/inject/anvil/processor/ContributesToProcessor.kt @@ -10,7 +10,6 @@ import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.FileSpec import com.squareup.kotlinpoet.TypeSpec import com.squareup.kotlinpoet.ksp.addOriginatingKSFile -import com.squareup.kotlinpoet.ksp.toAnnotationSpec import com.squareup.kotlinpoet.ksp.toClassName import com.squareup.kotlinpoet.ksp.writeTo import software.amazon.lastmile.kotlin.inject.anvil.ContextAware @@ -35,7 +34,6 @@ import software.amazon.lastmile.kotlin.inject.anvil.addOriginAnnotation * ``` * package $LOOKUP_PACKAGE * - * @SingleInAppScope * @Origin(ComponentInterface::class) * interface SoftwareAmazonTestComponentInterface : ComponentInterface * ``` @@ -51,6 +49,7 @@ internal class ContributesToProcessor( .onEach { checkIsInterface(it) checkIsPublic(it) + checkHasScope(it) } .forEach { generateComponentInterface(it) @@ -61,14 +60,12 @@ internal class ContributesToProcessor( private fun generateComponentInterface(clazz: KSClassDeclaration) { val componentClassName = ClassName(LOOKUP_PACKAGE, clazz.safeClassName) - val scope = clazz.scope() val fileSpec = FileSpec.builder(componentClassName) .addType( TypeSpec .interfaceBuilder(componentClassName) .addOriginatingKSFile(clazz.requireContainingFile()) - .addAnnotation(scope.toAnnotationSpec()) .addOriginAnnotation(clazz) .addSuperinterface(clazz.toClassName()) .build(), diff --git a/compiler/src/main/kotlin/software/amazon/lastmile/kotlin/inject/anvil/processor/MergeComponentProcessor.kt b/compiler/src/main/kotlin/software/amazon/lastmile/kotlin/inject/anvil/processor/MergeComponentProcessor.kt index edbce84..e389068 100644 --- a/compiler/src/main/kotlin/software/amazon/lastmile/kotlin/inject/anvil/processor/MergeComponentProcessor.kt +++ b/compiler/src/main/kotlin/software/amazon/lastmile/kotlin/inject/anvil/processor/MergeComponentProcessor.kt @@ -16,7 +16,6 @@ import com.google.devtools.ksp.symbol.KSType import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.FileSpec import com.squareup.kotlinpoet.TypeSpec -import com.squareup.kotlinpoet.ksp.toAnnotationSpec import com.squareup.kotlinpoet.ksp.toClassName import com.squareup.kotlinpoet.ksp.writeTo import software.amazon.lastmile.kotlin.inject.anvil.ContextAware @@ -28,6 +27,7 @@ import software.amazon.lastmile.kotlin.inject.anvil.MergeComponent import software.amazon.lastmile.kotlin.inject.anvil.OPTION_CONTRIBUTING_ANNOTATIONS import software.amazon.lastmile.kotlin.inject.anvil.extend.ContributingAnnotation import software.amazon.lastmile.kotlin.inject.anvil.internal.Subcomponent +import software.amazon.lastmile.kotlin.inject.anvil.requireQualifiedName /** * Generates the code for [MergeComponent]. @@ -151,8 +151,11 @@ internal class MergeComponentProcessor( val componentInterfaces = resolver.getDeclarationsFromPackage(LOOKUP_PACKAGE) .filterIsInstance() - .filter { it.scope().isSameAs(scope) } - .filter { it.origin().requireQualifiedName() !in excludeNames } + .filter { contributedInterface -> + val origin = contributedInterface.origin() + origin.scope() == scope && + origin.requireQualifiedName() !in excludeNames + } .filter { !it.isAnnotationPresent(Subcomponent::class) || it.contributedSubcomponent().requireQualifiedName() !in excludeNames @@ -168,7 +171,6 @@ internal class MergeComponentProcessor( .addType( TypeSpec .interfaceBuilder(className) - .addAnnotation(scope.toAnnotationSpec()) .addSuperinterfaces( generatedSubcomponents + componentInterfaces.map { it.toClassName() }, ) diff --git a/compiler/src/test/kotlin/software/amazon/lastmile/kotlin/inject/anvil/MergeScopeParserTest.kt b/compiler/src/test/kotlin/software/amazon/lastmile/kotlin/inject/anvil/MergeScopeParserTest.kt new file mode 100644 index 0000000..42bdac3 --- /dev/null +++ b/compiler/src/test/kotlin/software/amazon/lastmile/kotlin/inject/anvil/MergeScopeParserTest.kt @@ -0,0 +1,394 @@ +@file:OptIn(ExperimentalCompilerApi::class) + +package software.amazon.lastmile.kotlin.inject.anvil + +import assertk.Assert +import assertk.all +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.prop +import com.google.devtools.ksp.getClassDeclarationByName +import com.google.devtools.ksp.processing.KSPLogger +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.processing.SymbolProcessorProvider +import com.google.devtools.ksp.symbol.KSAnnotated +import com.tschuchort.compiletesting.KotlinCompilation.ExitCode.OK +import org.intellij.lang.annotations.Language +import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi +import org.junit.jupiter.api.Test + +class MergeScopeParserTest { + + @Test + fun `the scope can be parsed from a scope annotation with zero args`() { + compileInPlace( + """ + package software.amazon.test + + import software.amazon.lastmile.kotlin.inject.anvil.ContributesTo + import software.amazon.lastmile.kotlin.inject.anvil.ContributesBinding + + @ContributesTo + @SingleInAppScope + interface ComponentInterface + + @ContributesBinding + @SingleInAppScope + interface Binding : CharSequence + """, + ) { resolver -> + assertThat(resolver.clazz("software.amazon.test.ComponentInterface").scope()).isEqualTo( + fqName = "software.amazon.test.SingleInAppScope", + annotationFqName = "software.amazon.test.SingleInAppScope", + markerFqName = null, + ) + assertThat(resolver.clazz("software.amazon.test.Binding").scope()).isEqualTo( + fqName = "software.amazon.test.SingleInAppScope", + annotationFqName = "software.amazon.test.SingleInAppScope", + markerFqName = null, + ) + } + } + + @Test + fun `the scope can be parsed from a @ContributesBinding annotation`() { + compileInPlace( + """ + package software.amazon.test + + import software.amazon.lastmile.kotlin.inject.anvil.ContributesBinding + + @ContributesBinding(scope = SingleInAppScope::class) + interface Binding : CharSequence + """, + ) { resolver -> + assertThat(resolver.clazz("software.amazon.test.Binding").scope()).isEqualTo( + fqName = "software.amazon.test.SingleInAppScope", + annotationFqName = "software.amazon.test.SingleInAppScope", + markerFqName = null, + ) + } + } + + @Test + fun `the scope can be parsed from a @ContributesTo annotation`() { + compileInPlace( + """ + package software.amazon.test + + import software.amazon.lastmile.kotlin.inject.anvil.AppScope + import software.amazon.lastmile.kotlin.inject.anvil.ContributesTo + + @ContributesTo(AppScope::class) + interface ComponentInterface1 + + @ContributesTo(SingleInAppScope::class) + interface ComponentInterface2 + """, + ) { resolver -> + assertThat( + resolver.clazz("software.amazon.test.ComponentInterface1").scope(), + ).isEqualTo( + fqName = "software.amazon.lastmile.kotlin.inject.anvil.AppScope", + annotationFqName = null, + markerFqName = "software.amazon.lastmile.kotlin.inject.anvil.AppScope", + ) + assertThat( + resolver.clazz("software.amazon.test.ComponentInterface2").scope(), + ).isEqualTo( + fqName = "software.amazon.test.SingleInAppScope", + annotationFqName = "software.amazon.test.SingleInAppScope", + markerFqName = null, + ) + } + } + + @Test + fun `the scope can be parsed from a @ContributesSubcomponent annotation`() { + compileInPlace( + """ + package software.amazon.test + + import me.tatarka.inject.annotations.Scope + import software.amazon.lastmile.kotlin.inject.anvil.AppScope + import software.amazon.lastmile.kotlin.inject.anvil.ContributesSubcomponent + + @Scope + annotation class ChildScope + + @ContributesSubcomponent(AppScope::class) + interface SubcomponentInterface1 { + @ContributesSubcomponent.Factory(String::class) + interface Factory { + fun createSubcomponentInterface(): SubcomponentInterface1 + } + } + + @ContributesSubcomponent + @SingleInAppScope + interface SubcomponentInterface2 { + @ContributesSubcomponent.Factory + @ChildScope + interface Factory { + fun createSubcomponentInterface(): SubcomponentInterface2 + } + } + """, + ) { resolver -> + assertThat( + resolver.clazz("software.amazon.test.SubcomponentInterface1").scope(), + ).isEqualTo( + fqName = "software.amazon.lastmile.kotlin.inject.anvil.AppScope", + annotationFqName = null, + markerFqName = "software.amazon.lastmile.kotlin.inject.anvil.AppScope", + ) + assertThat( + resolver.clazz("software.amazon.test.SubcomponentInterface1.Factory").scope(), + ).isEqualTo( + fqName = "kotlin.String", + annotationFqName = null, + markerFqName = "kotlin.String", + ) + + assertThat( + resolver.clazz("software.amazon.test.SubcomponentInterface2").scope(), + ).isEqualTo( + fqName = "software.amazon.test.SingleInAppScope", + annotationFqName = "software.amazon.test.SingleInAppScope", + markerFqName = null, + ) + assertThat( + resolver.clazz("software.amazon.test.SubcomponentInterface2.Factory").scope(), + ).isEqualTo( + fqName = "software.amazon.test.ChildScope", + annotationFqName = "software.amazon.test.ChildScope", + markerFqName = null, + ) + } + } + + @Test + fun `the scope can be parsed from a @MergeComponent annotation`() { + compileInPlace( + """ + package software.amazon.test + + import me.tatarka.inject.annotations.Component + import software.amazon.lastmile.kotlin.inject.anvil.AppScope + import software.amazon.lastmile.kotlin.inject.anvil.MergeComponent + import software.amazon.lastmile.kotlin.inject.anvil.SingleIn + + @Component + @MergeComponent(AppScope::class) + abstract class ComponentInterface1 : ComponentInterface1Merged + + @Component + @MergeComponent(AppScope::class) + @SingleIn(AppScope::class) + abstract class ComponentInterface2 : ComponentInterface2Merged + + @Component + @MergeComponent + @SingleIn(AppScope::class) + abstract class ComponentInterface3 : ComponentInterface3Merged + """, + ) { resolver -> + assertThat( + resolver.clazz("software.amazon.test.ComponentInterface1").scope(), + ).isEqualTo( + fqName = "software.amazon.lastmile.kotlin.inject.anvil.AppScope", + annotationFqName = null, + markerFqName = "software.amazon.lastmile.kotlin.inject.anvil.AppScope", + ) + + assertThat( + resolver.clazz("software.amazon.test.ComponentInterface2").scope(), + ).isEqualTo( + fqName = "software.amazon.lastmile.kotlin.inject.anvil.AppScope", + annotationFqName = null, + markerFqName = "software.amazon.lastmile.kotlin.inject.anvil.AppScope", + ) + + assertThat( + resolver.clazz("software.amazon.test.ComponentInterface3").scope(), + ).isEqualTo( + fqName = "software.amazon.lastmile.kotlin.inject.anvil.AppScope", + annotationFqName = "software.amazon.lastmile.kotlin.inject.anvil.SingleIn", + markerFqName = "software.amazon.lastmile.kotlin.inject.anvil.AppScope", + ) + } + } + + @Test + fun `the scope can be parsed from a @ContributesBinding annotation without a parameter name`() { + compileInPlace( + """ + package software.amazon.test + + import software.amazon.lastmile.kotlin.inject.anvil.ContributesBinding + + @ContributesBinding(SingleInAppScope::class) + interface Binding : CharSequence + """, + ) { resolver -> + assertThat(resolver.clazz("software.amazon.test.Binding").scope()).isEqualTo( + fqName = "software.amazon.test.SingleInAppScope", + annotationFqName = "software.amazon.test.SingleInAppScope", + markerFqName = null, + ) + } + } + + @Test + fun `the scope can be parsed from a scope annotation with a marker`() { + compileInPlace( + """ + package software.amazon.test + + import software.amazon.lastmile.kotlin.inject.anvil.AppScope + import software.amazon.lastmile.kotlin.inject.anvil.ContributesTo + import software.amazon.lastmile.kotlin.inject.anvil.ContributesBinding + import software.amazon.lastmile.kotlin.inject.anvil.SingleIn + + @ContributesTo + @SingleIn(AppScope::class) + interface ComponentInterface + + @ContributesBinding + @SingleIn(AppScope::class) + interface Binding : CharSequence + """, + ) { resolver -> + assertThat(resolver.clazz("software.amazon.test.ComponentInterface").scope()).isEqualTo( + fqName = "software.amazon.lastmile.kotlin.inject.anvil.AppScope", + annotationFqName = "software.amazon.lastmile.kotlin.inject.anvil.SingleIn", + markerFqName = "software.amazon.lastmile.kotlin.inject.anvil.AppScope", + ) + + assertThat(resolver.clazz("software.amazon.test.Binding").scope()).isEqualTo( + fqName = "software.amazon.lastmile.kotlin.inject.anvil.AppScope", + annotationFqName = "software.amazon.lastmile.kotlin.inject.anvil.SingleIn", + markerFqName = "software.amazon.lastmile.kotlin.inject.anvil.AppScope", + ) + } + } + + @Test + fun `the scope can be parsed from an annotation without explicit scope annotation`() { + compileInPlace( + """ + package software.amazon.test + + import software.amazon.lastmile.kotlin.inject.anvil.AppScope + import software.amazon.lastmile.kotlin.inject.anvil.ContributesBinding + import software.amazon.lastmile.kotlin.inject.anvil.SingleIn + + @ContributesBinding(AppScope::class) + interface Binding1 : CharSequence + + @ContributesBinding(multibinding = true, scope = AppScope::class) + interface Binding2 : CharSequence + """, + ) { resolver -> + assertThat(resolver.clazz("software.amazon.test.Binding1").scope()).isEqualTo( + fqName = "software.amazon.lastmile.kotlin.inject.anvil.AppScope", + annotationFqName = null, + markerFqName = "software.amazon.lastmile.kotlin.inject.anvil.AppScope", + ) + assertThat(resolver.clazz("software.amazon.test.Binding2").scope()).isEqualTo( + fqName = "software.amazon.lastmile.kotlin.inject.anvil.AppScope", + annotationFqName = null, + markerFqName = "software.amazon.lastmile.kotlin.inject.anvil.AppScope", + ) + } + } + + @Test + fun `the marker scope is used and a different actual scope can be used for kotlin-inject`() { + compileInPlace( + """ + package software.amazon.test + + import software.amazon.lastmile.kotlin.inject.anvil.AppScope + import software.amazon.lastmile.kotlin.inject.anvil.ContributesBinding + + @ContributesBinding(AppScope::class) + @SingleInAppScope + interface Binding : CharSequence + """, + ) { resolver -> + assertThat(resolver.clazz("software.amazon.test.Binding").scope()).isEqualTo( + fqName = "software.amazon.lastmile.kotlin.inject.anvil.AppScope", + annotationFqName = null, + markerFqName = "software.amazon.lastmile.kotlin.inject.anvil.AppScope", + ) + } + } + + @Test + fun `the marker scope is used and a different actual scope can be used for kotlin-inject with a different marker`() { + compileInPlace( + """ + package software.amazon.test + + import software.amazon.lastmile.kotlin.inject.anvil.AppScope + import software.amazon.lastmile.kotlin.inject.anvil.ContributesBinding + import software.amazon.lastmile.kotlin.inject.anvil.SingleIn + + @ContributesBinding(AppScope::class) + @SingleIn(String::class) + interface Binding : CharSequence + """, + ) { resolver -> + assertThat(resolver.clazz("software.amazon.test.Binding").scope()).isEqualTo( + fqName = "software.amazon.lastmile.kotlin.inject.anvil.AppScope", + annotationFqName = null, + markerFqName = "software.amazon.lastmile.kotlin.inject.anvil.AppScope", + ) + } + } + + private fun symbolProcessorProvider( + block: ContextAware.(Resolver) -> Unit, + ): SymbolProcessorProvider = object : SymbolProcessorProvider { + override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { + return object : SymbolProcessor, ContextAware { + override fun process(resolver: Resolver): List { + block(this, resolver) + return emptyList() + } + + override val logger: KSPLogger = environment.logger + } + } + } + + private fun compileInPlace( + @Language("kotlin") vararg sources: String, + block: ContextAware.(Resolver) -> Unit, + ) { + Compilation() + .configureKotlinInjectAnvilProcessor( + symbolProcessorProviders = setOf(symbolProcessorProvider(block)), + ) + .compile(*sources) { + assertThat(exitCode).isEqualTo(OK) + } + } + + private fun Resolver.clazz(name: String) = requireNotNull(getClassDeclarationByName(name)) + + private fun Assert.isEqualTo( + fqName: String, + annotationFqName: String?, + markerFqName: String?, + ) { + all { + prop(MergeScope::fqName).isEqualTo(fqName) + prop(MergeScope::annotationFqName).isEqualTo(annotationFqName) + prop(MergeScope::markerFqName).isEqualTo(markerFqName) + } + } +} diff --git a/compiler/src/test/kotlin/software/amazon/lastmile/kotlin/inject/anvil/processor/ContributesBindingProcessorTest.kt b/compiler/src/test/kotlin/software/amazon/lastmile/kotlin/inject/anvil/processor/ContributesBindingProcessorTest.kt index 3f12319..ee8ed1e 100644 --- a/compiler/src/test/kotlin/software/amazon/lastmile/kotlin/inject/anvil/processor/ContributesBindingProcessorTest.kt +++ b/compiler/src/test/kotlin/software/amazon/lastmile/kotlin/inject/anvil/processor/ContributesBindingProcessorTest.kt @@ -20,7 +20,6 @@ import software.amazon.lastmile.kotlin.inject.anvil.isAnnotatedWith import software.amazon.lastmile.kotlin.inject.anvil.isNotAnnotatedWith import software.amazon.lastmile.kotlin.inject.anvil.origin import software.amazon.lastmile.kotlin.inject.anvil.otherScopeSource -import software.amazon.test.SingleInAppScope class ContributesBindingProcessorTest { @@ -44,7 +43,6 @@ class ContributesBindingProcessorTest { val generatedComponent = impl.generatedComponent assertThat(generatedComponent.packageName).isEqualTo(LOOKUP_PACKAGE) - assertThat(generatedComponent).isAnnotatedWith(SingleInAppScope::class) assertThat(generatedComponent.origin).isEqualTo(impl) val method = generatedComponent.declaredMethods.single() @@ -77,7 +75,6 @@ class ContributesBindingProcessorTest { val generatedComponent = impl.inner.generatedComponent assertThat(generatedComponent.packageName).isEqualTo(LOOKUP_PACKAGE) - assertThat(generatedComponent).isAnnotatedWith(SingleInAppScope::class) assertThat(generatedComponent.origin).isEqualTo(impl.inner) val method = generatedComponent.declaredMethods.single() @@ -185,66 +182,10 @@ class ContributesBindingProcessorTest { val generatedComponent = impl.generatedComponent assertThat(generatedComponent.packageName).isEqualTo(LOOKUP_PACKAGE) - assertThat(generatedComponent).isAnnotatedWith(SingleInAppScope::class) assertThat(generatedComponent.origin).isEqualTo(impl) } } - @Test - fun `it's an error to set the scope explicitly when the class is scoped`() { - compile( - """ - package software.amazon.test - - import software.amazon.lastmile.kotlin.inject.anvil.ContributesBinding - import me.tatarka.inject.annotations.Inject - - interface Base - - @Inject - @SingleInAppScope - @ContributesBinding(scope = SingleInAppScope::class) - class Impl : Base - """, - exitCode = COMPILATION_ERROR, - ) { - assertThat(messages).contains( - "A scope was defined explicitly on the @ContributesBinding annotation " + - "`software.amazon.test.SingleInAppScope` and the class itself is scoped " + - "using `software.amazon.test.SingleInAppScope`. In this case the explicit " + - "scope on the @ContributesBinding annotation can be removed.", - ) - } - } - - @Test - fun `it's an error to set the scope explicitly when the class is scoped - different scopes`() { - compile( - """ - package software.amazon.test - - import software.amazon.lastmile.kotlin.inject.anvil.ContributesBinding - import me.tatarka.inject.annotations.Inject - - interface Base - - @Inject - @OtherScope - @ContributesBinding(scope = SingleInAppScope::class) - class Impl : Base - """, - otherScopeSource, - exitCode = COMPILATION_ERROR, - ) { - assertThat(messages).contains( - "A scope was defined explicitly on the @ContributesBinding annotation " + - "`software.amazon.test.SingleInAppScope` and the class itself is scoped " + - "using `software.amazon.test.OtherScope`. It's not allowed to mix different " + - "scopes.", - ) - } - } - @Test fun `it's an error to not specify the scope for unscoped bindings`() { compile( @@ -264,7 +205,7 @@ class ContributesBindingProcessorTest { ) { assertThat(messages).contains( "Couldn't find scope for Impl. For unscoped objects it is required " + - "to specify the target scope on the @ContributesBinding annotation.", + "to specify the target scope on the annotation.", ) } } @@ -291,7 +232,6 @@ class ContributesBindingProcessorTest { val generatedComponent = impl.generatedComponent assertThat(generatedComponent.packageName).isEqualTo(LOOKUP_PACKAGE) - assertThat(generatedComponent).isAnnotatedWith(SingleInAppScope::class) assertThat(generatedComponent.origin).isEqualTo(impl) with(generatedComponent.declaredMethods.single { it.name == "provideImplBase" }) { @@ -329,7 +269,7 @@ class ContributesBindingProcessorTest { exitCode = COMPILATION_ERROR, ) { assertThat(messages).contains( - "All explicit scopes on @ContributesBinding annotations must be the same.", + "All explicit scopes on annotations must be the same.", ) } } @@ -356,7 +296,7 @@ class ContributesBindingProcessorTest { exitCode = COMPILATION_ERROR, ) { assertThat(messages).contains( - "If one @ContributesBinding annotation has an explicit scope, " + + "If one annotation has an explicit scope, " + "then all annotations must specify an explicit scope.", ) } @@ -408,7 +348,6 @@ class ContributesBindingProcessorTest { val generatedComponent = impl.generatedComponent assertThat(generatedComponent.packageName).isEqualTo(LOOKUP_PACKAGE) - assertThat(generatedComponent).isAnnotatedWith(SingleInAppScope::class) assertThat(generatedComponent.origin).isEqualTo(impl) val method = generatedComponent.declaredMethods.single() @@ -441,7 +380,6 @@ class ContributesBindingProcessorTest { val generatedComponent = impl.generatedComponent assertThat(generatedComponent.packageName).isEqualTo(LOOKUP_PACKAGE) - assertThat(generatedComponent).isAnnotatedWith(SingleInAppScope::class) assertThat(generatedComponent.origin).isEqualTo(impl) assertThat(generatedComponent.declaredMethods).hasSize(2) diff --git a/compiler/src/test/kotlin/software/amazon/lastmile/kotlin/inject/anvil/processor/ContributesSubcomponentFactoryProcessorTest.kt b/compiler/src/test/kotlin/software/amazon/lastmile/kotlin/inject/anvil/processor/ContributesSubcomponentFactoryProcessorTest.kt index 40318a3..b216fd5 100644 --- a/compiler/src/test/kotlin/software/amazon/lastmile/kotlin/inject/anvil/processor/ContributesSubcomponentFactoryProcessorTest.kt +++ b/compiler/src/test/kotlin/software/amazon/lastmile/kotlin/inject/anvil/processor/ContributesSubcomponentFactoryProcessorTest.kt @@ -15,9 +15,7 @@ import org.junit.jupiter.api.Test import software.amazon.lastmile.kotlin.inject.anvil.LOOKUP_PACKAGE import software.amazon.lastmile.kotlin.inject.anvil.compile import software.amazon.lastmile.kotlin.inject.anvil.generatedComponent -import software.amazon.lastmile.kotlin.inject.anvil.isAnnotatedWith import software.amazon.lastmile.kotlin.inject.anvil.origin -import kotlin.reflect.KClass class ContributesSubcomponentFactoryProcessorTest { @@ -45,7 +43,33 @@ class ContributesSubcomponentFactoryProcessorTest { assertThat(generatedComponent.packageName).isEqualTo(LOOKUP_PACKAGE) assertThat(generatedComponent.interfaces).containsExactly(subcomponent.factory) - assertThat(generatedComponent).isAnnotatedWith(parentScope) + assertThat(generatedComponent.origin).isEqualTo(subcomponent.factory) + } + } + + @Test + fun `a component interface is generated in the lookup package for a contributed subcomponent factory using a scope marker`() { + compile( + """ + package software.amazon.test + + import software.amazon.lastmile.kotlin.inject.anvil.AppScope + import software.amazon.lastmile.kotlin.inject.anvil.ContributesSubcomponent + + @ContributesSubcomponent(AppScope::class) + interface SubcomponentInterface { + @ContributesSubcomponent.Factory(String::class) + interface Factory { + fun createSubcomponentInterface(): SubcomponentInterface + } + } + """, + scopesSource, + ) { + val generatedComponent = subcomponent.factory.generatedComponent + + assertThat(generatedComponent.packageName).isEqualTo(LOOKUP_PACKAGE) + assertThat(generatedComponent.interfaces).containsExactly(subcomponent.factory) assertThat(generatedComponent.origin).isEqualTo(subcomponent.factory) } } @@ -302,11 +326,6 @@ class ContributesSubcomponentFactoryProcessorTest { private val JvmCompilationResult.subcomponent: Class<*> get() = classLoader.loadClass("software.amazon.test.SubcomponentInterface") - @Suppress("UNCHECKED_CAST") - private val JvmCompilationResult.parentScope: KClass - get() = classLoader.loadClass("software.amazon.test.ParentScope").kotlin - as KClass - private val Class<*>.factory: Class<*> get() = classes.single { it.simpleName == "Factory" } diff --git a/compiler/src/test/kotlin/software/amazon/lastmile/kotlin/inject/anvil/processor/ContributesSubcomponentProcessorTest.kt b/compiler/src/test/kotlin/software/amazon/lastmile/kotlin/inject/anvil/processor/ContributesSubcomponentProcessorTest.kt index a817184..1182e83 100644 --- a/compiler/src/test/kotlin/software/amazon/lastmile/kotlin/inject/anvil/processor/ContributesSubcomponentProcessorTest.kt +++ b/compiler/src/test/kotlin/software/amazon/lastmile/kotlin/inject/anvil/processor/ContributesSubcomponentProcessorTest.kt @@ -72,6 +72,59 @@ class ContributesSubcomponentProcessorTest { } } + @Test + fun `a contributed subcomponent is generated when the parent is merged using marker scopes`() { + compile( + """ + package software.amazon.test + + import me.tatarka.inject.annotations.Component + import me.tatarka.inject.annotations.Provides + 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.MergeComponent + import software.amazon.lastmile.kotlin.inject.anvil.SingleIn + + @MergeComponent + @Component + @SingleIn(AppScope::class) + interface ComponentInterface : ComponentInterfaceMerged + + @ContributesSubcomponent(String::class) + @SingleIn(String::class) + interface OtherComponent { + @ContributesSubcomponent.Factory(AppScope::class) + interface Parent { + fun otherComponent(): OtherComponent + } + } + + @ContributesTo(String::class) + interface ChildComponent { + @Provides + @SingleIn(String::class) + fun provideString(): String = "abc" + + val string: String + } + """, + ) { + val component = componentInterface.newComponent() + val childComponent = component::class.java.methods + .single { it.name == "otherComponent" } + .invoke(component) + + assertThat(childComponent).isNotNull() + + val string = childComponent::class.java.methods + .single { it.name == "getString" } + .invoke(childComponent) + + assertThat(string).isEqualTo("abc") + } + } + @Test fun `contributions to the child scope from a previous compilation are picked up`() { val previousResult1 = compile(scopesSource) @@ -248,6 +301,40 @@ class ContributesSubcomponentProcessorTest { } } + @Test + fun `a contributed subcomponent can be excluded using marker scopes`() { + compile( + """ + package software.amazon.test + + import me.tatarka.inject.annotations.Component + import software.amazon.lastmile.kotlin.inject.anvil.AppScope + import software.amazon.lastmile.kotlin.inject.anvil.ContributesSubcomponent + import software.amazon.lastmile.kotlin.inject.anvil.MergeComponent + import software.amazon.lastmile.kotlin.inject.anvil.SingleIn + + @MergeComponent(exclude = [OtherComponent::class]) + @Component + @SingleIn(AppScope::class) + interface ComponentInterface : ComponentInterfaceMerged + + @ContributesSubcomponent(String::class) + interface OtherComponent { + @ContributesSubcomponent.Factory(AppScope::class) + interface Parent { + fun otherComponent(): OtherComponent + } + } + """, + ) { + val component = componentInterface.newComponent() + + assertThat( + component::class.java.methods.filter { it.name == "otherComponent" }, + ).isEmpty() + } + } + @Test fun `contributed subcomponents can be chained`() { compile( diff --git a/compiler/src/test/kotlin/software/amazon/lastmile/kotlin/inject/anvil/processor/ContributesToProcessorTest.kt b/compiler/src/test/kotlin/software/amazon/lastmile/kotlin/inject/anvil/processor/ContributesToProcessorTest.kt index e775dd8..97532ac 100644 --- a/compiler/src/test/kotlin/software/amazon/lastmile/kotlin/inject/anvil/processor/ContributesToProcessorTest.kt +++ b/compiler/src/test/kotlin/software/amazon/lastmile/kotlin/inject/anvil/processor/ContributesToProcessorTest.kt @@ -14,9 +14,7 @@ import software.amazon.lastmile.kotlin.inject.anvil.compile import software.amazon.lastmile.kotlin.inject.anvil.componentInterface import software.amazon.lastmile.kotlin.inject.anvil.generatedComponent import software.amazon.lastmile.kotlin.inject.anvil.inner -import software.amazon.lastmile.kotlin.inject.anvil.isAnnotatedWith import software.amazon.lastmile.kotlin.inject.anvil.origin -import software.amazon.test.SingleInAppScope class ContributesToProcessorTest { @@ -37,7 +35,28 @@ class ContributesToProcessorTest { assertThat(generatedComponent.packageName).isEqualTo(LOOKUP_PACKAGE) assertThat(generatedComponent.interfaces).containsExactly(componentInterface) - assertThat(generatedComponent).isAnnotatedWith(SingleInAppScope::class) + assertThat(generatedComponent.origin).isEqualTo(componentInterface) + } + } + + @Test + fun `a component interface is generated in the lookup package for a contributed component interface using a scope marker`() { + compile( + """ + package software.amazon.test + + import software.amazon.lastmile.kotlin.inject.anvil.AppScope + import software.amazon.lastmile.kotlin.inject.anvil.ContributesTo + import software.amazon.lastmile.kotlin.inject.anvil.SingleIn + + @ContributesTo(AppScope::class) + interface ComponentInterface + """, + ) { + val generatedComponent = componentInterface.generatedComponent + + assertThat(generatedComponent.packageName).isEqualTo(LOOKUP_PACKAGE) + assertThat(generatedComponent.interfaces).containsExactly(componentInterface) assertThat(generatedComponent.origin).isEqualTo(componentInterface) } } @@ -61,7 +80,6 @@ class ContributesToProcessorTest { assertThat(generatedComponent.packageName).isEqualTo(LOOKUP_PACKAGE) assertThat(generatedComponent.interfaces).containsExactly(componentInterface.inner) - assertThat(generatedComponent).isAnnotatedWith(SingleInAppScope::class) assertThat(generatedComponent.origin).isEqualTo(componentInterface.inner) } } @@ -115,7 +133,10 @@ class ContributesToProcessorTest { """, exitCode = COMPILATION_ERROR, ) { - assertThat(messages).contains("Couldn't find scope annotation for ComponentInterface.") + assertThat(messages).contains( + "Couldn't find scope for ComponentInterface. For unscoped " + + "objects it is required to specify the target scope on the annotation.", + ) } } } diff --git a/compiler/src/test/kotlin/software/amazon/lastmile/kotlin/inject/anvil/processor/MergeComponentProcessorTest.kt b/compiler/src/test/kotlin/software/amazon/lastmile/kotlin/inject/anvil/processor/MergeComponentProcessorTest.kt index 706dcec..0c1a03f 100644 --- a/compiler/src/test/kotlin/software/amazon/lastmile/kotlin/inject/anvil/processor/MergeComponentProcessorTest.kt +++ b/compiler/src/test/kotlin/software/amazon/lastmile/kotlin/inject/anvil/processor/MergeComponentProcessorTest.kt @@ -69,6 +69,54 @@ class MergeComponentProcessorTest { } } + @Test + fun `component interfaces are merged using marker scopes`() { + compile( + """ + package software.amazon.test + + import me.tatarka.inject.annotations.Component + import me.tatarka.inject.annotations.Inject + import me.tatarka.inject.annotations.Provides + import software.amazon.lastmile.kotlin.inject.anvil.AppScope + import software.amazon.lastmile.kotlin.inject.anvil.ContributesTo + import software.amazon.lastmile.kotlin.inject.anvil.MergeComponent + import software.amazon.lastmile.kotlin.inject.anvil.SingleIn + + interface Base + + @Inject + @SingleIn(AppScope::class) + class Impl : Base { + + @ContributesTo(AppScope::class) + interface Component { + @Provides fun provideImpl(impl: Impl): Base = impl + + val string: String + } + } + + @ContributesTo(AppScope::class) + interface StringComponent { + @Provides fun provideString(): String = "abc" + } + + @Component + @MergeComponent(AppScope::class) + @SingleIn(AppScope::class) + abstract class ComponentInterface : ComponentInterfaceMerged { + abstract val base: Base + } + """, + ) { + assertThat(componentInterface.mergedComponent).isNotNull() + + assertThat(stringComponent.isAssignableFrom(componentInterface)).isTrue() + assertThat(implComponent.isAssignableFrom(componentInterface)).isTrue() + } + } + @Test fun `component interfaces are merged into inner class`() { compile( @@ -155,6 +203,41 @@ class MergeComponentProcessorTest { } } + @Test + fun `contributed bindings are merged using marker scopes`() { + compile( + """ + package software.amazon.test + + import me.tatarka.inject.annotations.Component + import me.tatarka.inject.annotations.Inject + import me.tatarka.inject.annotations.Provides + import software.amazon.lastmile.kotlin.inject.anvil.AppScope + import software.amazon.lastmile.kotlin.inject.anvil.ContributesBinding + import software.amazon.lastmile.kotlin.inject.anvil.MergeComponent + + interface Base + + @Inject + @ContributesBinding(AppScope::class) + class Impl : Base + + @Component + @MergeComponent(AppScope::class) + abstract class ComponentInterface : ComponentInterfaceMerged { + abstract val base: Base + } + """, + ) { + assertThat(componentInterface.mergedComponent).isNotNull() + + assertThat( + classLoader.loadClass("$LOOKUP_PACKAGE.SoftwareAmazonTestImpl") + .isAssignableFrom(componentInterface), + ).isTrue() + } + } + @Test fun `component interfaces from previous compilations are merged`() { val previousCompilation = compile( @@ -230,7 +313,10 @@ class MergeComponentProcessorTest { """, exitCode = COMPILATION_ERROR, ) { - assertThat(messages).contains("Couldn't find scope annotation for ComponentInterface.") + assertThat(messages).contains( + "Couldn't find scope for ComponentInterface. For unscoped " + + "objects it is required to specify the target scope on the annotation.", + ) } } @@ -360,7 +446,7 @@ class MergeComponentProcessorTest { } @Component - @MergeComponent([StringComponent::class]) + @MergeComponent(OtherScope::class, [StringComponent::class]) @OtherScope abstract class ComponentInterface : ComponentInterfaceMerged { abstract val base: Base @@ -410,6 +496,55 @@ class MergeComponentProcessorTest { } } + @Test + fun `using a different kotlin-inject scope with marker scopes is allowed`() { + compile( + """ + package software.amazon.test + + import me.tatarka.inject.annotations.Component + import me.tatarka.inject.annotations.Inject + import me.tatarka.inject.annotations.Provides + import software.amazon.lastmile.kotlin.inject.anvil.AppScope + import software.amazon.lastmile.kotlin.inject.anvil.ContributesTo + import software.amazon.lastmile.kotlin.inject.anvil.MergeComponent + + interface Base + + @Inject + @OtherScope + class Impl : Base { + + @ContributesTo(AppScope::class) + @OtherScope + interface Component { + @Provides fun provideImpl(impl: Impl): Base = impl + } + } + + // Note this is contributed to a different scope. + @ContributesTo + @OtherScope + interface StringComponent { + @Provides fun provideString(): String = "abc" + } + + @Component + @MergeComponent(AppScope::class) + @OtherScope + abstract class ComponentInterface : ComponentInterfaceMerged { + abstract val base: Base + } + """, + otherScopeSource, + ) { + assertThat(componentInterface.mergedComponent).isNotNull() + + assertThat(implComponent.isAssignableFrom(componentInterface)).isTrue() + assertThat(stringComponent.isAssignableFrom(componentInterface)).isFalse() + } + } + private val JvmCompilationResult.stringComponent: Class<*> get() = classLoader.loadClass("software.amazon.test.StringComponent") diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0366708..ae3745e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,7 +12,7 @@ kotlin-compile-testing = "0.4.1" kotlin-hierarchy = "1.1" kotlin-inject = "0.7.1" # This is a snapshot build with the latest fixes. We need that in the sample app. -kotlin-inject-bugfix = "0.7.2-20240710.214910-9" +kotlin-inject-bugfix = "0.7.2-20240911.020938-12" kotlin-poet = "1.17.0" kotlinx-binaryCompatibilityValidator = "0.16.2" ksp = "1.9.24-1.0.20" diff --git a/runtime/api/android/runtime.api b/runtime/api/android/runtime.api index c553634..d748e44 100644 --- a/runtime/api/android/runtime.api +++ b/runtime/api/android/runtime.api @@ -9,16 +9,20 @@ public abstract interface annotation class software/amazon/lastmile/kotlin/injec } public abstract interface annotation class software/amazon/lastmile/kotlin/inject/anvil/ContributesSubcomponent : java/lang/annotation/Annotation { + public abstract fun scope ()Ljava/lang/Class; } public abstract interface annotation class software/amazon/lastmile/kotlin/inject/anvil/ContributesSubcomponent$Factory : java/lang/annotation/Annotation { + public abstract fun scope ()Ljava/lang/Class; } public abstract interface annotation class software/amazon/lastmile/kotlin/inject/anvil/ContributesTo : java/lang/annotation/Annotation { + public abstract fun scope ()Ljava/lang/Class; } public abstract interface annotation class software/amazon/lastmile/kotlin/inject/anvil/MergeComponent : java/lang/annotation/Annotation { public abstract fun exclude ()[Ljava/lang/Class; + public abstract fun scope ()Ljava/lang/Class; } public abstract interface annotation class software/amazon/lastmile/kotlin/inject/anvil/extend/ContributingAnnotation : java/lang/annotation/Annotation { diff --git a/runtime/api/jvm/runtime.api b/runtime/api/jvm/runtime.api index c553634..d748e44 100644 --- a/runtime/api/jvm/runtime.api +++ b/runtime/api/jvm/runtime.api @@ -9,16 +9,20 @@ public abstract interface annotation class software/amazon/lastmile/kotlin/injec } public abstract interface annotation class software/amazon/lastmile/kotlin/inject/anvil/ContributesSubcomponent : java/lang/annotation/Annotation { + public abstract fun scope ()Ljava/lang/Class; } public abstract interface annotation class software/amazon/lastmile/kotlin/inject/anvil/ContributesSubcomponent$Factory : java/lang/annotation/Annotation { + public abstract fun scope ()Ljava/lang/Class; } public abstract interface annotation class software/amazon/lastmile/kotlin/inject/anvil/ContributesTo : java/lang/annotation/Annotation { + public abstract fun scope ()Ljava/lang/Class; } public abstract interface annotation class software/amazon/lastmile/kotlin/inject/anvil/MergeComponent : java/lang/annotation/Annotation { public abstract fun exclude ()[Ljava/lang/Class; + public abstract fun scope ()Ljava/lang/Class; } public abstract interface annotation class software/amazon/lastmile/kotlin/inject/anvil/extend/ContributingAnnotation : java/lang/annotation/Annotation { diff --git a/runtime/api/runtime.klib.api b/runtime/api/runtime.klib.api index e462159..069352b 100644 --- a/runtime/api/runtime.klib.api +++ b/runtime/api/runtime.klib.api @@ -22,31 +22,42 @@ open annotation class software.amazon.lastmile.kotlin.inject.anvil.internal/Subc } open annotation class software.amazon.lastmile.kotlin.inject.anvil/ContributesBinding : kotlin/Annotation { // software.amazon.lastmile.kotlin.inject.anvil/ContributesBinding|null[0] - constructor (kotlin.reflect/KClass =..., kotlin.reflect/KClass<*> =..., kotlin/Boolean =...) // software.amazon.lastmile.kotlin.inject.anvil/ContributesBinding.|(kotlin.reflect.KClass;kotlin.reflect.KClass<*>;kotlin.Boolean){}[0] + constructor (kotlin.reflect/KClass<*> =..., kotlin.reflect/KClass<*> =..., kotlin/Boolean =...) // software.amazon.lastmile.kotlin.inject.anvil/ContributesBinding.|(kotlin.reflect.KClass<*>;kotlin.reflect.KClass<*>;kotlin.Boolean){}[0] final val boundType // software.amazon.lastmile.kotlin.inject.anvil/ContributesBinding.boundType|{}boundType[0] final fun (): kotlin.reflect/KClass<*> // software.amazon.lastmile.kotlin.inject.anvil/ContributesBinding.boundType.|(){}[0] final val multibinding // software.amazon.lastmile.kotlin.inject.anvil/ContributesBinding.multibinding|{}multibinding[0] final fun (): kotlin/Boolean // software.amazon.lastmile.kotlin.inject.anvil/ContributesBinding.multibinding.|(){}[0] final val scope // software.amazon.lastmile.kotlin.inject.anvil/ContributesBinding.scope|{}scope[0] - final fun (): kotlin.reflect/KClass // software.amazon.lastmile.kotlin.inject.anvil/ContributesBinding.scope.|(){}[0] + final fun (): kotlin.reflect/KClass<*> // software.amazon.lastmile.kotlin.inject.anvil/ContributesBinding.scope.|(){}[0] } open annotation class software.amazon.lastmile.kotlin.inject.anvil/ContributesSubcomponent : kotlin/Annotation { // software.amazon.lastmile.kotlin.inject.anvil/ContributesSubcomponent|null[0] - constructor () // software.amazon.lastmile.kotlin.inject.anvil/ContributesSubcomponent.|(){}[0] + constructor (kotlin.reflect/KClass<*> =...) // software.amazon.lastmile.kotlin.inject.anvil/ContributesSubcomponent.|(kotlin.reflect.KClass<*>){}[0] + + final val scope // software.amazon.lastmile.kotlin.inject.anvil/ContributesSubcomponent.scope|{}scope[0] + final fun (): kotlin.reflect/KClass<*> // software.amazon.lastmile.kotlin.inject.anvil/ContributesSubcomponent.scope.|(){}[0] open annotation class Factory : kotlin/Annotation { // software.amazon.lastmile.kotlin.inject.anvil/ContributesSubcomponent.Factory|null[0] - constructor () // software.amazon.lastmile.kotlin.inject.anvil/ContributesSubcomponent.Factory.|(){}[0] + constructor (kotlin.reflect/KClass<*> =...) // software.amazon.lastmile.kotlin.inject.anvil/ContributesSubcomponent.Factory.|(kotlin.reflect.KClass<*>){}[0] + + final val scope // software.amazon.lastmile.kotlin.inject.anvil/ContributesSubcomponent.Factory.scope|{}scope[0] + final fun (): kotlin.reflect/KClass<*> // software.amazon.lastmile.kotlin.inject.anvil/ContributesSubcomponent.Factory.scope.|(){}[0] } } open annotation class software.amazon.lastmile.kotlin.inject.anvil/ContributesTo : kotlin/Annotation { // software.amazon.lastmile.kotlin.inject.anvil/ContributesTo|null[0] - constructor () // software.amazon.lastmile.kotlin.inject.anvil/ContributesTo.|(){}[0] + constructor (kotlin.reflect/KClass<*> =...) // software.amazon.lastmile.kotlin.inject.anvil/ContributesTo.|(kotlin.reflect.KClass<*>){}[0] + + final val scope // software.amazon.lastmile.kotlin.inject.anvil/ContributesTo.scope|{}scope[0] + final fun (): kotlin.reflect/KClass<*> // software.amazon.lastmile.kotlin.inject.anvil/ContributesTo.scope.|(){}[0] } open annotation class software.amazon.lastmile.kotlin.inject.anvil/MergeComponent : kotlin/Annotation { // software.amazon.lastmile.kotlin.inject.anvil/MergeComponent|null[0] - constructor (kotlin/Array> =...) // software.amazon.lastmile.kotlin.inject.anvil/MergeComponent.|(kotlin.Array>){}[0] + constructor (kotlin.reflect/KClass<*> =..., kotlin/Array> =...) // software.amazon.lastmile.kotlin.inject.anvil/MergeComponent.|(kotlin.reflect.KClass<*>;kotlin.Array>){}[0] final val exclude // software.amazon.lastmile.kotlin.inject.anvil/MergeComponent.exclude|{}exclude[0] final fun (): kotlin/Array> // software.amazon.lastmile.kotlin.inject.anvil/MergeComponent.exclude.|(){}[0] + final val scope // software.amazon.lastmile.kotlin.inject.anvil/MergeComponent.scope|{}scope[0] + final fun (): kotlin.reflect/KClass<*> // software.amazon.lastmile.kotlin.inject.anvil/MergeComponent.scope.|(){}[0] } diff --git a/runtime/src/commonMain/kotlin/software/amazon/lastmile/kotlin/inject/anvil/ContributesBinding.kt b/runtime/src/commonMain/kotlin/software/amazon/lastmile/kotlin/inject/anvil/ContributesBinding.kt index ad18f34..1b458cd 100644 --- a/runtime/src/commonMain/kotlin/software/amazon/lastmile/kotlin/inject/anvil/ContributesBinding.kt +++ b/runtime/src/commonMain/kotlin/software/amazon/lastmile/kotlin/inject/anvil/ContributesBinding.kt @@ -82,7 +82,7 @@ public annotation class ContributesBinding( /** * The scope in which to include this contributed binding. */ - val scope: KClass = Annotation::class, + val scope: KClass<*> = Unit::class, /** * The type that this class is bound to. When injecting [boundType] the concrete class will be * this annotated class. diff --git a/runtime/src/commonMain/kotlin/software/amazon/lastmile/kotlin/inject/anvil/ContributesSubcomponent.kt b/runtime/src/commonMain/kotlin/software/amazon/lastmile/kotlin/inject/anvil/ContributesSubcomponent.kt index f3face5..80301b1 100644 --- a/runtime/src/commonMain/kotlin/software/amazon/lastmile/kotlin/inject/anvil/ContributesSubcomponent.kt +++ b/runtime/src/commonMain/kotlin/software/amazon/lastmile/kotlin/inject/anvil/ContributesSubcomponent.kt @@ -1,6 +1,7 @@ package software.amazon.lastmile.kotlin.inject.anvil import kotlin.annotation.AnnotationTarget.CLASS +import kotlin.reflect.KClass /** * Generates a subcomponent when the parent component interface is merged. @@ -102,7 +103,12 @@ import kotlin.annotation.AnnotationTarget.CLASS * ``` */ @Target(CLASS) -public annotation class ContributesSubcomponent { +public annotation class ContributesSubcomponent( + /** + * The scope in which to include this contributed component interface. + */ + val scope: KClass<*> = Unit::class, +) { /** * A factory for the contributed subcomponent. * @@ -112,5 +118,10 @@ public annotation class ContributesSubcomponent { * The factory interface must have a single function with the contributed subcomponent as * return type. Parameters are supported as mentioned in [ContributesSubcomponent]. */ - public annotation class Factory + public annotation class Factory( + /** + * The scope in which to include this contributed component interface. + */ + val scope: KClass<*> = Unit::class, + ) } diff --git a/runtime/src/commonMain/kotlin/software/amazon/lastmile/kotlin/inject/anvil/ContributesTo.kt b/runtime/src/commonMain/kotlin/software/amazon/lastmile/kotlin/inject/anvil/ContributesTo.kt index 6cbfa8a..de6490b 100644 --- a/runtime/src/commonMain/kotlin/software/amazon/lastmile/kotlin/inject/anvil/ContributesTo.kt +++ b/runtime/src/commonMain/kotlin/software/amazon/lastmile/kotlin/inject/anvil/ContributesTo.kt @@ -1,12 +1,18 @@ package software.amazon.lastmile.kotlin.inject.anvil import kotlin.annotation.AnnotationTarget.CLASS +import kotlin.reflect.KClass /** * Marks a component interface to be included in the dependency graph in the given `scope`. * The processor will automatically add the interface as super type to the final component * marked with [MergeComponent]. + * ``` + * @ContributesTo(AppScope::class) + * interface ComponentInterface { .. } + * ``` * + * Or another example where the scope on the component interface is used. * ``` * @ContributesTo * @SingleInAppScope @@ -14,4 +20,9 @@ import kotlin.annotation.AnnotationTarget.CLASS * ``` */ @Target(CLASS) -public annotation class ContributesTo +public annotation class ContributesTo( + /** + * The scope in which to include this contributed component interface. + */ + val scope: KClass<*> = Unit::class, +) diff --git a/runtime/src/commonMain/kotlin/software/amazon/lastmile/kotlin/inject/anvil/MergeComponent.kt b/runtime/src/commonMain/kotlin/software/amazon/lastmile/kotlin/inject/anvil/MergeComponent.kt index 0c52326..7c067e5 100644 --- a/runtime/src/commonMain/kotlin/software/amazon/lastmile/kotlin/inject/anvil/MergeComponent.kt +++ b/runtime/src/commonMain/kotlin/software/amazon/lastmile/kotlin/inject/anvil/MergeComponent.kt @@ -33,6 +33,11 @@ import kotlin.reflect.KClass */ @Target(CLASS) public annotation class MergeComponent( + /** + * The scope in which to include this contributed component interface. + */ + val scope: KClass<*> = Unit::class, + /** * List of component interfaces that are contributed to the same scope, but should be * excluded from the component.