diff --git a/integration-tests/common-test/src/test/kotlin/me/tatarka/inject/test/MultibindsTest.kt b/integration-tests/common-test/src/test/kotlin/me/tatarka/inject/test/MultibindsTest.kt index df81ccde..878d6235 100644 --- a/integration-tests/common-test/src/test/kotlin/me/tatarka/inject/test/MultibindsTest.kt +++ b/integration-tests/common-test/src/test/kotlin/me/tatarka/inject/test/MultibindsTest.kt @@ -1,6 +1,7 @@ package me.tatarka.inject.test import assertk.assertThat +import assertk.assertions.containsExactlyInAnyOrder import assertk.assertions.containsOnly import kotlin.test.Test @@ -70,4 +71,14 @@ class MultibindsTest { assertThat(component.stringMap).containsOnly("1" to "string") assertThat(component.myStringMap).containsOnly("1" to "myString") } + + @Test + fun generate_a_component_with_qualified_multibindings() { + val component: MultibindWithQualifiersComponent = MultibindWithQualifiersComponent::class.create() + + assertThat(component.set1).containsExactlyInAnyOrder("1") + assertThat(component.set2).containsExactlyInAnyOrder("2") + assertThat(component.map1.toList()).containsExactlyInAnyOrder("1" to "2") + assertThat(component.map2.toList()).containsExactlyInAnyOrder("2" to "1") + } } diff --git a/integration-tests/common/src/main/kotlin/me/tatarka/inject/test/Multibinds.kt b/integration-tests/common/src/main/kotlin/me/tatarka/inject/test/Multibinds.kt index 2a7b06d4..fb618a4f 100644 --- a/integration-tests/common/src/main/kotlin/me/tatarka/inject/test/Multibinds.kt +++ b/integration-tests/common/src/main/kotlin/me/tatarka/inject/test/Multibinds.kt @@ -24,6 +24,30 @@ abstract class SetComponent { @Provides @IntoSet get() = FooValue("2") } +@Component +abstract class MultibindWithQualifiersComponent { + abstract val set1: Set<@Named("foo") String> + abstract val set2: Set<@Named("bar") String> + abstract val map1: Map<@Named("bar") String, @Named("foo") String> + abstract val map2: Map<@Named("foo") String, @Named("bar") String> + + @Provides + @IntoSet + protected fun fooSet(): @Named("foo") String = "1" + + @Provides + @IntoSet + protected fun barSet(): @Named("bar") String = "2" + + @Provides + @IntoMap + protected fun fooMap(): Pair<@Named("foo") String, @Named("bar") String> = "2" to "1" + + @Provides + @IntoMap + protected fun barMap(): Pair<@Named("bar") String, @Named("foo") String> = "1" to "2" +} + @Component abstract class DynamicKeyComponent { diff --git a/kotlin-inject-compiler/core/src/main/kotlin/me/tatarka/inject/compiler/InjectGenerator.kt b/kotlin-inject-compiler/core/src/main/kotlin/me/tatarka/inject/compiler/InjectGenerator.kt index 2cb13d37..af4395ee 100644 --- a/kotlin-inject-compiler/core/src/main/kotlin/me/tatarka/inject/compiler/InjectGenerator.kt +++ b/kotlin-inject-compiler/core/src/main/kotlin/me/tatarka/inject/compiler/InjectGenerator.kt @@ -21,7 +21,6 @@ import me.tatarka.kotlin.ast.AstProvider import me.tatarka.kotlin.ast.AstType import me.tatarka.kotlin.ast.AstVisibility import me.tatarka.kotlin.ast.Messenger -import me.tatarka.kotlin.ast.annotationAnnotatedWith import me.tatarka.kotlin.ast.hasAnnotation private const val ANNOTATION_PACKAGE_NAME = "me.tatarka.inject.annotations" @@ -342,6 +341,9 @@ fun findAssistedFactoryInjectFunction( return function } +fun AstType.typeQualifierAnnotations() = + annotationsAnnotatedWith(QUALIFIER.packageName, QUALIFIER.simpleName) + fun qualifier( provider: AstProvider, options: Options, @@ -349,7 +351,7 @@ fun qualifier( type: AstType, ): AstAnnotation? where E : AstElement, E : AstAnnotated { // check for qualifiers incorrectly applied to type arguments - fun checkTypeArgs(packageName: String, simpleName: String, type: AstType) { + fun checkTypeArgs(type: AstType) { @Suppress("SwallowedException") val arguments = try { type.arguments @@ -360,52 +362,30 @@ fun qualifier( } for (typeArg in arguments) { - val argQualifier = typeArg.annotationAnnotatedWith(packageName, simpleName) - if (argQualifier != null) { - provider.error("Qualifier: $argQualifier can only be applied to the outer type", typeArg) + val argQualifiers = typeArg.typeQualifierAnnotations().toList() + if (argQualifiers.size > 1) { + provider.error("Cannot apply multiple qualifiers: $argQualifiers", typeArg) } - checkTypeArgs(packageName, simpleName, typeArg) + checkTypeArgs(typeArg) } } - fun qualifier( - packageName: String, - simpleName: String, - provider: AstProvider, - element: E?, - type: AstType, - ): AstAnnotation? { - val qualifiers = ( - element?.annotationsAnnotatedWith(packageName, simpleName).orEmpty() + - type.annotationsAnnotatedWith(packageName, simpleName) - ).toList() - if (qualifiers.size > 1) { - provider.error("Cannot apply multiple qualifiers: $qualifiers", element) - } - checkTypeArgs(packageName, simpleName, type) - return qualifiers.firstOrNull() - } - // check our qualifier annotation first, then check the javax qualifier annotation. This allows you to have both - // in case your in the middle of a migration. - val qualifier = qualifier( - QUALIFIER.packageName, - QUALIFIER.simpleName, - provider, - element, - type, - ) - if (qualifier != null) return qualifier - return if (options.enableJavaxAnnotations) { - qualifier( - JAVAX_QUALIFIER.packageName, - JAVAX_QUALIFIER.simpleName, - provider, - element, - type, - ) - } else { - null + val qualifiers = ( + element?.annotationsAnnotatedWith(QUALIFIER.packageName, QUALIFIER.simpleName).orEmpty() + + if (options.enableJavaxAnnotations) { + element?.annotationsAnnotatedWith(JAVAX_QUALIFIER.packageName, JAVAX_QUALIFIER.simpleName).orEmpty() + } else { + emptySequence() + } + ).toList() + + val qualifiersIncludingType = (qualifiers + type.typeQualifierAnnotations().toList()).distinct() + if (qualifiersIncludingType.size > 1) { + provider.error("Cannot apply multiple qualifiers: $qualifiersIncludingType", element) } + + checkTypeArgs(type) + return qualifiers.firstOrNull() // type qualifier is used separately in TypeKey } fun AstMember.isProvider(): Boolean = diff --git a/kotlin-inject-compiler/core/src/main/kotlin/me/tatarka/inject/compiler/TypeCollector.kt b/kotlin-inject-compiler/core/src/main/kotlin/me/tatarka/inject/compiler/TypeCollector.kt index 48a7a895..2ca61822 100644 --- a/kotlin-inject-compiler/core/src/main/kotlin/me/tatarka/inject/compiler/TypeCollector.kt +++ b/kotlin-inject-compiler/core/src/main/kotlin/me/tatarka/inject/compiler/TypeCollector.kt @@ -123,7 +123,7 @@ class TypeCollector(private val provider: AstProvider, private val options: Opti val containerKey = ContainerKey.MapKey( resolvedType.arguments[0], resolvedType.arguments[1], - key.qualifier + key.memberQualifier ) addContainerType(provider, key, containerKey, member, accessor, scope, scopedComponent) } else { @@ -133,7 +133,7 @@ class TypeCollector(private val provider: AstProvider, private val options: Opti // A -> Set val returnType = member.returnTypeFor(astClass) val key = TypeKey(returnType, qualifier) - val containerKey = ContainerKey.SetKey(returnType, key.qualifier) + val containerKey = ContainerKey.SetKey(returnType, key.memberQualifier) addContainerType(provider, key, containerKey, member, accessor, scope, scopedComponent) } else { val returnType = member.returnTypeFor(astClass) @@ -507,29 +507,41 @@ sealed class ContainerKey { abstract val creator: String abstract fun containerTypeKey(provider: AstProvider): TypeKey - data class SetKey(val type: AstType, val qualifier: AstAnnotation? = null) : ContainerKey() { + data class SetKey( + val typeKey: TypeKey, + val qualifier: AstAnnotation? = null, + ) : ContainerKey() { + constructor(type: AstType, qualifier: AstAnnotation? = null) : this(TypeKey(type), qualifier) + override val creator: String = "setOf" override fun containerTypeKey(provider: AstProvider): TypeKey { - return TypeKey(provider.declaredTypeOf(Set::class, type), qualifier) + return TypeKey(provider.declaredTypeOf(Set::class, typeKey.type), qualifier) } } - data class MapKey(val key: AstType, val value: AstType, val qualifier: AstAnnotation? = null) : ContainerKey() { + data class MapKey( + val keyTypeKey: TypeKey, + val valueTypeKey: TypeKey, + val qualifier: AstAnnotation? = null, + ) : ContainerKey() { + constructor(key: AstType, value: AstType, qualifier: AstAnnotation? = null) : + this(TypeKey(key), TypeKey(value), qualifier) + override val creator: String = "mapOf" override fun containerTypeKey(provider: AstProvider): TypeKey { - return TypeKey(provider.declaredTypeOf(Map::class, key, value), qualifier) + return TypeKey(provider.declaredTypeOf(Map::class, keyTypeKey.type, valueTypeKey.type), qualifier) } } companion object { fun fromContainer(key: TypeKey): ContainerKey? { if (key.type.isSet()) { - return SetKey(key.type.arguments[0], key.qualifier) + return SetKey(key.type.arguments[0], key.memberQualifier) } if (key.type.isMap()) { - return MapKey(key.type.arguments[0], key.type.arguments[1], key.qualifier) + return MapKey(key.type.arguments[0], key.type.arguments[1], key.memberQualifier) } return null } diff --git a/kotlin-inject-compiler/core/src/main/kotlin/me/tatarka/inject/compiler/TypeKey.kt b/kotlin-inject-compiler/core/src/main/kotlin/me/tatarka/inject/compiler/TypeKey.kt index a3073da5..a340b319 100644 --- a/kotlin-inject-compiler/core/src/main/kotlin/me/tatarka/inject/compiler/TypeKey.kt +++ b/kotlin-inject-compiler/core/src/main/kotlin/me/tatarka/inject/compiler/TypeKey.kt @@ -3,11 +3,14 @@ package me.tatarka.inject.compiler import me.tatarka.kotlin.ast.AstAnnotation import me.tatarka.kotlin.ast.AstType -class TypeKey(val type: AstType, val qualifier: AstAnnotation? = null) { +class TypeKey(val type: AstType, val memberQualifier: AstAnnotation? = null) { + + val qualifier = memberQualifier ?: type.typeQualifierAnnotations().firstOrNull() override fun equals(other: Any?): Boolean { if (other !is TypeKey) return false - return qualifier == other.qualifier && type == other.type + return qualifier == other.qualifier && type == other.type && + type.arguments.eqvItr(other.type.arguments, ::qualifiersEquals) } override fun hashCode(): Int { @@ -24,4 +27,13 @@ class TypeKey(val type: AstType, val qualifier: AstAnnotation? = null) { } append(type) }.toString() + + companion object { + private fun qualifiersEquals(left: AstType, right: AstType): Boolean { + val leftAnnotations = left.typeQualifierAnnotations().asIterable() + val rightAnnotations = right.typeQualifierAnnotations().asIterable() + return leftAnnotations.eqvItr(rightAnnotations, AstAnnotation::equals) && + left.arguments.eqvItr(right.arguments, ::qualifiersEquals) + } + } } \ No newline at end of file diff --git a/kotlin-inject-compiler/core/src/main/kotlin/me/tatarka/inject/compiler/TypeResultResolver.kt b/kotlin-inject-compiler/core/src/main/kotlin/me/tatarka/inject/compiler/TypeResultResolver.kt index 618e131f..f9fd8e4b 100644 --- a/kotlin-inject-compiler/core/src/main/kotlin/me/tatarka/inject/compiler/TypeResultResolver.kt +++ b/kotlin-inject-compiler/core/src/main/kotlin/me/tatarka/inject/compiler/TypeResultResolver.kt @@ -235,7 +235,7 @@ class TypeResultResolver(private val provider: AstProvider, private val options: } if (key.type.isLazy()) { - val lKey = TypeKey(key.type.arguments[0], key.qualifier) + val lKey = TypeKey(key.type.arguments[0], key.memberQualifier) return Lazy(key = lKey) { resolveOrNull(this, element = element, key = lKey) ?: return null } @@ -282,7 +282,7 @@ class TypeResultResolver(private val provider: AstProvider, private val options: private fun Context.set(key: TypeKey): TypeResult? { val innerType = key.type.arguments[0] - val containerKey = ContainerKey.SetKey(innerType, key.qualifier) + val containerKey = ContainerKey.SetKey(innerType, key.memberQualifier) val args = types.containerArgs(containerKey) if (args.isNotEmpty()) { return Container( @@ -295,7 +295,7 @@ class TypeResultResolver(private val provider: AstProvider, private val options: } if (innerType.isFunction()) { - val containerKey = ContainerKey.SetKey(innerType.arguments.last(), key.qualifier) + val containerKey = ContainerKey.SetKey(innerType.arguments.last(), key.memberQualifier) val args = types.containerArgs(containerKey) if (args.isEmpty()) return null return Container( @@ -310,7 +310,7 @@ class TypeResultResolver(private val provider: AstProvider, private val options: } if (innerType.isLazy()) { - val containerKey = ContainerKey.SetKey(innerType.arguments[0], key.qualifier) + val containerKey = ContainerKey.SetKey(innerType.arguments[0], key.memberQualifier) val args = types.containerArgs(containerKey) if (args.isEmpty()) return null return Container( @@ -328,7 +328,7 @@ class TypeResultResolver(private val provider: AstProvider, private val options: private fun Context.map(key: TypeKey): TypeResult? { val type = key.type.resolvedType() - val containerKey = ContainerKey.MapKey(type.arguments[0], type.arguments[1], key.qualifier) + val containerKey = ContainerKey.MapKey(type.arguments[0], type.arguments[1], key.memberQualifier) val args = types.containerArgs(containerKey) if (args.isEmpty()) return null return Container( @@ -396,7 +396,7 @@ class TypeResultResolver(private val provider: AstProvider, private val options: ) } } - val fKey = TypeKey(resolveType.arguments.last(), key.qualifier) + val fKey = TypeKey(resolveType.arguments.last(), key.memberQualifier) return Function(this, args = args) { context -> resolveOrNull(context, element = element, key = fKey) ?: return null } diff --git a/kotlin-inject-compiler/test/src/test/kotlin/me/tatarka/inject/test/FailureTest.kt b/kotlin-inject-compiler/test/src/test/kotlin/me/tatarka/inject/test/FailureTest.kt index 06151316..3c608597 100644 --- a/kotlin-inject-compiler/test/src/test/kotlin/me/tatarka/inject/test/FailureTest.kt +++ b/kotlin-inject-compiler/test/src/test/kotlin/me/tatarka/inject/test/FailureTest.kt @@ -1437,7 +1437,7 @@ class FailureTest { @ParameterizedTest @EnumSource(Target::class) - fun fails_if_multiple_qualifier_is_applied_to_generic_type(target: Target) { + fun fails_if_type_qualifier_is_missing_in_generic_type(target: Target) { val projectCompiler = ProjectCompiler(target, workingDir) assertFailure { @@ -1457,12 +1457,12 @@ class FailureTest { abstract class MultipleQualifiersComponent { abstract val foo: List<@MyQualifier String> - @Provides fun providesFoo(): List<@MyQualifier String> = "test" + @Provides fun providesFoo(): List = listOf("test") } """.trimIndent() ).compile() }.output().all { - contains("Qualifier: @MyQualifier can only be applied to the outer type") + contains("Cannot find an @Inject constructor or provider for: List") } }