Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package me.tatarka.inject.test

import assertk.assertThat
import assertk.assertions.containsExactlyInAnyOrder
import assertk.assertions.containsOnly
import kotlin.test.Test

Expand Down Expand Up @@ -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")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -342,14 +341,17 @@ fun findAssistedFactoryInjectFunction(
return function
}

fun AstType.typeQualifierAnnotations() =
annotationsAnnotatedWith(QUALIFIER.packageName, QUALIFIER.simpleName)

fun <E> qualifier(
provider: AstProvider,
options: Options,
element: E?,
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
Expand All @@ -360,52 +362,30 @@ fun <E> 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 =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -133,7 +133,7 @@ class TypeCollector(private val provider: AstProvider, private val options: Opti
// A -> Set<A>
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)
Expand Down Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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(
Expand Down Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<String> = 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<kotlin.String>")
}
}

Expand Down