Repository: evant/kotlin-inject (kotlin)

Issue:
Compile error in generated code: Unresolved reference: _scoped
Here's a stripped down repro case. Basically I have a cycle, but the `Lazy` avoid its somewhat. kotlin-inject seems to handle this nicely via the generated `lateinit`... but the `run` block surrounding it removes the `require(parent is ScopedComponent)` from inferences (not sure why). 

If I remove the `run` from the generated code, everything works nicely. If I add an extra `require(parent is ScopedComponent)` within the run, it works nicely again.

``` kotlin
@Component
@ApplicationScope
abstract class ParentComponent {
    @Provides
    fun provideHttpClient(
        dataRepository: DataRepository
    ): HttpClient = HttpClient(dataRepository)
}

@Component
@ActivityScope
abstract class ChildComponent(
    @Component val parent: ParentComponent
) {
    abstract val dataRepository: DataRepository
}

@ApplicationScope
@Inject
class DataRepository(
    val authRepository: Lazy<Api>,
)

@ApplicationScope
@Inject
class Api(
    val dataRepository: Lazy<DataRepository>,
)

class HttpClient(
    val dataRepository: DataRepository
)
```

This generates:

``` kotlin
public class InjectChildComponent(
  parent: ParentComponent
) : ChildComponent(parent), ScopedComponent {
  public override val _scoped: LazyMap = LazyMap()

  public override val dataRepository: DataRepository
    get() {
      require(parent is ScopedComponent)
      return parent._scoped.get("app.tivi.inject.AuthRepository") {
        run {
          lateinit var dataRepository: DataRepository
          
          // Chris:
          // parent is no longer inferred as ScopedComponent here,
          // so `parent._scoped` below doesn't compile
          
          DataRepository(
            authRepository = lazy {
              parent._scoped.get("app.tivi.inject.Api") {
                Api(
                  dataRepository = lazy {
                    parent._scoped.get("app.tivi.inject.AuthRepository") {
                      dataRepository
                    }
                  }
                )
              }
            }
          ).also {
            dataRepository = it
          }
        }

      }
    }
}
```

Pull request:
Fixes #279

Patch:
diff --git a/kotlin-inject-compiler/core/src/main/kotlin/me/tatarka/inject/compiler/TypeResultGenerator.kt b/kotlin-inject-compiler/core/src/main/kotlin/me/tatarka/inject/compiler/TypeResultGenerator.kt
index 885a6a99..fdc6d097 100644
--- a/kotlin-inject-compiler/core/src/main/kotlin/me/tatarka/inject/compiler/TypeResultGenerator.kt
+++ b/kotlin-inject-compiler/core/src/main/kotlin/me/tatarka/inject/compiler/TypeResultGenerator.kt
@@ -155,6 +155,7 @@ data class TypeResultGenerator(val options: Options, val implicitAccessor: Acces
                             add("$paramName = ")
                             add(param.generate())
                         }
+
                         else -> add(param.generate())
                     }
                 }
@@ -197,6 +198,7 @@ data class TypeResultGenerator(val options: Options, val implicitAccessor: Acces
                 } else {
                     add("%T::class.java.name", typeName)
                 }
+
                 is ParameterizedTypeName -> {
                     addTypeName(typeName.rawType)
                     for (arg in typeName.typeArguments) {
@@ -204,6 +206,7 @@ data class TypeResultGenerator(val options: Options, val implicitAccessor: Acces
                         addTypeName(arg)
                     }
                 }
+
                 is LambdaTypeName -> {
                     val functionName = if (typeName.isSuspending) {
                         ClassName("kotlin.coroutines", "SuspendFunction${typeName.parameters.size}")
@@ -219,6 +222,7 @@ data class TypeResultGenerator(val options: Options, val implicitAccessor: Acces
                     add("+")
                     addTypeName(typeName.returnType)
                 }
+
                 else -> add("%S", typeName)
             }
         } else {
@@ -323,8 +327,14 @@ data class TypeResultGenerator(val options: Options, val implicitAccessor: Acces
 
     private fun TypeResult.LateInit.generate(): CodeBlock {
         return CodeBlock.builder().apply {
-            beginControlFlow("run")
-            addStatement("lateinit var %N: %T", name, result.key.type.toTypeName())
+            // Using the run extension method creates a new 'this' scope which causes kotlin's type inference for the
+            // current 'this' to be dropped. This breaks scoped parent resolution as we use
+            // require(parent is ScopedComponent) to access the parent scope.
+            // To work around this we explicitly pass the return type to ensure it calls the top-level run overload
+            // instead of the extension method.
+            val returnType = result.key.type.toTypeName()
+            beginControlFlow("run<%T>", returnType)
+            addStatement("lateinit var %N: %T", name, returnType)
             add(result.generate())
             beginControlFlow(".also")
             addStatement("%N = it", name)


Test patch:
diff --git a/integration-tests/common/src/test/kotlin/me/tatarka/inject/test/RecursiveTest.kt b/integration-tests/common/src/test/kotlin/me/tatarka/inject/test/RecursiveTest.kt
index 55d09e2e..accd5807 100644
--- a/integration-tests/common/src/test/kotlin/me/tatarka/inject/test/RecursiveTest.kt
+++ b/integration-tests/common/src/test/kotlin/me/tatarka/inject/test/RecursiveTest.kt
@@ -93,6 +93,24 @@ abstract class OptimizedCycleComponent {
     abstract val baz: CycleBaz
 }
 
+@CustomScope
+@Inject
+class ScopeBar(val foo: ScopeFoo)
+
+@Inject
+class ScopeFoo(val bar: Lazy<ScopeBar>)
+
+@Component
+@CustomScope
+abstract class ParentCycleComponent
+
+@Component
+abstract class ChildCycleComponent(
+    @Component val parent: ParentCycleComponent,
+) {
+    abstract val foo: ScopeFoo
+}
+
 class RecursiveTest {
 
     @Test
@@ -152,4 +170,14 @@ class RecursiveTest {
             assertThat(baz.foo).isSameAs(baz.foo.bar.foo())
         }
     }
+
+    @Test
+    fun generates_a_component_with_a_scoped_parent_dependency_recursively() {
+        val component = ChildCycleComponent::class.create(ParentCycleComponent::class.create())
+        val foo = component.foo
+        val bar = foo.bar.value
+
+        assertThat(foo).isSameAs(foo.bar.value.foo)
+        assertThat(bar).isSameAs(bar.foo.bar.value)
+    }
 }
\ No newline at end of file