Repository: evant/kotlin-inject (kotlin)

Issue:
Implemented function still considered abstract
Imagine this example:
```kotlin
interface Interface1 {
  fun string(): String
}
                    
interface Interface2 : Interface1 {
  override fun string(): String = "abc"
}

@Component 
abstract class MyComponent1 : Interface1, Interface2
```
This fails with:
```
Cannot find an @Inject constructor or provider for: String
```
But if I change the order of the super types to the following, then the code compiles successfully:
```kotlin
@Component 
abstract class MyComponent1 : Interface2, Interface1
```

Pull request:
The old mechanism depended on the order types were defined and processed them in order without respecting overridden functions. With the fix overridden functions get updated when collecting types.

Fixes #375

Patch:
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 900d5edc..0654dd21 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
@@ -285,27 +285,53 @@ class TypeCollector(private val provider: AstProvider, private val options: Opti
                 val existing = methods.firstOrNull {
                     it.name == method.name && (it.signatureEquals(method) || it.overrides(method))
                 }
-                if (existing != null) {
-                    // mark provides if it overrides one that's annotated
-                    if (method.isProvides()) {
-                        isProvides[existing] = true
+
+                when {
+                    existing != null && method.overrides(existing) -> {
+                        // When we find a function that overrides the existing one, then we need to update all entries
+                        // for the new method and remove the existing one, because it has higher priority.
+                        methods.add(method)
+                        methods.remove(existing)
+
+                        isProvides[method] = existing.isProvides() || method.isProvides()
+                        isProvides.remove(existing)
+
+                        if (methodScopeTypes.isNotEmpty()) {
+                            val existingScopeTypes = scopeTypes[existing]
+                            if (existingScopeTypes == null) {
+                                scopeTypes[method] = methodScopeTypes
+                            } else {
+                                if (existingScopeTypes != methodScopeTypes) {
+                                    scopedMethodsWithScopedSuperMethod[method] = existing to existingScopeTypes
+                                }
+                            }
+                        }
+                        scopedMethodsWithScopedSuperMethod.remove(existing)
+                        scopeTypes.remove(existing)
                     }
+                    existing != null -> {
+                        // mark provides if it overrides one that's annotated
+                        if (method.isProvides()) {
+                            isProvides[existing] = true
+                        }
 
-                    if (methodScopeTypes.isNotEmpty()) {
-                        val existingScopeTypes = scopeTypes[existing]
-                        if (existingScopeTypes == null) {
-                            scopeTypes[existing] = methodScopeTypes
-                        } else {
-                            if (existingScopeTypes != methodScopeTypes) {
-                                scopedMethodsWithScopedSuperMethod[existing] = method to methodScopeTypes
+                        if (methodScopeTypes.isNotEmpty()) {
+                            val existingScopeTypes = scopeTypes[existing]
+                            if (existingScopeTypes == null) {
+                                scopeTypes[existing] = methodScopeTypes
+                            } else {
+                                if (existingScopeTypes != methodScopeTypes) {
+                                    scopedMethodsWithScopedSuperMethod[existing] = method to methodScopeTypes
+                                }
                             }
                         }
                     }
-                } else {
-                    methods.add(method)
-                    isProvides[method] = method.isProvides()
-                    if (methodScopeTypes.isNotEmpty()) {
-                        scopeTypes[method] = methodScopeTypes
+                    else -> {
+                        methods.add(method)
+                        isProvides[method] = method.isProvides()
+                        if (methodScopeTypes.isNotEmpty()) {
+                            scopeTypes[method] = methodScopeTypes
+                        }
                     }
                 }
             }


Test patch:
diff --git a/kotlin-inject-compiler/test/src/test/kotlin/me/tatarka/inject/test/InheritanceTest.kt b/kotlin-inject-compiler/test/src/test/kotlin/me/tatarka/inject/test/InheritanceTest.kt
new file mode 100644
index 00000000..8b2b9eec
--- /dev/null
+++ b/kotlin-inject-compiler/test/src/test/kotlin/me/tatarka/inject/test/InheritanceTest.kt
@@ -0,0 +1,47 @@
+package me.tatarka.inject.test
+
+import me.tatarka.inject.ProjectCompiler
+import me.tatarka.inject.Target
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertDoesNotThrow
+import org.junit.jupiter.api.io.TempDir
+import java.io.File
+
+class InheritanceTest {
+
+    private val target = Target.KSP
+
+    @TempDir
+    lateinit var workingDir: File
+
+    @Test
+    fun abstract_functions_can_be_implemented_by_parent_component_interfaces() {
+        val projectCompiler = ProjectCompiler(target, workingDir)
+
+        assertDoesNotThrow {
+            projectCompiler.source(
+                "MyComponent.kt",
+                """
+                    package com.test
+                    
+                    import me.tatarka.inject.annotations.Component
+                    import me.tatarka.inject.annotations.Provides
+                    
+                    interface Interface1 {
+                        fun string(): String
+                    }
+                    
+                    interface Interface2 : Interface1 {
+                        override fun string(): String = "abc"
+                    }
+
+                    @Component 
+                    abstract class MyComponent1 : Interface1, Interface2
+
+                    @Component 
+                    abstract class MyComponent2 : Interface2, Interface1
+                """.trimIndent()
+            ).compile()
+        }
+    }
+}