Repository: evant/kotlin-inject (kotlin)

Issue:
Question: multiple interfaces and one implementation
Hi all, I am struggling with this design: ONE adapter implements TWO interfaces. How do I define this correctly? Thanks!

```kotlin 
interface UserReadPort {
    fun findAll(): Set<UserEntity>
    fun findById(id: UserId): UserEntity?
}

interface UserWritePort {
    fun addUser(user: UserEntity)
    fun remove(id: UserId)
    fun removeAll()
}

@Inject
class UserInMemoryAdapter : UserReadPort, UserWritePort { // ...

@Inject
class CreateUserUseCase(lazyUserWritePort: Lazy<UserWritePort>, lazyUserReadPort: Lazy<UserReadPort>) {
    private val userWritePort by lazyUserWritePort
    private val userReadPort by lazyUserReadPort

@Component
abstract class AppComponent(private val port: Int) {
    abstract val createUserUseCase: CreateUserUseCase

    @Provides
    fun userReadInMemoryAdapter(userReadPort: UserReadPort): UserReadPort = userReadPort

    @Provides
    fun userWriteInMemoryAdapter(userWritePort: UserWritePort): UserWritePort = userWritePort
    

```

produces

```
Exception in thread "main" kotlin.UninitializedPropertyAccessException: lateinit property userWritePort has not been initialized
	at InjectAppComponent$createUserUseCase$1.invoke(InjectAppComponent.kt:15)
	at InjectAppComponent$createUserUseCase$1.invoke(InjectAppComponent.kt:11)
...
```

Generated code

```kotlin

public fun KClass<AppComponent>.create(port: Int): AppComponent = InjectAppComponent(port)

public class InjectAppComponent(
  port: Int,
) : AppComponent(port) {
  override val createUserUseCase: CreateUserUseCase
    get() = CreateUserUseCase(
      lazyUserWritePort = lazy {
        run<UserWritePort> {
          lateinit var userWritePort: UserWritePort
          userWriteInMemoryAdapter(
            userWritePort = userWritePort
          ).also {
            userWritePort = it
          }
        }

      },
      lazyUserReadPort = lazy {
        run<UserReadPort> {
          lateinit var userReadPort: UserReadPort
          userReadInMemoryAdapter(
            userReadPort = userReadPort
          ).also {
            userReadPort = it
          }
        }

      }
    )
}
```

Pull request:
…(#386)

* added test for case with circular dependency inside lazy dependency #386
* fix for the test case by considering any cycles in dependency chain not separated by lazy dep to be unresolvable

Fixes #386

Patch:
diff --git a/kotlin-inject-compiler/core/src/main/kotlin/me/tatarka/inject/compiler/CycleDetector.kt b/kotlin-inject-compiler/core/src/main/kotlin/me/tatarka/inject/compiler/CycleDetector.kt
index 76c89e7f..b06fd64a 100644
--- a/kotlin-inject-compiler/core/src/main/kotlin/me/tatarka/inject/compiler/CycleDetector.kt
+++ b/kotlin-inject-compiler/core/src/main/kotlin/me/tatarka/inject/compiler/CycleDetector.kt
@@ -38,8 +38,9 @@ class CycleDetector {
      * @see CycleResult
      */
     fun <T> check(key: TypeKey, element: AstElement, block: (CycleResult) -> T): T {
-        val cycleResult = if (entries.any { it is Entry.Element && it.value == element }) {
-            if (entries.any { it is Entry.Delayed }) {
+        val lastRepeatIndex = entries.indexOfLast { it is Entry.Element && it.value == element }
+        val cycleResult = if (lastRepeatIndex != -1) {
+            if (entries.indexOfLast { it is Entry.Delayed } > lastRepeatIndex) {
                 val name = key.type.toVariableName()
                 resolving.add(Resolving(key, name))
                 CycleResult.Resolvable(name)


Test patch:
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 87fc6adb..fdc8ff68 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
@@ -823,6 +823,34 @@ class FailureTest {
         }
     }
 
+    @ParameterizedTest
+    @EnumSource(Target::class)
+    fun fails_if_cycle_is_in_lazy_dependency(target: Target) {
+        val projectCompiler = ProjectCompiler(target, workingDir)
+
+        assertFailure {
+            projectCompiler.source(
+                "MyComponent.kt",
+                """
+                import me.tatarka.inject.annotations.Component
+                import me.tatarka.inject.annotations.Inject
+                
+                @Inject
+                class Cycle(val cycle: Cycle)        
+                
+                @Inject
+                class Dep(val cycle: Lazy<Cycle>)
+
+                @Component abstract class MyComponent {
+                    abstract val dep: Dep
+                }  
+                """.trimIndent()
+            ).compile()
+        }.output().all {
+            contains("Cycle detected")
+        }
+    }
+
     @ParameterizedTest
     @EnumSource(Target::class)
     fun fails_if_scoped_provides_is_suspend(target: Target) {