diff --git a/CHANGELOG.md b/CHANGELOG.md index 9300045..11c9225 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ ### Changed +* **BREAKING CHANGE:** Enforce scope parameter on all `@Contributes*` annotations and stop using the kotlin-inject scope implicitly, see #36. + ### Deprecated ### Removed diff --git a/README.md b/README.md index fd85484..48ade9c 100644 --- a/README.md +++ b/README.md @@ -179,61 +179,39 @@ every component in your project. ### Scopes -The plugin builds a connection between contributions and merged components through the scope. -How scopes function with `kotlin-inject` is described in the -[documentation](https://github.com/evant/kotlin-inject#scopes). - -`kotlin-inject` supports scopes with and without parameters. For `kotlin-inject-anvil` we decided -to prefer scope references as parameter to contribute and merge types as the -[`@SingleIn` annotation](runtime-optional/src/commonMain/kotlin/software/amazon/lastmile/kotlin/inject/anvil/SingleIn.kt) -defines it: +The plugin builds a connection between contributions and merged components through the scope +parameters. Scope classes are only markers and have no further meaning besides building a +connection between contributions and merging them. The class `AppScope` from the sample could +look like this: +```kotlin +object AppScope +``` + +Scope classes are independent of the `kotlin-inject` +[scopes](https://github.com/evant/kotlin-inject#scopes). It's still necessary to set a scope for +the `kotlin-inject` components or to make instances a singleton in a scope, e.g. ```kotlin @Inject -@SingleIn(AppScope::class) +@SingleIn(AppScope::class) // scope for kotlin-inject @ContributesBinding(AppScope::class) class RealAuthenticator : Authenticator @Component @MergeComponent(AppScope::class) -@SingleIn(AppScope::class) -interface AppComponent : AppComponentMerged +@SingleIn(AppScope::class) // scope for kotlin-inject +interface AppComponent ``` -The `@SingleIn` annotation needs to be explicitly imported! + +`kotlin-inject-anvil` provides the +[`@SingleIn` scope annotation](runtime-optional/src/commonMain/kotlin/software/amazon/lastmile/kotlin/inject/anvil/SingleIn.kt) +optionally by importing following module. We strongly recommend to use the annotation for +consistency. ```groovy dependencies { commonMainImplementation "software.amazon.lastmile.kotlin.inject.anvil:runtime-optional:$version" } ``` -However, scopes without parameters are also supported, such as: -```kotlin -import me.tatarka.inject.annotations.Scope - -@Scope -annotation class Singleton -``` -Instead of using the `scope` parameter on the `@Contributes*` annotations, you'd add this -annotation on the class itself and `kotlin-anvil-inject` will still build the correct -connections and merge the code: -```kotlin -@ContributesTo -@Singleton -interface AppIdComponent { - @Provides - fun provideAppId(): String = "demo app" -} - -@Inject -@Singleton -@ContributesBinding -class RealAuthenticator : Authenticator - -@Component -@MergeComponent -@Singleton -interface AppComponent : AppComponentMerged -``` - ## Sample A [sample project](sample) for Android and iOS is available. 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 d9e3199..0914ad8 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 @@ -71,31 +71,24 @@ internal interface ContextAware { fun KSClassDeclaration.scope(): MergeScope { return requireNotNull(scopeOrNull(), this) { - "Couldn't find scope annotation for $this." + "Couldn't find scope for $this." } } 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) - } + val annotationsWithScopeParameter = annotations.filter { it.hasScopeParameter() } + .toList() + .ifEmpty { return null } + + return scopeForAnnotationsWithScopeParameters(this, annotationsWithScopeParameter) } - fun isScopeAnnotation(annotation: KSAnnotation): Boolean { - return isScopeAnnotation(annotation.annotationType.resolve()) + fun KSAnnotation.isKotlinInjectScopeAnnotation(): Boolean { + return annotationType.resolve().isKotlinInjectScopeAnnotation() } - private fun isScopeAnnotation(type: KSType): Boolean { - return type.declaration.annotations.any { + private fun KSType.isKotlinInjectScopeAnnotation(): Boolean { + return declaration.annotations.any { it.annotationType.resolve().declaration.requireQualifiedName() == scopeFqName } } @@ -109,50 +102,31 @@ internal interface ContextAware { clazz: KSClassDeclaration, annotations: List, ): MergeScope { - val explicitScopes = annotations.mapNotNull { annotation -> - annotation.scopeParameter(this) + val explicitScopes = annotations.map { annotation -> + annotation.scopeParameter() } - 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 scopes on annotations must be the same." } + previous + } - explicitScopes.scan( - explicitScopes.first().declaration.requireQualifiedName(), - ) { previous, next -> - check(previous == next.declaration.requireQualifiedName(), clazz) { - "All explicit scopes on annotations must be the same." - } - previous - } + return MergeScope(explicitScopes.first()) + } - 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, - ) - } + private fun KSAnnotation.scopeParameter(): KSType { + return requireNotNull(scopeParameterOrNull(), this) { + "Couldn't find a scope parameter." } + } - 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." - } + private fun KSAnnotation.scopeParameterOrNull(): KSType? { + return arguments.firstOrNull { it.name?.asString() == "scope" } + ?.let { it.value as? KSType } } fun KSClassDeclaration.origin(): KSClassDeclaration { @@ -184,6 +158,15 @@ internal interface ContextAware { return getDeclaredFunctions().filter { it.isAbstract } } + fun requireKotlinInjectScope(clazz: KSClassDeclaration): KSAnnotation { + return requireNotNull( + clazz.annotations.firstOrNull { it.isKotlinInjectScopeAnnotation() }, + clazz, + ) { + "A kotlin-inject scope like @SingleIn(Abc::class) is missing." + } + } + fun KSDeclaration.requireContainingFile(): KSFile = requireNotNull(containingFile, this) { "Containing file was null for $this" } 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 index 201771e..c8141c9 100644 --- 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 @@ -1,16 +1,10 @@ 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: + * the merge phase, e.g. * ``` * @ContributesTo(AppScope::class) * interface ContributedComponentInterface @@ -19,139 +13,8 @@ import com.squareup.kotlinpoet.ksp.toClassName * @MergeComponent(AppScope::class) * interface MergedComponent * ``` - * Or the old way: - * ``` - * @ContributesTo - * @Singleton - * interface ContributedComponentInterface - * - * @Component - * @MergeComponent - * @Singleton - * interface MergedComponent - * ``` + * Where `AppScope` would represent the "MergeScope". */ -internal sealed class MergeScope { - /** - * The fully qualified name of the annotation used as scope, e.g. - * ``` - * @ContributesTo - * @Singleton - * 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 = Singleton::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 - * @Singleton - * interface Abc - * ``` - * - * The value is also null, when the `scope` parameter is used and the argument is annotated - * with `@Scope`, e.g. - * ``` - * @ContributesBinding(scope = Singleton::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, - ) - } - } -} +internal data class MergeScope( + val type: KSType, +) 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 f392612..24542d3 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 @@ -4,7 +4,6 @@ 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 @@ -74,12 +73,3 @@ internal fun KSDeclaration.requireQualifiedName(contextAware: ContextAware): Str 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 8893b6d..9470329 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 @@ -38,7 +38,7 @@ import kotlin.reflect.KClass * * @Inject * @SingleIn(AppScope::class) - * @ContributesBinding + * @ContributesBinding(AppScope::class) * class RealAuthenticator : Authenticator * ``` * Will generate: 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 ba05db0..4b1ba7a 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 @@ -112,6 +112,8 @@ internal class ContributesSubcomponentFactoryProcessor( "Factory interfaces must be inner classes of the contributed subcomponent, which " + "need to be annotated with @ContributesSubcomponent." } + + requireKotlinInjectScope(subcomponent) } private fun checkSingleFunction(factory: KSClassDeclaration) { 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 758b910..f8823db 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,6 +13,7 @@ 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 @@ -22,7 +23,6 @@ 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 @@ -117,14 +117,7 @@ internal class ContributesSubcomponentProcessor( factoryInterface: KSClassDeclaration, generatedFactoryInterface: KSClassDeclaration, ): ClassName { - val scope = requireNotNull( - value = subcomponent.annotations - .firstOrNull { isScopeAnnotation(it) } - ?.let { MergeScope(this, it) }, - symbol = subcomponent, - ) { - "A scope like @SingleIn(Abc::class) is missing." - } + val kotlinInjectScope = requireKotlinInjectScope(subcomponent) val function = factoryInterface.factoryFunctions().single() @@ -142,22 +135,16 @@ internal class ContributesSubcomponentProcessor( TypeSpec .classBuilder(finalComponentClassName) .addAnnotation(Component::class) - .apply { - val scopeOnSubcomponentAnnotation = subcomponent.scope() - if (scopeOnSubcomponentAnnotation.markerFqName != null) { - addAnnotation( - AnnotationSpec.builder(MergeComponent::class) - .addMember( - "scope = %T::class", - ClassName.bestGuess(scopeOnSubcomponentAnnotation.fqName), - ) - .build(), + .addAnnotation( + AnnotationSpec + .builder(MergeComponent::class) + .addMember( + "scope = %T::class", + subcomponent.scope().type.toClassName(), ) - } else { - addAnnotation(MergeComponent::class) - } - } - .addAnnotation(scope.toAnnotationSpec()) + .build(), + ) + .addAnnotation(kotlinInjectScope.toAnnotationSpec()) .addOriginAnnotation(subcomponent) .addModifiers(KModifier.ABSTRACT) .primaryConstructor( diff --git a/compiler/src/test/kotlin/software/amazon/lastmile/kotlin/inject/anvil/CommonSourceCode.kt b/compiler/src/test/kotlin/software/amazon/lastmile/kotlin/inject/anvil/CommonSourceCode.kt index 730315f..2daaf14 100644 --- a/compiler/src/test/kotlin/software/amazon/lastmile/kotlin/inject/anvil/CommonSourceCode.kt +++ b/compiler/src/test/kotlin/software/amazon/lastmile/kotlin/inject/anvil/CommonSourceCode.kt @@ -3,7 +3,6 @@ package software.amazon.lastmile.kotlin.inject.anvil import com.tschuchort.compiletesting.JvmCompilationResult -import org.intellij.lang.annotations.Language import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi import software.amazon.lastmile.kotlin.inject.anvil.internal.Origin import java.lang.reflect.Method @@ -58,16 +57,3 @@ internal val Class<*>.propertyAnnotations: Array .filter { Modifier.isStatic(it.modifiers) } .single { it.name.endsWith("\$annotations") } .annotations - -@Language("kotlin") -internal val otherScopeSource = """ - package software.amazon.test - - import me.tatarka.inject.annotations.Scope - - @Scope - annotation class OtherScope - - @Scope - annotation class OtherScope2 -""" diff --git a/compiler/src/test/kotlin/software/amazon/lastmile/kotlin/inject/anvil/KotlinInjectExtensionSymbolProcessorProviderTest.kt b/compiler/src/test/kotlin/software/amazon/lastmile/kotlin/inject/anvil/KotlinInjectExtensionSymbolProcessorProviderTest.kt index 51edb1d..779a160 100644 --- a/compiler/src/test/kotlin/software/amazon/lastmile/kotlin/inject/anvil/KotlinInjectExtensionSymbolProcessorProviderTest.kt +++ b/compiler/src/test/kotlin/software/amazon/lastmile/kotlin/inject/anvil/KotlinInjectExtensionSymbolProcessorProviderTest.kt @@ -26,9 +26,10 @@ class KotlinInjectExtensionSymbolProcessorProviderTest { package software.amazon.test import software.amazon.lastmile.kotlin.inject.anvil.ContributesTo + import software.amazon.lastmile.kotlin.inject.anvil.SingleIn - @ContributesTo - @Singleton + @ContributesTo(Unit::class) + @SingleIn(Unit::class) interface ComponentInterface """, ) 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 index 1e2aa19..7bbd23b 100644 --- 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 @@ -3,10 +3,10 @@ package software.amazon.lastmile.kotlin.inject.anvil import assertk.Assert -import assertk.all +import assertk.assertFailure import assertk.assertThat import assertk.assertions.isEqualTo -import assertk.assertions.prop +import assertk.assertions.messageContains import com.google.devtools.ksp.getClassDeclarationByName import com.google.devtools.ksp.processing.KSPLogger import com.google.devtools.ksp.processing.Resolver @@ -14,6 +14,8 @@ 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 +import com.tschuchort.compiletesting.KotlinCompilation.ExitCode.COMPILATION_ERROR import com.tschuchort.compiletesting.KotlinCompilation.ExitCode.OK import org.intellij.lang.annotations.Language import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi @@ -22,53 +24,29 @@ import org.junit.jupiter.api.Test class MergeScopeParserTest { @Test - fun `the scope can be parsed from a scope annotation with zero args`() { + fun `the scope can be parsed from a @ContributesBinding annotation`() { compileInPlace( """ package software.amazon.test - import software.amazon.lastmile.kotlin.inject.anvil.ContributesTo import software.amazon.lastmile.kotlin.inject.anvil.ContributesBinding - @ContributesTo - @Singleton - interface ComponentInterface - - @ContributesBinding - @Singleton - interface Binding : CharSequence - """, - ) { resolver -> - assertThat(resolver.clazz("software.amazon.test.ComponentInterface").scope()).isEqualTo( - fqName = "software.amazon.test.Singleton", - annotationFqName = "software.amazon.test.Singleton", - markerFqName = null, - ) - assertThat(resolver.clazz("software.amazon.test.Binding").scope()).isEqualTo( - fqName = "software.amazon.test.Singleton", - annotationFqName = "software.amazon.test.Singleton", - markerFqName = null, - ) - } - } + @ContributesBinding(scope = Unit::class) + interface Binding1 : CharSequence - @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(String::class) + interface Binding2 : CharSequence - @ContributesBinding(scope = Singleton::class) - interface Binding : CharSequence + @ContributesBinding(multibinding = true, scope = CharSequence::class) + interface Binding3 : CharSequence """, ) { resolver -> - assertThat(resolver.clazz("software.amazon.test.Binding").scope()).isEqualTo( - fqName = "software.amazon.test.Singleton", - annotationFqName = "software.amazon.test.Singleton", - markerFqName = null, - ) + assertThat(resolver.clazz("software.amazon.test.Binding1").scope()) + .isEqualTo("kotlin.Unit") + assertThat(resolver.clazz("software.amazon.test.Binding2").scope()) + .isEqualTo("kotlin.String") + assertThat(resolver.clazz("software.amazon.test.Binding3").scope()) + .isEqualTo("kotlin.CharSequence") } } @@ -84,24 +62,14 @@ class MergeScopeParserTest { @ContributesTo(AppScope::class) interface ComponentInterface1 - @ContributesTo(Singleton::class) + @ContributesTo(scope = Unit::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.Singleton", - annotationFqName = "software.amazon.test.Singleton", - markerFqName = null, - ) + assertThat(resolver.clazz("software.amazon.test.ComponentInterface1").scope()) + .isEqualTo("software.amazon.lastmile.kotlin.inject.anvil.AppScope") + assertThat(resolver.clazz("software.amazon.test.ComponentInterface2").scope()) + .isEqualTo("kotlin.Unit") } } @@ -111,61 +79,25 @@ class MergeScopeParserTest { """ 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 + import software.amazon.lastmile.kotlin.inject.anvil.SingleIn - @Scope - annotation class ChildScope - - @ContributesSubcomponent(AppScope::class) - interface SubcomponentInterface1 { - @ContributesSubcomponent.Factory(String::class) - interface Factory { - fun createSubcomponentInterface(): SubcomponentInterface1 - } - } - - @ContributesSubcomponent - @Singleton - interface SubcomponentInterface2 { - @ContributesSubcomponent.Factory - @ChildScope + @ContributesSubcomponent(String::class) + @SingleIn(String::class) + interface SubcomponentInterface { + @ContributesSubcomponent.Factory(AppScope::class) interface Factory { - fun createSubcomponentInterface(): SubcomponentInterface2 + fun createSubcomponentInterface(): SubcomponentInterface } } """, ) { resolver -> + assertThat(resolver.clazz("software.amazon.test.SubcomponentInterface").scope()) + .isEqualTo("kotlin.String") 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.Singleton", - annotationFqName = "software.amazon.test.Singleton", - markerFqName = null, - ) - assertThat( - resolver.clazz("software.amazon.test.SubcomponentInterface2.Factory").scope(), - ).isEqualTo( - fqName = "software.amazon.test.ChildScope", - annotationFqName = "software.amazon.test.ChildScope", - markerFqName = null, - ) + resolver.clazz("software.amazon.test.SubcomponentInterface.Factory").scope(), + ).isEqualTo("software.amazon.lastmile.kotlin.inject.anvil.AppScope") } } @@ -190,132 +122,38 @@ class MergeScopeParserTest { abstract class ComponentInterface2 : ComponentInterface2Merged """, ) { 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.ComponentInterface1").scope()) + .isEqualTo("software.amazon.lastmile.kotlin.inject.anvil.AppScope") + assertThat(resolver.clazz("software.amazon.test.ComponentInterface2").scope()) + .isEqualTo("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(Singleton::class) - interface Binding : CharSequence - """, - ) { resolver -> - assertThat(resolver.clazz("software.amazon.test.Binding").scope()).isEqualTo( - fqName = "software.amazon.test.Singleton", - annotationFqName = "software.amazon.test.Singleton", - 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`() { + fun `the scope can be different for kotlin-inject`() { 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.ContributesBinding + + @Scope + annotation class Singleton @ContributesBinding(AppScope::class) @Singleton 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", - ) + assertThat(resolver.clazz("software.amazon.test.Binding").scope()) + .isEqualTo("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`() { + fun `using two different scope parameters is forbidden`() { compileInPlace( """ package software.amazon.test @@ -324,16 +162,26 @@ class MergeScopeParserTest { import software.amazon.lastmile.kotlin.inject.anvil.ContributesBinding import software.amazon.lastmile.kotlin.inject.anvil.SingleIn + interface Interface1 + interface Interface2 + @ContributesBinding(AppScope::class) @SingleIn(String::class) - interface Binding : CharSequence + interface Binding1 : Interface1 + + @ContributesBinding(AppScope::class, boundType = Interface1::class) + @ContributesBinding(String::class, boundType = Interface2::class) + interface Binding2 : Interface1, Interface2 """, + exitCode = COMPILATION_ERROR, ) { 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", - ) + assertFailure { + resolver.clazz("software.amazon.test.Binding1").scope() + }.messageContains("All scopes on annotations must be the same.") + + assertFailure { + resolver.clazz("software.amazon.test.Binding2").scope() + }.messageContains("All scopes on annotations must be the same.") } } @@ -354,6 +202,7 @@ class MergeScopeParserTest { private fun compileInPlace( @Language("kotlin") vararg sources: String, + exitCode: KotlinCompilation.ExitCode = OK, block: ContextAware.(Resolver) -> Unit, ) { Compilation() @@ -361,21 +210,15 @@ class MergeScopeParserTest { symbolProcessorProviders = setOf(symbolProcessorProvider(block)), ) .compile(*sources) { - assertThat(exitCode).isEqualTo(OK) + assertThat(this.exitCode).isEqualTo(exitCode) } } 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) - } + private fun Assert.isEqualTo(scopeFqName: String) { + transform { + requireNotNull(it.type.declaration.qualifiedName).asString() + }.isEqualTo(scopeFqName) } } 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 b2680a2..bcd52d1 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 @@ -19,7 +19,6 @@ import software.amazon.lastmile.kotlin.inject.anvil.inner 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 class ContributesBindingProcessorTest { @@ -35,8 +34,7 @@ class ContributesBindingProcessorTest { interface Base @Inject - @Singleton - @ContributesBinding + @ContributesBinding(Unit::class) class Impl : Base """, ) { @@ -66,8 +64,7 @@ class ContributesBindingProcessorTest { interface Impl { @Inject - @Singleton - @ContributesBinding + @ContributesBinding(Unit::class) class Inner : Base } """, @@ -98,13 +95,11 @@ class ContributesBindingProcessorTest { interface Base2 : Base @Inject - @Singleton - @ContributesBinding(boundType = Base::class) + @ContributesBinding(Unit::class, boundType = Base::class) class Impl : Base2 @Inject - @Singleton - @ContributesBinding + @ContributesBinding(Unit::class) class Impl2 : Base2 """, ) { @@ -125,8 +120,7 @@ class ContributesBindingProcessorTest { import me.tatarka.inject.annotations.Inject @Inject - @Singleton - @ContributesBinding + @ContributesBinding(Unit::class) class Impl """, exitCode = COMPILATION_ERROR, @@ -150,8 +144,7 @@ class ContributesBindingProcessorTest { interface Base2 @Inject - @Singleton - @ContributesBinding + @ContributesBinding(Unit::class) class Impl : Base, Base2 """, exitCode = COMPILATION_ERROR, @@ -163,53 +156,6 @@ class ContributesBindingProcessorTest { } } - @Test - fun `the scope must be added explicitly for unscoped bindings`() { - compile( - """ - package software.amazon.test - - import software.amazon.lastmile.kotlin.inject.anvil.ContributesBinding - import me.tatarka.inject.annotations.Inject - - interface Base - - @Inject - @ContributesBinding(scope = Singleton::class) - class Impl : Base - """, - ) { - val generatedComponent = impl.generatedComponent - - assertThat(generatedComponent.packageName).isEqualTo(LOOKUP_PACKAGE) - assertThat(generatedComponent.origin).isEqualTo(impl) - } - } - - @Test - fun `it's an error to not specify the scope for unscoped bindings`() { - compile( - """ - package software.amazon.test - - import software.amazon.lastmile.kotlin.inject.anvil.ContributesBinding - import me.tatarka.inject.annotations.Inject - - interface Base - - @Inject - @ContributesBinding - class Impl : Base - """, - exitCode = COMPILATION_ERROR, - ) { - assertThat(messages).contains( - "Couldn't find scope for Impl. For unscoped objects it is required " + - "to specify the target scope on the annotation.", - ) - } - } - @Test fun `bindings are repeatable`() { compile( @@ -223,9 +169,8 @@ class ContributesBindingProcessorTest { interface Base2 @Inject - @Singleton - @ContributesBinding(boundType = Base::class) - @ContributesBinding(boundType = Base2::class) + @ContributesBinding(Unit::class, boundType = Base::class) + @ContributesBinding(Unit::class, boundType = Base2::class) class Impl : Base, Base2 """, ) { @@ -261,43 +206,14 @@ class ContributesBindingProcessorTest { interface Base2 @Inject - @ContributesBinding(scope = Singleton::class, boundType = Base::class) - @ContributesBinding(scope = OtherScope::class, boundType = Base2::class) - class Impl : Base, Base2 - """, - otherScopeSource, - exitCode = COMPILATION_ERROR, - ) { - assertThat(messages).contains( - "All explicit scopes on annotations must be the same.", - ) - } - } - - @Test - fun `it's an error to use different scopes for multiple bindings - on class`() { - compile( - """ - package software.amazon.test - - import software.amazon.lastmile.kotlin.inject.anvil.ContributesBinding - import me.tatarka.inject.annotations.Inject - - interface Base - interface Base2 - - @Inject - @OtherScope - @ContributesBinding(scope = Singleton::class, boundType = Base::class) - @ContributesBinding(boundType = Base2::class) + @ContributesBinding(scope = String::class, boundType = Base::class) + @ContributesBinding(scope = Unit::class, boundType = Base2::class) class Impl : Base, Base2 """, - otherScopeSource, exitCode = COMPILATION_ERROR, ) { assertThat(messages).contains( - "If one annotation has an explicit scope, " + - "then all annotations must specify an explicit scope.", + "All scopes on annotations must be the same.", ) } } @@ -310,14 +226,12 @@ class ContributesBindingProcessorTest { import software.amazon.lastmile.kotlin.inject.anvil.ContributesBinding import me.tatarka.inject.annotations.Inject - import me.tatarka.inject.annotations.Scope interface Base @Inject - @Singleton - @ContributesBinding(boundType = Base::class) - @ContributesBinding(boundType = Base::class) + @ContributesBinding(Unit::class, boundType = Base::class) + @ContributesBinding(Unit::class, boundType = Base::class) class Impl : Base, Base2 """, exitCode = COMPILATION_ERROR, @@ -340,8 +254,7 @@ class ContributesBindingProcessorTest { interface Base @Inject - @Singleton - @ContributesBinding(multibinding = true) + @ContributesBinding(Unit::class, multibinding = true) class Impl : Base """, ) { @@ -371,9 +284,8 @@ class ContributesBindingProcessorTest { interface Base @Inject - @Singleton - @ContributesBinding(multibinding = false) - @ContributesBinding(multibinding = true) + @ContributesBinding(Unit::class, multibinding = false) + @ContributesBinding(Unit::class, multibinding = true) class Impl : Base """, ) { 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 b216fd5..0b65d53 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 @@ -9,7 +9,6 @@ import assertk.assertions.isEqualTo import assertk.assertions.isNotNull import com.tschuchort.compiletesting.JvmCompilationResult import com.tschuchort.compiletesting.KotlinCompilation.ExitCode.COMPILATION_ERROR -import org.intellij.lang.annotations.Language import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi import org.junit.jupiter.api.Test import software.amazon.lastmile.kotlin.inject.anvil.LOOKUP_PACKAGE @@ -26,37 +25,10 @@ class ContributesSubcomponentFactoryProcessorTest { package software.amazon.test import software.amazon.lastmile.kotlin.inject.anvil.ContributesSubcomponent + import software.amazon.lastmile.kotlin.inject.anvil.SingleIn - @ContributesSubcomponent - @ChildScope - interface SubcomponentInterface { - @ContributesSubcomponent.Factory - @ParentScope - 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) - } - } - - @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) + @ContributesSubcomponent(Unit::class) + @SingleIn(Unit::class) interface SubcomponentInterface { @ContributesSubcomponent.Factory(String::class) interface Factory { @@ -64,7 +36,6 @@ class ContributesSubcomponentFactoryProcessorTest { } } """, - scopesSource, ) { val generatedComponent = subcomponent.factory.generatedComponent @@ -81,19 +52,18 @@ class ContributesSubcomponentFactoryProcessorTest { package software.amazon.test import software.amazon.lastmile.kotlin.inject.anvil.ContributesSubcomponent + import software.amazon.lastmile.kotlin.inject.anvil.SingleIn - @ContributesSubcomponent - @ChildScope + @ContributesSubcomponent(String::class) + @SingleIn(String::class) interface SubcomponentInterface { - @ContributesSubcomponent.Factory - @ParentScope + @ContributesSubcomponent.Factory(Unit::class) interface Factory { fun createSubcomponentInterface(): SubcomponentInterface fun createSubcomponentInterface2(): SubcomponentInterface } } """, - scopesSource, exitCode = COMPILATION_ERROR, ) { assertThat(messages).contains( @@ -110,12 +80,12 @@ class ContributesSubcomponentFactoryProcessorTest { package software.amazon.test import software.amazon.lastmile.kotlin.inject.anvil.ContributesSubcomponent + import software.amazon.lastmile.kotlin.inject.anvil.SingleIn - @ContributesSubcomponent - @ChildScope + @ContributesSubcomponent(String::class) + @SingleIn(String::class) interface SubcomponentInterface { - @ContributesSubcomponent.Factory - @ParentScope + @ContributesSubcomponent.Factory(Unit::class) interface Factory { fun createSubcomponentInterface(): SubcomponentInterface fun createSubcomponentInterface2(): SubcomponentInterface = @@ -125,7 +95,6 @@ class ContributesSubcomponentFactoryProcessorTest { } } """, - scopesSource, ) { assertThat(subcomponent.factory.generatedComponent).isNotNull() } @@ -138,16 +107,15 @@ class ContributesSubcomponentFactoryProcessorTest { package software.amazon.test import software.amazon.lastmile.kotlin.inject.anvil.ContributesSubcomponent + import software.amazon.lastmile.kotlin.inject.anvil.SingleIn - @ContributesSubcomponent - @ChildScope + @ContributesSubcomponent(String::class) + @SingleIn(String::class) interface SubcomponentInterface { - @ContributesSubcomponent.Factory - @ParentScope + @ContributesSubcomponent.Factory(Unit::class) interface Factory } """, - scopesSource, exitCode = COMPILATION_ERROR, ) { assertThat(messages).contains( @@ -164,18 +132,17 @@ class ContributesSubcomponentFactoryProcessorTest { package software.amazon.test import software.amazon.lastmile.kotlin.inject.anvil.ContributesSubcomponent + import software.amazon.lastmile.kotlin.inject.anvil.SingleIn - @ContributesSubcomponent - @ChildScope + @ContributesSubcomponent(String::class) + @SingleIn(String::class) interface SubcomponentInterface { - @ContributesSubcomponent.Factory - @ParentScope + @ContributesSubcomponent.Factory(Unit::class) interface Factory { fun createSubcomponentInterface(): Any } } """, - scopesSource, exitCode = COMPILATION_ERROR, ) { assertThat(messages).contains( @@ -192,18 +159,17 @@ class ContributesSubcomponentFactoryProcessorTest { package software.amazon.test import software.amazon.lastmile.kotlin.inject.anvil.ContributesSubcomponent + import software.amazon.lastmile.kotlin.inject.anvil.SingleIn - @ContributesSubcomponent - @ChildScope + @ContributesSubcomponent(String::class) + @SingleIn(String::class) interface SubcomponentInterface { - @ContributesSubcomponent.Factory - @ParentScope + @ContributesSubcomponent.Factory(Unit::class) interface Factory { fun createSubcomponentInterface() } } """, - scopesSource, exitCode = COMPILATION_ERROR, ) { assertThat(messages).contains( @@ -221,16 +187,13 @@ class ContributesSubcomponentFactoryProcessorTest { import software.amazon.lastmile.kotlin.inject.anvil.ContributesSubcomponent - @ChildScope interface SubcomponentInterface { - @ContributesSubcomponent.Factory - @ParentScope + @ContributesSubcomponent.Factory(Unit::class) interface Factory { fun createSubcomponentInterface(): SubcomponentInterface } } """, - scopesSource, exitCode = COMPILATION_ERROR, ) { assertThat(messages).contains( @@ -248,13 +211,11 @@ class ContributesSubcomponentFactoryProcessorTest { import software.amazon.lastmile.kotlin.inject.anvil.ContributesSubcomponent - @ContributesSubcomponent.Factory - @ParentScope + @ContributesSubcomponent.Factory(String::class) interface Factory { fun createSubcomponentInterface(): Factory } """, - scopesSource, exitCode = COMPILATION_ERROR, ) { assertThat(messages).contains( @@ -271,16 +232,13 @@ class ContributesSubcomponentFactoryProcessorTest { import software.amazon.lastmile.kotlin.inject.anvil.ContributesSubcomponent - @ContributesSubcomponent - @ChildScope + @ContributesSubcomponent(String::class) interface SubcomponentInterface { - @ParentScope interface Factory { fun createSubcomponentInterface(): SubcomponentInterface } } """, - scopesSource, exitCode = COMPILATION_ERROR, ) { assertThat(messages).contains( @@ -297,23 +255,21 @@ class ContributesSubcomponentFactoryProcessorTest { package software.amazon.test import software.amazon.lastmile.kotlin.inject.anvil.ContributesSubcomponent + import software.amazon.lastmile.kotlin.inject.anvil.SingleIn - @ContributesSubcomponent - @ChildScope + @ContributesSubcomponent(String::class) + @SingleIn(String::class) interface SubcomponentInterface { - @ParentScope - @ContributesSubcomponent.Factory + @ContributesSubcomponent.Factory(Unit::class) interface Factory1 { fun createSubcomponentInterface1(): SubcomponentInterface } - @ParentScope - @ContributesSubcomponent.Factory + @ContributesSubcomponent.Factory(Unit::class) interface Factory2 { fun createSubcomponentInterface2(): SubcomponentInterface } } """, - scopesSource, exitCode = COMPILATION_ERROR, ) { assertThat(messages).contains( @@ -323,22 +279,33 @@ class ContributesSubcomponentFactoryProcessorTest { } } + @Test + fun `a contributed subcomponent must have a kotlin-inject scope`() { + compile( + """ + package software.amazon.test + + import software.amazon.lastmile.kotlin.inject.anvil.ContributesSubcomponent + + @ContributesSubcomponent(String::class) + interface SubcomponentInterface { + @ContributesSubcomponent.Factory(Unit::class) + interface Factory { + fun createSubcomponentInterface1(): SubcomponentInterface + } + } + """, + exitCode = COMPILATION_ERROR, + ) { + assertThat(messages).contains( + "A kotlin-inject scope like @SingleIn(Abc::class) is missing.", + ) + } + } + private val JvmCompilationResult.subcomponent: Class<*> get() = classLoader.loadClass("software.amazon.test.SubcomponentInterface") private val Class<*>.factory: Class<*> get() = classes.single { it.simpleName == "Factory" } - - @Language("kotlin") - internal val scopesSource = """ - package software.amazon.test - - import me.tatarka.inject.annotations.Scope - - @Scope - annotation class ParentScope - - @Scope - annotation class ChildScope - """ } 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 3e6a561..ab9e899 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 @@ -21,6 +21,7 @@ import software.amazon.lastmile.kotlin.inject.anvil.compile import software.amazon.lastmile.kotlin.inject.anvil.componentInterface import software.amazon.lastmile.kotlin.inject.anvil.newComponent import software.amazon.lastmile.kotlin.inject.anvil.origin +import kotlin.reflect.KClass class ContributesSubcomponentProcessorTest { @@ -30,29 +31,29 @@ class ContributesSubcomponentProcessorTest { """ 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.MergeComponent + import software.amazon.lastmile.kotlin.inject.anvil.SingleIn import me.tatarka.inject.annotations.Component import me.tatarka.inject.annotations.Provides - @MergeComponent + @MergeComponent(AppScope::class) @Component - @ParentScope + @SingleIn(AppScope::class) interface ComponentInterface : ComponentInterfaceMerged - @ContributesSubcomponent - @ChildScope + @ContributesSubcomponent(LoggedInScope::class) + @SingleIn(LoggedInScope::class) interface OtherComponent { - @ContributesSubcomponent.Factory - @ParentScope + @ContributesSubcomponent.Factory(AppScope::class) interface Parent { fun otherComponent(): OtherComponent } } - @ContributesTo - @ChildScope + @ContributesTo(LoggedInScope::class) interface ChildComponent { @Provides fun provideString(): String = "abc" @@ -77,75 +78,10 @@ class ContributesSubcomponentProcessorTest { val generatedClass = otherComponent.generatedSubcomponent assertThat(generatedClass.getAnnotation(Component::class.java)).isNotNull() assertThat(generatedClass.getAnnotation(MergeComponent::class.java).scope) - .isEqualTo(Unit::class) - - @Suppress("UNCHECKED_CAST") - val childScopeClass = classLoader - .loadClass("software.amazon.test.ChildScope") as Class - - assertThat(generatedClass.getAnnotation(childScopeClass)).isNotNull() - assertThat(generatedClass.origin).isEqualTo(otherComponent) - assertThat(generatedClass.origin).isEqualTo(otherComponent) - } - } - - @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 - - @Component - @MergeComponent(AppScope::class) - @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) + .isEqualTo(loggedInScope) - assertThat(childComponent).isNotNull() - - val string = childComponent::class.java.methods - .single { it.name == "getString" } - .invoke(childComponent) - - assertThat(string).isEqualTo("abc") - - val generatedClass = otherComponent.generatedSubcomponent - assertThat(generatedClass.getAnnotation(Component::class.java)).isNotNull() - assertThat(generatedClass.getAnnotation(MergeComponent::class.java).scope) - .isEqualTo(String::class) assertThat(generatedClass.getAnnotation(SingleIn::class.java).scope) - .isEqualTo(String::class) + .isEqualTo(loggedInScope) assertThat(generatedClass.origin).isEqualTo(otherComponent) } } @@ -161,8 +97,7 @@ class ContributesSubcomponentProcessorTest { import software.amazon.lastmile.kotlin.inject.anvil.ContributesTo import me.tatarka.inject.annotations.Provides - @ContributesTo - @ChildScope + @ContributesTo(LoggedInScope::class) interface ChildComponent { @Provides fun provideString(): String = "abc" @@ -176,13 +111,14 @@ class ContributesSubcomponentProcessorTest { """ 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.SingleIn - @ContributesSubcomponent - @ChildScope + @ContributesSubcomponent(LoggedInScope::class) + @SingleIn(LoggedInScope::class) interface OtherComponent { - @ContributesSubcomponent.Factory - @ParentScope + @ContributesSubcomponent.Factory(AppScope::class) interface Parent { fun otherComponent(): OtherComponent } @@ -202,12 +138,14 @@ class ContributesSubcomponentProcessorTest { """ package software.amazon.test + import software.amazon.lastmile.kotlin.inject.anvil.AppScope import software.amazon.lastmile.kotlin.inject.anvil.MergeComponent + import software.amazon.lastmile.kotlin.inject.anvil.SingleIn import me.tatarka.inject.annotations.Component - @MergeComponent + @MergeComponent(AppScope::class) @Component - @ParentScope + @SingleIn(AppScope::class) interface ComponentInterface : ComponentInterfaceMerged """, ) @@ -235,34 +173,34 @@ class ContributesSubcomponentProcessorTest { """ 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.MergeComponent + import software.amazon.lastmile.kotlin.inject.anvil.SingleIn import me.tatarka.inject.annotations.Component import me.tatarka.inject.annotations.Provides - @MergeComponent @Component - @ParentScope + @MergeComponent(AppScope::class) + @SingleIn(AppScope::class) interface ComponentInterface : ComponentInterfaceMerged - @MergeComponent @Component - @ParentScope + @MergeComponent(AppScope::class) + @SingleIn(AppScope::class) interface ComponentInterface2 : ComponentInterface2Merged - @ContributesSubcomponent - @ChildScope + @ContributesSubcomponent(LoggedInScope::class) + @SingleIn(LoggedInScope::class) interface OtherComponent { - @ContributesSubcomponent.Factory - @ParentScope + @ContributesSubcomponent.Factory(AppScope::class) interface Parent { fun otherComponent(): OtherComponent } } - @ContributesTo - @ChildScope + @ContributesTo(LoggedInScope::class) interface ChildComponent { @Provides fun provideString(): String = "abc" @@ -297,60 +235,26 @@ class ContributesSubcomponentProcessorTest { """ package software.amazon.test - import software.amazon.lastmile.kotlin.inject.anvil.ContributesSubcomponent - import software.amazon.lastmile.kotlin.inject.anvil.MergeComponent - import me.tatarka.inject.annotations.Component - - @MergeComponent(exclude = [OtherComponent::class]) - @Component - @ParentScope - interface ComponentInterface : ComponentInterfaceMerged - - @ContributesSubcomponent - @ChildScope - interface OtherComponent { - @ContributesSubcomponent.Factory - @ParentScope - interface Parent { - fun otherComponent(): OtherComponent - } - } - """, - scopesSource, - ) { - val component = componentInterface.newComponent() - - assertThat( - component::class.java.methods.filter { it.name == "otherComponent" }, - ).isEmpty() - } - } - - @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 + import me.tatarka.inject.annotations.Component @Component - @MergeComponent(AppScope::class ,exclude = [OtherComponent::class]) - @SingleIn(AppScope::class) + @MergeComponent(Unit::class, exclude = [OtherComponent::class]) + @SingleIn(Unit::class) interface ComponentInterface : ComponentInterfaceMerged @ContributesSubcomponent(String::class) + @SingleIn(String::class) interface OtherComponent { - @ContributesSubcomponent.Factory(AppScope::class) + @ContributesSubcomponent.Factory(Unit::class) interface Parent { fun otherComponent(): OtherComponent } } """, + scopesSource, ) { val component = componentInterface.newComponent() @@ -366,39 +270,38 @@ class ContributesSubcomponentProcessorTest { """ 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.MergeComponent + import software.amazon.lastmile.kotlin.inject.anvil.SingleIn import me.tatarka.inject.annotations.Component import me.tatarka.inject.annotations.Provides - @MergeComponent @Component - @ParentScope + @MergeComponent(AppScope::class) + @SingleIn(AppScope::class) interface ComponentInterface : ComponentInterfaceMerged - @ContributesSubcomponent - @ChildScope + @ContributesSubcomponent(LoggedInScope::class) + @SingleIn(LoggedInScope::class) interface ComponentInterface2 { - @ContributesSubcomponent.Factory - @ParentScope + @ContributesSubcomponent.Factory(AppScope::class) interface Parent { fun componentInterface2(): ComponentInterface2 } } - @ContributesSubcomponent - @GrandChildScope + @ContributesSubcomponent(Unit::class) + @SingleIn(Unit::class) interface OtherComponent { - @ContributesSubcomponent.Factory - @ChildScope + @ContributesSubcomponent.Factory(LoggedInScope::class) interface Parent { fun otherComponent(): OtherComponent } } - @ContributesTo - @GrandChildScope + @ContributesTo(Unit::class) interface ChildComponent { @Provides fun provideString(): String = "abc" @@ -434,29 +337,29 @@ class ContributesSubcomponentProcessorTest { """ 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.MergeComponent + import software.amazon.lastmile.kotlin.inject.anvil.SingleIn import me.tatarka.inject.annotations.Component import me.tatarka.inject.annotations.Provides - @MergeComponent @Component - @ParentScope + @MergeComponent(AppScope::class) + @SingleIn(AppScope::class) interface ComponentInterface : ComponentInterfaceMerged - @ContributesSubcomponent - @ChildScope + @ContributesSubcomponent(LoggedInScope::class) + @SingleIn(LoggedInScope::class) interface OtherComponent { - @ContributesSubcomponent.Factory - @ParentScope + @ContributesSubcomponent.Factory(AppScope::class) interface Parent { fun otherComponent(stringArg: String, intArg: Int): OtherComponent } } - @ContributesTo - @ChildScope + @ContributesTo(LoggedInScope::class) interface ChildComponent { val string: String val int: Int @@ -492,14 +395,14 @@ class ContributesSubcomponentProcessorTest { import software.amazon.lastmile.kotlin.inject.anvil.ContributesSubcomponent import software.amazon.lastmile.kotlin.inject.anvil.ContributesTo + import software.amazon.lastmile.kotlin.inject.anvil.SingleIn import me.tatarka.inject.annotations.Component import me.tatarka.inject.annotations.Provides - @ContributesSubcomponent - @ChildScope + @ContributesSubcomponent(LoggedInScope::class) + @SingleIn(LoggedInScope::class) abstract class OtherComponent { - @ContributesSubcomponent.Factory - @ParentScope + @ContributesSubcomponent.Factory(Unit::class) interface Parent { fun otherComponent(stringArg: String, intArg: Int): OtherComponent } @@ -522,22 +425,16 @@ class ContributesSubcomponentProcessorTest { private val JvmCompilationResult.otherComponent: Class<*> get() = classLoader.loadClass("software.amazon.test.OtherComponent") + private val JvmCompilationResult.loggedInScope: KClass<*> + get() = classLoader.loadClass("software.amazon.test.LoggedInScope").kotlin + private val Class<*>.generatedSubcomponent: Class<*> get() = classLoader.loadClass("${canonicalName}FinalComponentInterface") @Language("kotlin") internal val scopesSource = """ package software.amazon.test - - import me.tatarka.inject.annotations.Scope - - @Scope - annotation class ParentScope - - @Scope - annotation class ChildScope - @Scope - annotation class GrandChildScope + object LoggedInScope """ } 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 2ccc3cc..9982818 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 @@ -26,30 +26,7 @@ class ContributesToProcessorTest { import software.amazon.lastmile.kotlin.inject.anvil.ContributesTo - @ContributesTo - @Singleton - interface ComponentInterface - """, - ) { - val generatedComponent = componentInterface.generatedComponent - - assertThat(generatedComponent.packageName).isEqualTo(LOOKUP_PACKAGE) - assertThat(generatedComponent.interfaces).containsExactly(componentInterface) - 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) + @ContributesTo(Unit::class) interface ComponentInterface """, ) { @@ -70,8 +47,7 @@ class ContributesToProcessorTest { import software.amazon.lastmile.kotlin.inject.anvil.ContributesTo interface ComponentInterface { - @ContributesTo - @Singleton + @ContributesTo(Unit::class) interface Inner } """, @@ -92,8 +68,7 @@ class ContributesToProcessorTest { import software.amazon.lastmile.kotlin.inject.anvil.ContributesTo - @ContributesTo - @Singleton + @ContributesTo(Unit::class) private interface ComponentInterface """, exitCode = COMPILATION_ERROR, @@ -110,8 +85,7 @@ class ContributesToProcessorTest { import software.amazon.lastmile.kotlin.inject.anvil.ContributesTo - @ContributesTo - @Singleton + @ContributesTo(Unit::class) abstract class ComponentInterface """, exitCode = COMPILATION_ERROR, @@ -119,24 +93,4 @@ class ContributesToProcessorTest { assertThat(messages).contains("Only interfaces can be contributed.") } } - - @Test - fun `a scope must be present`() { - compile( - """ - package software.amazon.test - - import software.amazon.lastmile.kotlin.inject.anvil.ContributesTo - - @ContributesTo - interface ComponentInterface - """, - exitCode = COMPILATION_ERROR, - ) { - 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 15dea53..5aca6ef 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 @@ -16,61 +16,11 @@ import software.amazon.lastmile.kotlin.inject.anvil.compile import software.amazon.lastmile.kotlin.inject.anvil.componentInterface import software.amazon.lastmile.kotlin.inject.anvil.inner import software.amazon.lastmile.kotlin.inject.anvil.mergedComponent -import software.amazon.lastmile.kotlin.inject.anvil.otherScopeSource class MergeComponentProcessorTest { @Test fun `component interfaces are merged`() { - 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.ContributesTo - import software.amazon.lastmile.kotlin.inject.anvil.MergeComponent - - interface Base - - @Inject - @OtherScope - class Impl : Base { - - @ContributesTo - @OtherScope - interface Component { - @Provides fun provideImpl(impl: Impl): Base = impl - - val string: String - } - } - - @ContributesTo - @OtherScope - interface StringComponent { - @Provides fun provideString(): String = "abc" - } - - @Component - @MergeComponent - @OtherScope - abstract class ComponentInterface : ComponentInterfaceMerged { - abstract val base: Base - } - """, - otherScopeSource, - ) { - assertThat(componentInterface.mergedComponent).isNotNull() - - assertThat(stringComponent.isAssignableFrom(componentInterface)).isTrue() - assertThat(implComponent.isAssignableFrom(componentInterface)).isTrue() - } - } - - @Test - fun `component interfaces are merged using marker scopes`() { compile( """ package software.amazon.test @@ -126,17 +76,18 @@ class MergeComponentProcessorTest { 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 - @OtherScope + @SingleIn(AppScope::class) class Impl : Base { - @ContributesTo - @OtherScope + @ContributesTo(AppScope::class) interface Component { @Provides fun provideImpl(impl: Impl): Base = impl @@ -144,20 +95,18 @@ class MergeComponentProcessorTest { } } - @ContributesTo - @OtherScope + @ContributesTo(AppScope::class) interface StringComponent { @Provides fun provideString(): String = "abc" } interface ComponentInterface { @Component - @MergeComponent - @OtherScope + @MergeComponent(AppScope::class) + @SingleIn(AppScope::class) interface Inner : ComponentInterfaceInnerMerged } """, - otherScopeSource, ) { assertThat(componentInterface.inner.mergedComponent).isNotNull() @@ -168,43 +117,6 @@ class MergeComponentProcessorTest { @Test fun `contributed bindings are merged`() { - 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.ContributesBinding - import software.amazon.lastmile.kotlin.inject.anvil.MergeComponent - - interface Base - - @Inject - @OtherScope - @ContributesBinding - class Impl : Base - - @Component - @MergeComponent - @OtherScope - abstract class ComponentInterface : ComponentInterfaceMerged { - abstract val base: Base - } - """, - otherScopeSource, - ) { - assertThat(componentInterface.mergedComponent).isNotNull() - - assertThat( - classLoader.loadClass("$LOOKUP_PACKAGE.SoftwareAmazonTestImpl") - .isAssignableFrom(componentInterface), - ).isTrue() - } - } - - @Test - fun `contributed bindings are merged using marker scopes`() { compile( """ package software.amazon.test @@ -215,6 +127,7 @@ class MergeComponentProcessorTest { import software.amazon.lastmile.kotlin.inject.anvil.AppScope import software.amazon.lastmile.kotlin.inject.anvil.ContributesBinding import software.amazon.lastmile.kotlin.inject.anvil.MergeComponent + import software.amazon.lastmile.kotlin.inject.anvil.SingleIn interface Base @@ -224,6 +137,7 @@ class MergeComponentProcessorTest { @Component @MergeComponent(AppScope::class) + @SingleIn(AppScope::class) abstract class ComponentInterface : ComponentInterfaceMerged { abstract val base: Base } @@ -246,16 +160,17 @@ class MergeComponentProcessorTest { 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.SingleIn interface Base @Inject - @OtherScope + @SingleIn(AppScope::class) class Impl : Base { - @ContributesTo - @OtherScope + @ContributesTo(AppScope::class) interface Component { @Provides fun provideImpl(impl: Impl): Base = impl @@ -263,7 +178,6 @@ class MergeComponentProcessorTest { } } """, - otherScopeSource, ) compile( @@ -273,18 +187,19 @@ class MergeComponentProcessorTest { 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 - @ContributesTo - @OtherScope + @ContributesTo(AppScope::class) interface StringComponent { @Provides fun provideString(): String = "abc" } @Component - @MergeComponent - @OtherScope + @MergeComponent(AppScope::class) + @SingleIn(AppScope::class) abstract class ComponentInterface : ComponentInterfaceMerged { abstract val base: Base } @@ -298,28 +213,6 @@ class MergeComponentProcessorTest { } } - @Test - fun `a scope must be present`() { - compile( - """ - package software.amazon.test - - import me.tatarka.inject.annotations.Component - import software.amazon.lastmile.kotlin.inject.anvil.MergeComponent - - @Component - @MergeComponent - abstract class ComponentInterface : ComponentInterfaceMerged - """, - exitCode = COMPILATION_ERROR, - ) { - assertThat(messages).contains( - "Couldn't find scope for ComponentInterface. For unscoped " + - "objects it is required to specify the target scope on the annotation.", - ) - } - } - @Test fun `component interfaces with a different scope are not merged`() { compile( @@ -329,36 +222,35 @@ class MergeComponentProcessorTest { 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 - @OtherScope + @SingleIn(AppScope::class) class Impl : Base { - @ContributesTo - @OtherScope + @ContributesTo(AppScope::class) interface Component { @Provides fun provideImpl(impl: Impl): Base = impl } } - @ContributesTo - @OtherScope2 + @ContributesTo(Unit::class) interface StringComponent { @Provides fun provideString(): String = "abc" } @Component - @MergeComponent - @OtherScope + @MergeComponent(AppScope::class) + @SingleIn(AppScope::class) abstract class ComponentInterface : ComponentInterfaceMerged { abstract val base: Base } """, - otherScopeSource, ) { assertThat(componentInterface.mergedComponent).isNotNull() @@ -376,36 +268,35 @@ class MergeComponentProcessorTest { 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 - @OtherScope + @SingleIn(AppScope::class) class Impl : Base { - @ContributesTo - @OtherScope + @ContributesTo(AppScope::class) interface Component { @Provides fun provideImpl(impl: Impl): Base = impl } } - @ContributesTo - @OtherScope + @ContributesTo(AppScope::class) interface StringComponent { val string: String } @Component - @MergeComponent(exclude = [StringComponent::class]) - @OtherScope + @MergeComponent(AppScope::class, exclude = [StringComponent::class]) + @SingleIn(AppScope::class) abstract class ComponentInterface : ComponentInterfaceMerged { abstract val base: Base } """, - otherScopeSource, ) { assertThat(componentInterface.mergedComponent).isNotNull() @@ -423,36 +314,35 @@ class MergeComponentProcessorTest { 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 - @OtherScope + @SingleIn(AppScope::class) class Impl : Base { - @ContributesTo - @OtherScope + @ContributesTo(AppScope::class) interface Component { @Provides fun provideImpl(impl: Impl): Base = impl } } - @ContributesTo - @OtherScope + @ContributesTo(AppScope::class) interface StringComponent { val string: String } @Component - @MergeComponent(OtherScope::class, [StringComponent::class]) - @OtherScope + @MergeComponent(AppScope::class, [StringComponent::class]) + @SingleIn(AppScope::class) abstract class ComponentInterface : ComponentInterfaceMerged { abstract val base: Base } """, - otherScopeSource, ) { assertThat(componentInterface.mergedComponent).isNotNull() @@ -470,19 +360,21 @@ class MergeComponentProcessorTest { 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 + import software.amazon.lastmile.kotlin.inject.anvil.SingleIn interface Base @Inject - @Singleton - @ContributesBinding + @SingleIn(AppScope::class) + @ContributesBinding(AppScope::class) class Impl : Base @Component - @MergeComponent(exclude = [Impl::class]) - @Singleton + @MergeComponent(AppScope::class, exclude = [Impl::class]) + @SingleIn(AppScope::class) abstract class ComponentInterface : ComponentInterfaceMerged { abstract val base: Base } @@ -505,38 +397,39 @@ class MergeComponentProcessorTest { import me.tatarka.inject.annotations.Component import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Provides + import me.tatarka.inject.annotations.Scope import software.amazon.lastmile.kotlin.inject.anvil.AppScope import software.amazon.lastmile.kotlin.inject.anvil.ContributesTo import software.amazon.lastmile.kotlin.inject.anvil.MergeComponent + @Scope + annotation class Singleton + interface Base @Inject - @OtherScope + @Singleton 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 + @ContributesTo(Unit::class) interface StringComponent { @Provides fun provideString(): String = "abc" } @Component @MergeComponent(AppScope::class) - @OtherScope + @Singleton abstract class ComponentInterface : ComponentInterfaceMerged { abstract val base: Base } """, - otherScopeSource, ) { assertThat(componentInterface.mergedComponent).isNotNull() diff --git a/compiler/src/test/kotlin/software/amazon/lastmile/kotlin/inject/anvil/processor/extend/CustomSymbolProcessorTest.kt b/compiler/src/test/kotlin/software/amazon/lastmile/kotlin/inject/anvil/processor/extend/CustomSymbolProcessorTest.kt index cbdd602..f8cf0de 100644 --- a/compiler/src/test/kotlin/software/amazon/lastmile/kotlin/inject/anvil/processor/extend/CustomSymbolProcessorTest.kt +++ b/compiler/src/test/kotlin/software/amazon/lastmile/kotlin/inject/anvil/processor/extend/CustomSymbolProcessorTest.kt @@ -11,6 +11,7 @@ import com.google.devtools.ksp.processing.SymbolProcessorEnvironment import com.google.devtools.ksp.processing.SymbolProcessorProvider import com.google.devtools.ksp.symbol.KSAnnotated import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.squareup.kotlinpoet.AnnotationSpec import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.FileSpec import com.squareup.kotlinpoet.FunSpec @@ -25,10 +26,10 @@ 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.compile import software.amazon.lastmile.kotlin.inject.anvil.componentInterface import software.amazon.lastmile.kotlin.inject.anvil.mergedComponent -import software.amazon.test.Singleton private const val CONTRIBUTING_ANNOTATION = "software.amazon.lastmile.kotlin.inject.anvil.extend.ContributingAnnotation" @@ -64,6 +65,7 @@ class CustomSymbolProcessorTest { import me.tatarka.inject.annotations.Component import software.amazon.lastmile.kotlin.inject.anvil.MergeComponent + import software.amazon.lastmile.kotlin.inject.anvil.SingleIn import kotlin.annotation.AnnotationTarget.CLASS $markerAnnotation @@ -74,8 +76,8 @@ class CustomSymbolProcessorTest { class Renderer @Component - @MergeComponent - @Singleton + @MergeComponent(Unit::class) + @SingleIn(Unit::class) interface ComponentInterface : ComponentInterfaceMerged { val string: String } @@ -132,13 +134,14 @@ class CustomSymbolProcessorTest { import me.tatarka.inject.annotations.Component import software.amazon.lastmile.kotlin.inject.anvil.MergeComponent + import software.amazon.lastmile.kotlin.inject.anvil.SingleIn @ContributesRenderer class Renderer @Component - @MergeComponent - @Singleton + @MergeComponent(Unit::class) + @SingleIn(Unit::class) interface ComponentInterface : ComponentInterfaceMerged { val string: String } @@ -167,8 +170,16 @@ class CustomSymbolProcessorTest { TypeSpec .interfaceBuilder(componentClassName) .addOriginatingKSFile(clazz.containingFile!!) - .addAnnotation(ContributesTo::class) - .addAnnotation(Singleton::class) + .addAnnotation( + AnnotationSpec.builder(ContributesTo::class) + .addMember("Unit::class") + .build(), + ) + .addAnnotation( + AnnotationSpec.builder(SingleIn::class) + .addMember("Unit::class") + .build(), + ) .addFunction( FunSpec .builder("provideString") diff --git a/compiler/src/test/kotlin/software/amazon/test/Singleton.kt b/compiler/src/test/kotlin/software/amazon/test/Singleton.kt deleted file mode 100644 index 3100040..0000000 --- a/compiler/src/test/kotlin/software/amazon/test/Singleton.kt +++ /dev/null @@ -1,7 +0,0 @@ -package software.amazon.test - -import me.tatarka.inject.annotations.Scope - -@Scope -@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER) -annotation class Singleton 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 45021ca..c021ee2 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 @@ -64,19 +64,6 @@ import kotlin.reflect.KClass * @ContributesBinding(AppScope::class, boundType = Base2::class, multibinding = true) * class Impl : Base, Base2 * ``` - * - * ## Custom scopes - * - * If you use your own scope annotations without the references such as `AppScope::class`, then - * you can use your scope directly on the class: - * ``` - * interface Authenticator - * - * @Inject - * @Singleton - * @ContributesBinding - * class RealAuthenticator : Authenticator - * ``` */ @Target(CLASS) @Repeatable @@ -84,7 +71,7 @@ public annotation class ContributesBinding( /** * The scope in which to include this contributed binding. */ - val scope: KClass<*> = Unit::class, + val scope: KClass<*>, /** * 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 08350b2..1e2aa10 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 @@ -101,31 +101,13 @@ import kotlin.reflect.KClass * } * } * ``` - * - * - * ## Custom scopes - * - * If you use your own scope annotations without the references such as `AppScope::class`, then - * you can use your scope directly on the class: - * ``` - * @ContributesSubcomponent - * @LoggedInScope - * interface LoggedInComponent { - * - * @ContributesSubcomponent.Factory - * @Singleton - * interface Factory { - * fun createLoggedInComponent(): LoggedInComponent - * } - * } - * ``` */ @Target(CLASS) public annotation class ContributesSubcomponent( /** * The scope in which to include this contributed component interface. */ - val scope: KClass<*> = Unit::class, + val scope: KClass<*>, ) { /** * A factory for the contributed subcomponent. @@ -140,6 +122,6 @@ public annotation class ContributesSubcomponent( /** * The scope in which to include this contributed component interface. */ - val scope: KClass<*> = Unit::class, + val scope: KClass<*>, ) } 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 b6bb0ee..c58d52b 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 @@ -11,22 +11,11 @@ import kotlin.reflect.KClass * @ContributesTo(AppScope::class) * interface ComponentInterface { .. } * ``` - * - * - * ## Custom scopes - * - * If you use your own scope annotations without the references such as `AppScope::class`, then - * you can use your scope directly on the class: - * ``` - * @ContributesTo - * @Singleton - * interface ComponentInterface { .. } - * ``` */ @Target(CLASS) public annotation class ContributesTo( /** * The scope in which to include this contributed component interface. */ - val scope: KClass<*> = Unit::class, + val scope: KClass<*>, ) 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 f4afece..ee57e49 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 @@ -31,27 +31,13 @@ import kotlin.reflect.KClass * ) * interface AppComponent * ``` - * - * - * ## Custom scopes - * - * If you use your own scope annotations without the references such as `AppScope::class`, then - * you can use your scope directly on the class: - * ``` - * @Component - * @MergeComponent - * @Singleton - * abstract class AppComponent( - * ... - * ) : AppComponentMerged - * ``` */ @Target(CLASS) public annotation class MergeComponent( /** * The scope in which to include this contributed component interface. */ - val scope: KClass<*> = Unit::class, + val scope: KClass<*>, /** * List of component interfaces that are contributed to the same scope, but should be