Repository: evant/kotlin-inject (kotlin)

Issue:
Improve error message when scoped dependency is bound in multiple scopes
If some scoped dependency is (erroneously) specified in multiple components with different scopes, the error message can be confusing.

Here's the setup to copy-paste && reproduce:
```kotlin
// Some dependency
@Inject
@Scope1
class ItemsBuilder

@Scope1
@Component
abstract class Component1(
) {
  abstract val itemsBuilder: ItemsBuilder
}

@Scope2
@Component
abstract class Component2 {
  abstract val itemsBuilder: ItemsBuilder
}

@Scope
@Target(AnnotationTarget.CLASS)
annotation class Scope1

@Scope
@Target(AnnotationTarget.CLASS)
annotation class Scope2
```

Compiling this code produces the following errors:
> - Cannot find component with scope: @com.example.Scope1 to inject com.example.ItemsBuilder
> - Cannot find an `@Inject` constructor or provider for: com.example.ItemsBuilder

It is confusing, because by looking at the source you do see that there's a component with `Scope1` and that also there's an `@Inject` constructor for `ItemBuilder`.

In my actual project I had a leftover `Component2` file in other package, which I actually didn't use, and only when I found it I understood what happened -- until then I was quite puzzled by the situation, the error seemed to contradict what I actually saw :) 

P.S. This also happens if second component is unscoped (this might be important to note when implementing the fix)

Pull request:
1. Prints out checked components for easier scanning of why scope was not found.
2. Does not print out extra injection failure message is it's already covered by the above message.

Fixes #264

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 ab4a8897..b8b1ab07 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
@@ -15,15 +15,16 @@ class TypeCollector(private val provider: AstProvider, private val options: Opti
     fun collect(astClass: AstClass, accessor: Accessor = Accessor.Empty): Result {
         val typeInfo = collectTypeInfo(astClass)
         return if (!typeInfo.valid) {
-            Result(null, emptyList())
+            Result(astClass, null, emptyList())
         } else {
-            val result = Result(typeInfo.scopeClass, typeInfo.providerMethods)
+            val result = Result(astClass, typeInfo.scopeClass, typeInfo.providerMethods)
             result.collectTypes(astClass, accessor, typeInfo)
             result
         }
     }
 
     inner class Result internal constructor(
+        val astClass: AstClass,
         val scopeClass: AstClass?,
         val providerMethods: List<AstMember>,
     ) {
@@ -42,7 +43,7 @@ class TypeCollector(private val provider: AstProvider, private val options: Opti
 
         private val parents = mutableListOf<Result>()
 
-        private fun iterator(): Iterator<Result> = iterator {
+        fun iterator(): Iterator<Result> = iterator {
             yield(this@Result)
             for (parent in parents) {
                 yieldAll(parent.iterator())
@@ -157,7 +158,7 @@ class TypeCollector(private val provider: AstProvider, private val options: Opti
                         val elemAstClass = parameter.type.toAstClass()
                         val elemTypeInfo = collectTypeInfo(elemAstClass)
 
-                        val parentResult = Result(scopeClass, providerMethods)
+                        val parentResult = Result(elemAstClass, scopeClass, providerMethods)
                         parents.add(parentResult)
                         parentResult.collectTypes(
                             astClass = elemAstClass,
diff --git a/kotlin-inject-compiler/core/src/main/kotlin/me/tatarka/inject/compiler/TypeResultResolver.kt b/kotlin-inject-compiler/core/src/main/kotlin/me/tatarka/inject/compiler/TypeResultResolver.kt
index 287c60ab..736ca597 100644
--- a/kotlin-inject-compiler/core/src/main/kotlin/me/tatarka/inject/compiler/TypeResultResolver.kt
+++ b/kotlin-inject-compiler/core/src/main/kotlin/me/tatarka/inject/compiler/TypeResultResolver.kt
@@ -343,12 +343,25 @@ class TypeResultResolver(private val provider: AstProvider, private val options:
         }
     }
 
-    private fun Context.constructor(key: TypeKey, injectCtor: AstConstructor, astClass: AstClass): TypeResult? {
+    private fun Context.constructor(key: TypeKey, injectCtor: AstConstructor, astClass: AstClass): TypeResult {
         val scope = astClass.scopeType(options)
         val scopedResult = if (scope != null) types.scopedAccessor(scope) else null
         if (scope != null && scopedResult == null) {
-            provider.error("Cannot find component with scope: @$scope to inject $astClass", astClass)
-            return null
+            val checkedComponents = types.iterator().asSequence().map { result ->
+                buildString {
+                    val scope = result.astClass.scopeType(options)
+                    if (scope != null) {
+                        append("@$scope ")
+                    }
+                    append(result.astClass)
+                }
+            }
+            throw FailedToGenerateException(
+                """Cannot find component with scope: @$scope to inject $astClass
+                    |checked: [${checkedComponents.joinToString(", ")}]
+                """.trimMargin(),
+                astClass,
+            )
         }
         return if (scopedResult != null && skipScoped != injectCtor.type) {
             val (scopedComponent, types) = scopedResult


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 8d203aa8..387352c5 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
@@ -3,6 +3,7 @@ package me.tatarka.inject.test
 import assertk.all
 import assertk.assertFailure
 import assertk.assertions.contains
+import assertk.assertions.doesNotContain
 import me.tatarka.inject.ProjectCompiler
 import me.tatarka.inject.Target
 import me.tatarka.inject.compiler.Options
@@ -274,15 +275,23 @@ class FailureTest {
                     import me.tatarka.inject.annotations.Inject
                     
                     @Scope annotation class MyScope
+                    @Scope annotation class OtherScope
                     @MyScope @Inject class Foo
                     
-                    @Component abstract class MyComponent {
+                    @OtherScope @Component abstract class ParentComponent
+                    
+                    @Component abstract class MyComponent(@Component val parent: ParentComponent) {
                         abstract val f: Foo
                     }
                 """.trimIndent()
             ).compile()
-        }.output()
-            .contains("Cannot find component with scope: @MyScope to inject Foo")
+        }.output().all {
+            contains(
+                "Cannot find component with scope: @MyScope to inject Foo",
+                "checked: [MyComponent, @OtherScope ParentComponent]"
+            )
+            doesNotContain("Cannot find an @Inject constructor or provider for: Foo")
+        }
     }
 
     @ParameterizedTest