Skip to content

check for duplicate bindings between child and parent components #445

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Oct 9, 2024
Merged
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
Expand Up @@ -152,6 +152,24 @@ class TypeCollector(private val provider: AstProvider, private val options: Opti

val constructor = astClass.primaryConstructor
if (constructor != null) {
fun Result.getAllTypesAndMethods(): Map<TypeKey, AstMember> =
types.mapValues { it.value.method } +
providerTypes.mapValues { it.value.method }

fun Result.getContainerTypesAndMethods(): Map<TypeKey, AstMember> =
containerTypes.mapKeys { it.key.containerTypeKey(provider) }
.mapValues { it.value.first().method }

val childAndParentsTypes = mutableMapOf<TypeKey, AstMember>()
val childAndParentsContainerTypes = mutableMapOf<TypeKey, AstMember>()
val needsToCheckDuplicateTypesWithParents = !accessor.isNotEmpty()

if (needsToCheckDuplicateTypesWithParents) {
// Start adding the children types
childAndParentsTypes.putAll(getAllTypesAndMethods())
childAndParentsContainerTypes.putAll(getContainerTypesAndMethods())
}

for (parameter in constructor.parameters) {
if (parameter.isComponent()) {
val elemAstClass = parameter.type.toAstClass()
Expand All @@ -164,6 +182,20 @@ class TypeCollector(private val provider: AstProvider, private val options: Opti
accessor = accessor + parameter.name,
typeInfo = elemTypeInfo
)

if (needsToCheckDuplicateTypesWithParents) {
val parentTypes = parentResult.getAllTypesAndMethods()
val parentContainerTypes = parentResult.getContainerTypesAndMethods()
checkDuplicateTypesBetweenResults(
parentTypes,
parentContainerTypes,
childAndParentsTypes,
childAndParentsContainerTypes
)

childAndParentsTypes.putAll(parentTypes)
childAndParentsContainerTypes.putAll(parentContainerTypes)
}
}
}
}
Expand All @@ -183,6 +215,26 @@ class TypeCollector(private val provider: AstProvider, private val options: Opti
}
}

private fun checkDuplicateTypesBetweenResults(
result1Types: Map<TypeKey, AstMember>,
result1ContainerTypes: Map<TypeKey, AstMember>,
result2Types: Map<TypeKey, AstMember>,
result2ContainerTypes: Map<TypeKey, AstMember>,
) {
val result1TypesAndContainerTypes = result1Types + result1ContainerTypes

val result2TypesAndContainerTypes = result2Types + result2ContainerTypes

// We should allow for both Results to contribute to the same multibinding type
result1Types.keys.intersect(result2TypesAndContainerTypes.keys).forEach {
duplicate(it, result1Types.getValue(it), result2TypesAndContainerTypes.getValue(it))
}

result1TypesAndContainerTypes.keys.intersect(result2Types.keys).forEach {
duplicate(it, result1TypesAndContainerTypes.getValue(it), result2Types.getValue(it))
}
}

private fun addContainerType(
provider: AstProvider,
key: TypeKey,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import assertk.assertions.isTrue
import me.tatarka.inject.ProjectCompiler
import me.tatarka.inject.Target
import me.tatarka.inject.compiler.Options
import org.junit.jupiter.api.assertDoesNotThrow
import org.junit.jupiter.api.io.TempDir
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.EnumSource
Expand Down Expand Up @@ -132,6 +133,165 @@ class FailureTest {
.contains("Cannot provide: String", "as it is already provided")
}

@ParameterizedTest
@EnumSource(Target::class)
fun fails_if_the_same_type_is_provided_by_child_and_parent(target: Target) {
val projectCompiler = ProjectCompiler(target, workingDir)
assertFailure {
projectCompiler.source(
"MyComponent.kt",
"""
import me.tatarka.inject.annotations.Component
import me.tatarka.inject.annotations.Provides

interface Parent {
val string1: String
}

@Component abstract class MyComponent(@Component val parent: Parent) {
@Provides fun providesString2(): String = "two"
}
""".trimIndent()
).compile()
}.output()
.contains("Cannot provide: String", "as it is already provided")
}

@ParameterizedTest
@EnumSource(Target::class)
fun fails_if_the_same_type_is_provided_by_child_and_parent_component(target: Target) {
val projectCompiler = ProjectCompiler(target, workingDir)
assertFailure {
projectCompiler.source(
"MyComponent.kt",
"""
import me.tatarka.inject.annotations.Component
import me.tatarka.inject.annotations.Provides

@Component abstract class Parent {
@Provides fun providesString1(): String = "one"
}

@Component abstract class MyComponent(@Component val parent: Parent) {
@Provides fun providesString2(): String = "two"
}
""".trimIndent()
).compile()
}.output()
.contains("Cannot provide: String", "as it is already provided")
}

@ParameterizedTest
@EnumSource(Target::class)
fun some_test(target: Target) {
val projectCompiler = ProjectCompiler(target, workingDir)
assertDoesNotThrow {
projectCompiler.source(
"MyComponent.kt",
"""
import me.tatarka.inject.annotations.Component
import me.tatarka.inject.annotations.Provides
import me.tatarka.inject.annotations.Inject

@Component abstract class ParentComponent {
abstract val parentString: String

@Provides
protected fun foo() = "parent"
}

@Component abstract class SimpleChildComponent1(@Component val parent: ParentComponent) {
abstract val childString: String
}

@Component abstract class SimpleChildComponent2(@Component val parent: SimpleChildComponent1) {
abstract val childString: String
}
""".trimIndent()
).compile()
}
}

@ParameterizedTest
@EnumSource(Target::class)
fun fails_if_a_type_is_provided_by_child_as_multibinding_and_by_parent_component_as_type(target: Target) {
val projectCompiler = ProjectCompiler(target, workingDir)
assertFailure {
projectCompiler.source(
"MyComponent.kt",
"""
import me.tatarka.inject.annotations.Component
import me.tatarka.inject.annotations.Provides
import me.tatarka.inject.annotations.IntoSet

@Component abstract class Parent {
@Provides fun providesSetOfString1(): Set<String> = setOf("one")
}

@Component abstract class MyComponent(@Component val parent: Parent) {
@IntoSet @Provides fun providesListOfString2(): String = "two"
}
""".trimIndent()
).compile()
}.output()
.contains("Cannot provide: Set<kotlin.String>", "as it is already provided")
}

@ParameterizedTest
@EnumSource(Target::class)
fun doesnt_fail_if_a_type_is_provided_by_child_as_multibinding_and_by_parent_component_as_multibinding(
target: Target
) {
val projectCompiler = ProjectCompiler(target, workingDir)
assertDoesNotThrow {
projectCompiler.source(
"MyComponent.kt",
"""
import me.tatarka.inject.annotations.Component
import me.tatarka.inject.annotations.Provides
import me.tatarka.inject.annotations.IntoSet

@Component abstract class Parent {
@IntoSet @Provides fun providesListOfString1(): String = "one"
}

@Component abstract class MyComponent(@Component val parent: Parent) {
@IntoSet @Provides fun providesListOfString2(): String = "two"
}
""".trimIndent()
).compile()
}
}

@ParameterizedTest
@EnumSource(Target::class)
fun fails_if_the_same_type_is_provided_by_two_parents(target: Target) {
val projectCompiler = ProjectCompiler(target, workingDir)
assertFailure {
projectCompiler.source(
"MyComponent.kt",
"""
import me.tatarka.inject.annotations.Component
import me.tatarka.inject.annotations.Provides

interface Parent1 {
val int1: Int
}

interface Parent2 {
val int2: Int
}

@Component abstract class MyComponent(
@Component val parent1: Parent1,
@Component val parent2: Parent2
)
""".trimIndent()
).compile()
}.output()
.contains("Cannot provide: Int", "as it is already provided")
}

@ParameterizedTest
@EnumSource(Target::class)
fun fails_if_type_cannot_be_provided(target: Target) {
Expand Down