Repository: evant/kotlin-inject (kotlin)

Issue:
Conflicting declarations with multiple identical overrides
Code such as the following:

```kotlin
class MyDep

interface Provisioner {

    @Provides
    fun provideMyDep() = MyDep()
}

interface Accessor1 {

    val myDep: MyDep
}

interface Accessor2 {

    val myDep: MyDep
}

@Component
interface MyComponent : Provisioner, Accessor1, Accessor2
```

Generates the following:

```kotlin
public fun KClass<MyComponent>.create(): MyComponent = InjectMyComponent()

public class InjectMyComponent : MyComponent {
  public override val myDep: MyDep
    get() = provideMyDep()

  public override val myDep: MyDep
    get() = provideMyDep()
}
```

The override should only be generated once, as this code cannot be compiled:

```
Conflicting declarations: public open val myDep: MyDep, public open val myDep: MyDep
```

Pull request:
Fixes #309

Patch:
diff --git a/ast/core/src/main/kotlin/me/tatarka/kotlin/ast/Ast.kt b/ast/core/src/main/kotlin/me/tatarka/kotlin/ast/Ast.kt
index e731a3ff..ab909fb1 100644
--- a/ast/core/src/main/kotlin/me/tatarka/kotlin/ast/Ast.kt
+++ b/ast/core/src/main/kotlin/me/tatarka/kotlin/ast/Ast.kt
@@ -101,6 +101,8 @@ sealed class AstMember : AstElement(), AstAnnotated, AstHasModifiers {
 
     abstract fun overrides(other: AstMember): Boolean
 
+    abstract fun signatureEquals(other: AstMember): Boolean
+
     abstract val returnType: AstType
 
     abstract fun returnTypeFor(enclosingClass: AstClass): AstType
@@ -133,8 +135,19 @@ abstract class AstFunction : AstMember() {
 
     abstract override fun hashCode(): Int
 
-    override fun toString(): String {
-        return "$name(${parameters.joinToString(", ")}): $returnType"
+    override fun toString(): String = buildString {
+        receiverParameterType?.let {
+            append(it)
+            append(".")
+        }
+        append(name)
+        append("(")
+        append(parameters.joinToString())
+        append(")")
+        if (returnType.isNotUnit()) {
+            append(": ")
+            append(returnType)
+        }
     }
 }
 
@@ -144,8 +157,16 @@ abstract class AstProperty : AstMember() {
 
     abstract override fun hashCode(): Int
 
-    override fun toString(): String {
-        return "$name: $returnType"
+    override fun toString(): String = buildString {
+        receiverParameterType?.let {
+            append(it)
+            append(".")
+        }
+        append(name)
+        if (returnType.isNotUnit()) {
+            append(": ")
+            append(returnType)
+        }
     }
 }
 
diff --git a/ast/ksp/src/main/kotlin/me/tatarka/kotlin/ast/KSAst.kt b/ast/ksp/src/main/kotlin/me/tatarka/kotlin/ast/KSAst.kt
index bc5060a2..820f68d9 100644
--- a/ast/ksp/src/main/kotlin/me/tatarka/kotlin/ast/KSAst.kt
+++ b/ast/ksp/src/main/kotlin/me/tatarka/kotlin/ast/KSAst.kt
@@ -285,6 +285,11 @@ private class KSAstFunction(override val resolver: Resolver, override val declar
         return resolver.overrides(declaration, other.declaration)
     }
 
+    override fun signatureEquals(other: AstMember): Boolean {
+        if (other !is KSAstFunction) return false
+        return signatureEquals(declaration, other.declaration)
+    }
+
     override val returnType: AstType
         get() = KSAstType(resolver, declaration.returnType!!)
 
@@ -332,6 +337,11 @@ private class KSAstProperty(override val resolver: Resolver, override val declar
         return resolver.overrides(declaration, other.declaration)
     }
 
+    override fun signatureEquals(other: AstMember): Boolean {
+        if (other !is KSAstProperty) return false
+        return signatureEquals(declaration, other.declaration)
+    }
+
     override val returnType: AstType
         get() = KSAstType(resolver, declaration.type)
 
diff --git a/ast/ksp/src/main/kotlin/me/tatarka/kotlin/ast/KSUtil.kt b/ast/ksp/src/main/kotlin/me/tatarka/kotlin/ast/KSUtil.kt
index f096d629..e4c828ba 100644
--- a/ast/ksp/src/main/kotlin/me/tatarka/kotlin/ast/KSUtil.kt
+++ b/ast/ksp/src/main/kotlin/me/tatarka/kotlin/ast/KSUtil.kt
@@ -3,6 +3,8 @@ package me.tatarka.kotlin.ast
 import com.google.devtools.ksp.symbol.KSAnnotated
 import com.google.devtools.ksp.symbol.KSAnnotation
 import com.google.devtools.ksp.symbol.KSDeclaration
+import com.google.devtools.ksp.symbol.KSFunctionDeclaration
+import com.google.devtools.ksp.symbol.KSPropertyDeclaration
 import com.google.devtools.ksp.symbol.KSType
 import com.google.devtools.ksp.symbol.KSTypeAlias
 import com.google.devtools.ksp.symbol.KSTypeParameter
@@ -112,4 +114,16 @@ internal fun KSType.isConcrete(): Boolean {
     if (declaration is KSTypeParameter) return false
     if (arguments.isEmpty()) return true
     return arguments.all { it.type?.resolve()?.isConcrete() ?: false }
+}
+
+internal fun signatureEquals(left: KSPropertyDeclaration, right: KSPropertyDeclaration): Boolean {
+    return left.type.resolve() == right.type.resolve() &&
+        left.extensionReceiver?.resolve() == right.extensionReceiver?.resolve()
+}
+
+internal fun signatureEquals(left: KSFunctionDeclaration, right: KSFunctionDeclaration): Boolean {
+    return left.returnType?.resolve() == right.returnType?.resolve() &&
+        left.extensionReceiver?.resolve() == right.extensionReceiver?.resolve() &&
+        left.parameters.size == right.parameters.size &&
+        left.parameters.map { it.type.resolve() } == right.parameters.map { it.type.resolve() }
 }
\ No newline at end of file
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 ed5d056f..b6da3254 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
@@ -277,19 +277,23 @@ class TypeCollector(private val provider: AstProvider, private val options: Opti
 
             val allMethods = astClass.allMethods
             // some methods may override others
-            val methods = mutableMapOf<AstMember, AstMember?>()
+            val methods = mutableListOf<AstMember>()
+            val isProvides = mutableMapOf<AstMember, Boolean>()
             for (method in allMethods) {
-                val overrides = methods.keys.find { it.overrides(method) }
-                if (overrides != null) {
-                    methods[overrides] = method
+                val existing = methods.firstOrNull { it.name == method.name && it.signatureEquals(method) }
+                if (existing != null) {
+                    // mark provides if it overrides one that's annotated
+                    if (method.isProvides()) {
+                        isProvides[existing] = true
+                    }
                 } else {
-                    methods[method] = null
+                    methods.add(method)
+                    isProvides[method] = method.isProvides()
                 }
             }
-            for (method in methods.keys) {
+            for (method in methods) {
                 val abstract = method.isAbstract
-                val overriden = methods[method]
-                if (method.isProvides() || overriden?.isProvides() == true) {
+                if (isProvides.getValue(method)) {
                     if (method.visibility == AstVisibility.PRIVATE) {
                         provider.error("@Provides method must not be private", method)
                         continue


Test patch:
diff --git a/integration-tests/common/src/test/kotlin/me/tatarka/inject/test/InheritanceTest.kt b/integration-tests/common/src/test/kotlin/me/tatarka/inject/test/InheritanceTest.kt
index ea882f60..1a8bf208 100644
--- a/integration-tests/common/src/test/kotlin/me/tatarka/inject/test/InheritanceTest.kt
+++ b/integration-tests/common/src/test/kotlin/me/tatarka/inject/test/InheritanceTest.kt
@@ -2,12 +2,11 @@ package me.tatarka.inject.test
 
 import assertk.assertThat
 import assertk.assertions.hasClass
+import assertk.assertions.isEqualTo
 import assertk.assertions.isNotNull
 import assertk.assertions.isSameAs
 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 kotlin.test.Test
 
 interface ComponentInterface {
@@ -74,6 +73,23 @@ abstract class AbstractProvidesImplComponent: AbstractProvidesInterface {
         get() = Foo()
 }
 
+interface DuplicateDeclaration1 {
+    val bar: Bar
+
+    fun bar2(): Bar
+}
+
+interface DuplicateDeclaration2 {
+    val bar: Bar
+
+    fun bar2(): Bar
+}
+
+@Component
+abstract class DuplicateDeclarationComponent : DuplicateDeclaration1, DuplicateDeclaration2 {
+    @get:Provides val foo: Foo = Foo()
+}
+
 class InheritanceTest {
     @Test
     fun generates_a_component_that_provides_a_dep_defined_in_an_implemented_interface() {
@@ -112,4 +128,11 @@ class InheritanceTest {
 
         assertThat(component.foo).isSameAs(component.foo)
     }
+
+    @Test
+    fun generates_a_component_that_inherits_multiple_interfaces_with_the_same_signature() {
+        val component = DuplicateDeclarationComponent::class.create()
+
+        assertThat(component.bar).isEqualTo(component.bar2())
+    }
 }
\ No newline at end of file