Repository: evant/kotlin-inject
Pull request #32 (2020-08-06 01:47:56)

Initial cycle detector implementation


Patch:
diff --git a/kotlin-inject-compiler-core/src/main/kotlin/me/tatarka/inject/compiler/Ast.kt b/kotlin-inject-compiler-core/src/main/kotlin/me/tatarka/inject/compiler/Ast.kt
index 15aa9f3..ef93491 100644
--- a/kotlin-inject-compiler-core/src/main/kotlin/me/tatarka/inject/compiler/Ast.kt
+++ b/kotlin-inject-compiler-core/src/main/kotlin/me/tatarka/inject/compiler/Ast.kt
@@ -12,7 +12,10 @@ interface AstProvider {
 
     fun warn(message: String, element: AstElement) = messenger.warn(message, element)
 
-    fun error(message: String, element: AstElement) = messenger.error(message, element)
+    fun error(message: String, element: AstElement) =
+        messenger.error(message, element)
+
+    fun AstElement.toTrace(): String
 
     fun TypeSpec.Builder.addOriginatingElement(astClass: AstClass): TypeSpec.Builder
 }
@@ -93,13 +96,38 @@ abstract class AstConstructor(private val parent: AstClass) : AstElement() {
     val type: AstType get() = parent.type
 
     abstract val parameters: List<AstParam>
+
+    abstract override fun equals(other: Any?): Boolean
+
+    abstract override fun hashCode(): Int
+
+    override fun toString(): String {
+        return "$parent(${parameters.joinToString(", ")})"
+    }
 }
 
 abstract class AstFunction : AstMethod() {
     abstract val parameters: List<AstParam>
+
+    abstract override fun equals(other: Any?): Boolean
+
+    abstract override fun hashCode(): Int
+
+    override fun toString(): String {
+        return "$name(${parameters.joinToString(", ")}): $returnType"
+    }
 }
 
-abstract class AstProperty : AstMethod()
+abstract class AstProperty : AstMethod() {
+
+    abstract override fun equals(other: Any?): Boolean
+
+    abstract override fun hashCode(): Int
+
+    override fun toString(): String {
+        return "$name: $returnType"
+    }
+}
 
 abstract class AstType : AstElement() {
 
@@ -139,6 +167,10 @@ abstract class AstParam : AstElement() {
     abstract val type: AstType
 
     abstract fun asParameterSpec(): ParameterSpec
+
+    override fun toString(): String {
+        return "$name: $type"
+    }
 }
 
 enum class AstModifier {
@@ -146,4 +178,4 @@ enum class AstModifier {
 }
 
 fun ParameterSpec.Companion.parametersOf(constructor: AstConstructor) =
-        constructor.parameters.map { it.asParameterSpec() }
+    constructor.parameters.map { it.asParameterSpec() }
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
new file mode 100644
index 0000000..bae3e99
--- /dev/null
+++ b/kotlin-inject-compiler-core/src/main/kotlin/me/tatarka/inject/compiler/CycleDetector.kt
@@ -0,0 +1,27 @@
+package me.tatarka.inject.compiler
+
+class CycleDetector {
+
+    private val _elements = mutableListOf<AstElement>()
+
+    fun trace(provider: AstProvider, message: String): String = message + ":\n" +
+            _elements.reversed().joinToString(separator = "\n") { with(provider) { it.toTrace() } }
+
+    fun check(element: AstElement): CycleResult {
+        return if (element in _elements) {
+            //TODO: check resolvable
+            CycleResult.Cycle
+        } else {
+            _elements.add(element)
+            CycleResult.None
+        }
+    }
+
+    fun pop() {
+        _elements.removeAt(_elements.lastIndex)
+    }
+}
+
+enum class CycleResult {
+    None, Resolvable, Cycle
+}
diff --git a/kotlin-inject-compiler-core/src/main/kotlin/me/tatarka/inject/compiler/FailedToGenerateException.kt b/kotlin-inject-compiler-core/src/main/kotlin/me/tatarka/inject/compiler/FailedToGenerateException.kt
index b1c76d6..f16ee86 100644
--- a/kotlin-inject-compiler-core/src/main/kotlin/me/tatarka/inject/compiler/FailedToGenerateException.kt
+++ b/kotlin-inject-compiler-core/src/main/kotlin/me/tatarka/inject/compiler/FailedToGenerateException.kt
@@ -1,3 +1,4 @@
 package me.tatarka.inject.compiler
 
-class FailedToGenerateException(message: String, val element: AstElement) : Exception(message)
+class FailedToGenerateException(message: String, val element: AstElement) :
+    Exception(message)
diff --git a/kotlin-inject-compiler-core/src/main/kotlin/me/tatarka/inject/compiler/InjectGenerator.kt b/kotlin-inject-compiler-core/src/main/kotlin/me/tatarka/inject/compiler/InjectGenerator.kt
index 6d72e6e..1d89e61 100644
--- a/kotlin-inject-compiler-core/src/main/kotlin/me/tatarka/inject/compiler/InjectGenerator.kt
+++ b/kotlin-inject-compiler-core/src/main/kotlin/me/tatarka/inject/compiler/InjectGenerator.kt
@@ -200,11 +200,14 @@ class InjectGenerator(provider: AstProvider, private val options: Options) :
             isComponent = isComponent,
             scopedInjects = scopedInjects
         )
+        val cycleDetector = CycleDetector()
         val elementScopeClass = astClass.scopeClass(messenger, options)
         val scopeFromParent = elementScopeClass != astClass
         return Context(
+            provider = this,
             source = astClass,
             collector = typeCollector,
+            cycleDetector = cycleDetector,
             scopeInterface = if (scopeFromParent) elementScopeClass else null
         )
     }
@@ -235,7 +238,7 @@ class InjectGenerator(provider: AstProvider, private val options: Options) :
     private fun provideProvides(
         providesResult: Result.Provides,
         context: Context
-    ): CodeBlock {
+    ): CodeBlock = context.use(providesResult.method) { context ->
         val codeBlock = CodeBlock.builder()
 
         val method = providesResult.method
@@ -285,13 +288,13 @@ class InjectGenerator(provider: AstProvider, private val options: Options) :
             codeBlock.endControlFlow()
         }
 
-        return codeBlock.build()
+        codeBlock.build()
     }
 
     private fun provideConstructor(
         constructor: AstConstructor,
         context: Context
-    ): CodeBlock {
+    ): CodeBlock = context.use(constructor) { context ->
         val codeBlock = CodeBlock.builder()
         codeBlock.add("%T(", constructor.type.asTypeName())
         val size = constructor.parameters.size
@@ -308,7 +311,7 @@ class InjectGenerator(provider: AstProvider, private val options: Options) :
             })
         }
         codeBlock.add(")")
-        return codeBlock.build()
+        codeBlock.build()
     }
 
     private fun provideScoped(key: TypeKey, result: Result.Scoped): CodeBlock {
@@ -335,58 +338,60 @@ class InjectGenerator(provider: AstProvider, private val options: Options) :
         }.build()
     }
 
-    private fun provideFunction(result: Result.Function, context: Context): CodeBlock {
-        return CodeBlock.builder().apply {
-            beginControlFlow("{")
-            val argList = mutableListOf<Pair<AstType, String>>()
-            result.args.forEachIndexed { index, arg ->
-                if (index != 0) {
-                    add(",")
+    private fun provideFunction(result: Result.Function, context: Context): CodeBlock =
+        context.use(result.element) { context ->
+            CodeBlock.builder().apply {
+                beginControlFlow("{")
+                val argList = mutableListOf<Pair<AstType, String>>()
+                result.args.forEachIndexed { index, arg ->
+                    if (index != 0) {
+                        add(",")
+                    }
+                    val name = "arg$index"
+                    argList.add(arg to name)
+                    add(" %L", name)
+                }
+                if (result.args.isNotEmpty()) {
+                    add(" ->")
                 }
-                val name = "arg$index"
-                argList.add(arg to name)
-                add(" %L", name)
-            }
-            if (result.args.isNotEmpty()) {
-                add(" ->")
-            }
 
-            add(provide(result.key, context.withArgs(argList)))
-            endControlFlow()
-        }.build()
-    }
+                add(provide(result.key, context.withArgs(argList)))
+                endControlFlow()
+            }.build()
+        }
 
-    private fun provideNamedFunction(result: Result.NamedFunction, context: Context): CodeBlock {
-        return CodeBlock.builder().apply {
-            beginControlFlow("{")
-            val argList = mutableListOf<Pair<AstType, String>>()
-            result.args.forEachIndexed { index, arg ->
-                if (index != 0) {
-                    add(",")
+    private fun provideNamedFunction(result: Result.NamedFunction, context: Context): CodeBlock =
+        context.use(result.function) { context ->
+            CodeBlock.builder().apply {
+                beginControlFlow("{")
+                val argList = mutableListOf<Pair<AstType, String>>()
+                result.args.forEachIndexed { index, arg ->
+                    if (index != 0) {
+                        add(",")
+                    }
+                    val name = "arg$index"
+                    argList.add(arg to name)
+                    add(" %L", name)
+                }
+                if (result.args.isNotEmpty()) {
+                    add(" ->")
                 }
-                val name = "arg$index"
-                argList.add(arg to name)
-                add(" %L", name)
-            }
-            if (result.args.isNotEmpty()) {
-                add(" ->")
-            }
 
-            val context = context.withArgs(argList)
+                val context = context.withArgs(argList)
 
-            add("%L(", result.function.asMemberName().toString())
-            val size = result.function.parameters.size
-            result.function.parameters.forEachIndexed { i, param ->
-                if (i != 0) {
-                    add(",")
+                add("%L(", result.function.asMemberName().toString())
+                val size = result.function.parameters.size
+                result.function.parameters.forEachIndexed { i, param ->
+                    if (i != 0) {
+                        add(",")
+                    }
+                    add(provide(TypeKey(param.type), context) { context.findWithIndex(it, i, size) })
                 }
-                add(provide(TypeKey(param.type), context) { context.findWithIndex(it, i, size) })
-            }
-            add(")")
+                add(")")
 
-            endControlFlow()
-        }.build()
-    }
+                endControlFlow()
+            }.build()
+        }
 
     private fun provideLazy(result: Result.Lazy, context: Context): CodeBlock {
         return CodeBlock.builder().apply {
@@ -432,6 +437,7 @@ class InjectGenerator(provider: AstProvider, private val options: Options) :
             }
             val fKey = TypeKey(key.type.arguments.last())
             return Result.Function(
+                element = key.type.toAstClass(),
                 key = fKey,
                 args = key.type.arguments.dropLast(1)
             )
@@ -468,8 +474,10 @@ class InjectGenerator(provider: AstProvider, private val options: Options) :
     }
 
     data class Context(
-        val source: AstClass,
+        val provider: AstProvider,
+        val source: AstElement,
         val collector: TypeCollector,
+        val cycleDetector: CycleDetector,
         val scopeInterface: AstClass? = null,
         val args: List<Pair<AstType, String>> = emptyList(),
         val skipScoped: AstType? = null
@@ -477,6 +485,20 @@ class InjectGenerator(provider: AstProvider, private val options: Options) :
         fun withoutScoped(scoped: AstType) = copy(skipScoped = scoped)
 
         fun withArgs(args: List<Pair<AstType, String>>) = copy(args = args)
+
+        fun <T> use(source: AstElement, f: (context: Context) -> T): T {
+            return when (cycleDetector.check(source)) {
+                CycleResult.None -> {
+                    f(copy(source = source)).also {
+                        cycleDetector.pop()
+                    }
+                }
+                CycleResult.Cycle -> {
+                    throw FailedToGenerateException(cycleDetector.trace(provider, "Cycle detected"), source)
+                }
+                CycleResult.Resolvable -> TODO()
+            }
+        }
     }
 
     sealed class Result(val name: String?) {
@@ -484,7 +506,7 @@ class InjectGenerator(provider: AstProvider, private val options: Options) :
         class Scoped(name: String?, val astClass: AstClass) : Result(name)
         class Constructor(val constructor: AstConstructor) : Result(null)
         class Container(val creator: String, val args: List<Provides>) : Result(null)
-        class Function(val key: TypeKey, val args: List<AstType>) : Result(null)
+        class Function(val element: AstElement, val key: TypeKey, val args: List<AstType>) : Result(null)
         class NamedFunction(val function: AstFunction, val args: List<AstType>) : Result(null)
 
         class Arg(val argName: String) : Result(null)
diff --git a/kotlin-inject-compiler-kapt/build.gradle b/kotlin-inject-compiler-kapt/build.gradle
index 9be5aac..fa555ee 100644
--- a/kotlin-inject-compiler-kapt/build.gradle
+++ b/kotlin-inject-compiler-kapt/build.gradle
@@ -7,6 +7,7 @@ dependencies {
     implementation project(':kotlin-inject-compiler-core')
     implementation "org.jetbrains.kotlin:kotlin-stdlib"
     implementation 'org.jetbrains.kotlinx:kotlinx-metadata-jvm:0.1.0'
+    compileOnly 'io.earcam.wrapped:jdk.compiler:1.8.132'
 }
 
 sourceCompatibility = 1.8
diff --git a/kotlin-inject-compiler-kapt/src/main/kotlin/me/tatarka/inject/compiler/kapt/ModelAst.kt b/kotlin-inject-compiler-kapt/src/main/kotlin/me/tatarka/inject/compiler/kapt/ModelAst.kt
index 0a25f85..f2071d2 100644
--- a/kotlin-inject-compiler-kapt/src/main/kotlin/me/tatarka/inject/compiler/kapt/ModelAst.kt
+++ b/kotlin-inject-compiler-kapt/src/main/kotlin/me/tatarka/inject/compiler/kapt/ModelAst.kt
@@ -7,6 +7,9 @@ import kotlinx.metadata.*
 import kotlinx.metadata.jvm.annotations
 import kotlinx.metadata.jvm.syntheticMethodForAnnotations
 import me.tatarka.inject.compiler.*
+import java.io.StringWriter
+import java.lang.Error
+import java.lang.StringBuilder
 import javax.annotation.processing.Messager
 import javax.annotation.processing.ProcessingEnvironment
 import javax.lang.model.element.*
@@ -19,7 +22,7 @@ import javax.tools.Diagnostic
 import kotlin.reflect.KClass
 
 interface ModelAstProvider :
-        AstProvider {
+    AstProvider {
 
     val env: ProcessingEnvironment
 
@@ -40,9 +43,9 @@ interface ModelAstProvider :
         for (element in ElementFilter.typesIn(packageElement.enclosedElements)) {
             for (function in ElementFilter.methodsIn(element.enclosedElements)) {
                 if (function.simpleName.contentEquals(functionName)
-                        && function.modifiers.contains(Modifier.STATIC) && function.modifiers.contains(Modifier.STATIC) && function.modifiers.contains(
-                                Modifier.FINAL
-                        )
+                    && function.modifiers.contains(Modifier.STATIC) && function.modifiers.contains(Modifier.STATIC) && function.modifiers.contains(
+                        Modifier.FINAL
+                    )
                 ) {
                     val metadata = element.metadata?.toKmPackage() ?: continue
                     val kmFunction = metadata.functions.find { it.name == functionName } ?: continue
@@ -56,9 +59,9 @@ interface ModelAstProvider :
     override fun declaredTypeOf(klass: KClass<*>, vararg astTypes: AstType): AstType {
         val type = elements.getTypeElement(klass.java.canonicalName)
         return ModelAstType(
-                this,
-                declaredType(type, astTypes.asList()),
-                klass.toKmType()
+            this,
+            declaredType(type, astTypes.asList()),
+            klass.toKmType()
         )
     }
 
@@ -66,6 +69,11 @@ interface ModelAstProvider :
         (it as ModelAstType).type
     }.toTypedArray())
 
+    override fun AstElement.toTrace(): String {
+        require(this is ModelAstElement)
+        return toString()
+    }
+
     override fun TypeSpec.Builder.addOriginatingElement(astClass: AstClass): TypeSpec.Builder = apply {
         require(astClass is ModelAstClass)
         addOriginatingElement(astClass.element)
@@ -103,16 +111,16 @@ private interface ModelAstMethod : ModelAstElement {
 }
 
 private class ModelBasicElement(provider: ModelAstProvider, override val element: Element) : AstBasicElement(),
-        ModelAstElement, ModelAstProvider by provider {
+    ModelAstElement, ModelAstProvider by provider {
     override val simpleName: String get() = element.simpleName.toString()
 }
 
 private class ModelAstClass(
-        provider: ModelAstProvider,
-        override val element: TypeElement,
-        val kmClass: KmClass?
+    provider: ModelAstProvider,
+    override val element: TypeElement,
+    val kmClass: KmClass?
 ) : AstClass(),
-        ModelAstElement, ModelAstProvider by provider {
+    ModelAstElement, ModelAstProvider by provider {
 
     override val packageName: String get() = elements.getPackageOf(element).qualifiedName.toString()
 
@@ -120,7 +128,7 @@ private class ModelAstClass(
 
     override val modifiers: Set<AstModifier> by lazy {
         collectModifiers(
-                kmClass?.flags
+            kmClass?.flags
         )
     }
 
@@ -150,10 +158,10 @@ private class ModelAstClass(
         ElementFilter.constructorsIn(element.enclosedElements).mapNotNull { constructor ->
             //TODO: not sure how to match constructors
             ModelAstConstructor(
-                    this,
-                    this,
-                    constructor,
-                    kmClass?.constructors?.first()
+                this,
+                this,
+                constructor,
+                kmClass?.constructors?.first()
             )
         }.firstOrNull()
     }
@@ -164,20 +172,20 @@ private class ModelAstClass(
                 for (property in kmClass.properties) {
                     if (method.matches(property)) {
                         return@mapNotNull ModelAstProperty(
-                                this,
-                                element,
-                                method,
-                                property
+                            this,
+                            element,
+                            method,
+                            property
                         )
                     }
                 }
                 for (function in kmClass.functions) {
                     if (method.matches(function)) {
                         return@mapNotNull ModelAstFunction(
-                                this,
-                                element,
-                                method,
-                                function
+                            this,
+                            element,
+                            method,
+                            function
                         )
                     }
                 }
@@ -198,12 +206,12 @@ private class ModelAstClass(
 }
 
 private class ModelAstConstructor(
-        provider: ModelAstProvider,
-        parent: AstClass,
-        override val element: ExecutableElement,
-        private val kmConstructor: KmConstructor?
+    provider: ModelAstProvider,
+    parent: AstClass,
+    override val element: ExecutableElement,
+    private val kmConstructor: KmConstructor?
 ) : AstConstructor(parent),
-        ModelAstElement, ModelAstProvider by provider {
+    ModelAstElement, ModelAstProvider by provider {
 
     override val parameters: List<AstParam> by lazy {
         element.parameters.mapNotNull { element ->
@@ -211,9 +219,9 @@ private class ModelAstConstructor(
                 for (parameter in kmConstructor.valueParameters) {
                     if (element.simpleName.contentEquals(parameter.name)) {
                         return@mapNotNull ModelAstParam(
-                                this,
-                                element,
-                                parameter
+                            this,
+                            element,
+                            parameter
                         )
                     }
                 }
@@ -221,29 +229,37 @@ private class ModelAstConstructor(
             null
         }
     }
+
+    override fun equals(other: Any?): Boolean {
+        return other is ModelAstConstructor && element == other.element
+    }
+
+    override fun hashCode(): Int {
+        return element.hashCode()
+    }
 }
 
 private class ModelAstFunction(
-        provider: ModelAstProvider,
-        val parent: TypeElement,
-        override val element: ExecutableElement,
-        private val kmFunction: KmFunction
+    provider: ModelAstProvider,
+    val parent: TypeElement,
+    override val element: ExecutableElement,
+    private val kmFunction: KmFunction
 ) : AstFunction(),
-        ModelAstMethod, ModelAstProvider by provider {
+    ModelAstMethod, ModelAstProvider by provider {
 
     override val name: String get() = kmFunction.name
 
     override val modifiers: Set<AstModifier> by lazy {
         collectModifiers(
-                kmFunction.flags
+            kmFunction.flags
         )
     }
 
     override val returnType: AstType
         get() = ModelAstType(
-                this,
-                element.returnType,
-                kmFunction.returnType
+            this,
+            element.returnType,
+            kmFunction.returnType
         )
 
     override fun returnTypeFor(enclosingClass: AstClass): AstType {
@@ -251,9 +267,9 @@ private class ModelAstFunction(
         val declaredType = enclosingClass.element.asType() as DeclaredType
         val methodType = types.asMemberOf(declaredType, element) as ExecutableType
         return ModelAstType(
-                this,
-                methodType.returnType,
-                kmFunction.returnType
+            this,
+            methodType.returnType,
+            kmFunction.returnType
         )
     }
 
@@ -267,9 +283,9 @@ private class ModelAstFunction(
             for (parameter in kmFunction.valueParameters) {
                 if (element.simpleName.contentEquals(parameter.name)) {
                     return@mapNotNull ModelAstParam(
-                            this,
-                            element,
-                            parameter
+                        this,
+                        element,
+                        parameter
                     )
                 }
             }
@@ -285,15 +301,23 @@ private class ModelAstFunction(
     override fun asMemberName(): MemberName {
         return MemberName(elements.getPackageOf(element).qualifiedName.toString(), name)
     }
+
+    override fun equals(other: Any?): Boolean {
+        return other is ModelAstFunction && element == other.element
+    }
+
+    override fun hashCode(): Int {
+        return element.hashCode()
+    }
 }
 
 private class ModelAstProperty(
-        provider: ModelAstProvider,
-        val parent: TypeElement,
-        override val element: ExecutableElement,
-        private val kmProperty: KmProperty
+    provider: ModelAstProvider,
+    val parent: TypeElement,
+    override val element: ExecutableElement,
+    private val kmProperty: KmProperty
 ) : AstProperty(),
-        ModelAstMethod, ModelAstProvider by provider {
+    ModelAstMethod, ModelAstProvider by provider {
 
     override val name: String get() = kmProperty.name
 
@@ -311,9 +335,9 @@ private class ModelAstProperty(
 
     override val returnType: AstType
         get() = ModelAstType(
-                this,
-                element.returnType,
-                kmProperty.returnType
+            this,
+            element.returnType,
+            kmProperty.returnType
         )
 
     override fun returnTypeFor(enclosingClass: AstClass): AstType {
@@ -321,9 +345,9 @@ private class ModelAstProperty(
         val declaredType = enclosingClass.element.asType() as DeclaredType
         val methodType = types.asMemberOf(declaredType, element) as ExecutableType
         return ModelAstType(
-                this,
-                methodType.returnType,
-                kmProperty.returnType
+            this,
+            methodType.returnType,
+            kmProperty.returnType
         )
     }
 
@@ -358,14 +382,22 @@ private class ModelAstProperty(
     override fun asMemberName(): MemberName {
         return MemberName(elements.getPackageOf(element).qualifiedName.toString(), name)
     }
+
+    override fun equals(other: Any?): Boolean {
+        return other is ModelAstProperty && element == other.elements
+    }
+
+    override fun hashCode(): Int {
+        return element.hashCode()
+    }
 }
 
 private class ModelAstType(
-        provider: ModelAstProvider,
-        val type: TypeMirror,
-        private val kmType: KmType?
+    provider: ModelAstProvider,
+    val type: TypeMirror,
+    private val kmType: KmType?
 ) : AstType(),
-        ModelAstElement, ModelAstProvider by provider {
+    ModelAstElement, ModelAstProvider by provider {
 
     override val element: Element get() = types.asElement(type)
 
@@ -378,9 +410,9 @@ private class ModelAstType(
             kmType.annotations.map { annotation ->
                 val mirror = provider.elements.getTypeElement(annotation.className.replace('/', '.'))
                 ModelAstAnnotation(
-                        this,
-                        mirror.asType() as DeclaredType,
-                        annotation
+                    this,
+                    mirror.asType() as DeclaredType,
+                    annotation
                 )
             }
         } else {
@@ -406,87 +438,12 @@ private class ModelAstType(
     override fun isUnit(): Boolean = type is NoType
 
     override fun asElement(): AstBasicElement =
-            ModelBasicElement(this, element)
+        ModelBasicElement(this, element)
 
     override fun toAstClass(): AstClass = (element as TypeElement).toAstClass()
 
-    private fun rawTypeName(): ClassName {
-        val kotlinClassName =
-                (kmType?.classifier as? KmClassifier.Class)?.name?.replace('/', '.')?.let { ClassName.bestGuess(it) }
-                        ?: ((type as DeclaredType).asElement() as TypeElement).asClassName()
-        return when (kotlinClassName.canonicalName) {
-            "java.util.Map" -> ClassName("kotlin.collections", "Map")
-            "java.util.Set" -> ClassName("kotlin.collections", "Set")
-            "java.lang.String" -> ClassName("kotlin", "String")
-            else -> kotlinClassName
-        }
-    }
-
     override fun asTypeName(): TypeName {
-        return type.accept(object : SimpleTypeVisitor7<TypeName, Void?>() {
-            override fun visitPrimitive(t: PrimitiveType, p: Void?): TypeName {
-                return when (t.kind) {
-                    TypeKind.BOOLEAN -> BOOLEAN
-                    TypeKind.BYTE -> BYTE
-                    TypeKind.SHORT -> SHORT
-                    TypeKind.INT -> INT
-                    TypeKind.LONG -> LONG
-                    TypeKind.CHAR -> CHAR
-                    TypeKind.FLOAT -> FLOAT
-                    TypeKind.DOUBLE -> DOUBLE
-                    else -> throw AssertionError()
-                }
-            }
-
-            override fun visitDeclared(t: DeclaredType, p: Void?): TypeName {
-                val rawType: ClassName = rawTypeName().copy(nullable = isNullable) as ClassName
-                val enclosingType = t.enclosingType
-                val enclosing = if (enclosingType.kind != TypeKind.NONE &&
-                        Modifier.STATIC !in t.asElement().modifiers
-                )
-                    enclosingType.accept(this, null) else
-                    null
-                if (t.typeArguments.isEmpty() && enclosing !is ParameterizedTypeName) {
-                    return rawType
-                }
-
-                val typeArgumentNames = mutableListOf<TypeName>()
-                for (typeArgument in arguments) {
-                    typeArgumentNames += typeArgument.asTypeName()
-                }
-                return if (enclosing is ParameterizedTypeName)
-                    enclosing.nestedClass(rawType.simpleName, typeArgumentNames) else
-                    rawType.parameterizedBy(typeArgumentNames)
-            }
-
-            override fun visitError(t: ErrorType, p: Void?): TypeName {
-                return visitDeclared(t, p)
-            }
-
-            override fun visitArray(t: ArrayType, p: Void?): ParameterizedTypeName {
-                return ARRAY.parameterizedBy(t.componentType.asTypeName())
-            }
-
-            override fun visitTypeVariable(
-                    t: TypeVariable,
-                    p: Void?
-            ): TypeName {
-                return t.asTypeVariableName()
-            }
-
-            override fun visitWildcard(t: WildcardType, p: Void?): TypeName {
-                return t.asWildcardTypeName()
-            }
-
-            override fun visitNoType(t: NoType, p: Void?): TypeName {
-                if (t.kind == TypeKind.VOID) return UNIT
-                return super.visitUnknown(t, p)
-            }
-
-            override fun defaultAction(e: TypeMirror?, p: Void?): TypeName {
-                throw IllegalArgumentException("Unexpected type mirror: " + e!!)
-            }
-        }, null)
+        return type.asTypeName(kmType)
     }
 
     override fun isAssignableFrom(other: AstType): Boolean {
@@ -502,20 +459,14 @@ private class ModelAstType(
     override fun hashCode(): Int {
         return asTypeName().hashCode()
     }
-
-    val isNullable: Boolean
-        get() {
-            if (kmType == null) return false
-            return Flag.Type.IS_NULLABLE(kmType.flags)
-        }
 }
 
 private class ModelAstAnnotation(
-        provider: ModelAstProvider,
-        val annotationType: DeclaredType,
-        private val kmAnnotation: KmAnnotation
+    provider: ModelAstProvider,
+    val annotationType: DeclaredType,
+    private val kmAnnotation: KmAnnotation
 ) : AstAnnotation(),
-        ModelAstElement, ModelAstProvider by provider {
+    ModelAstElement, ModelAstProvider by provider {
     override val element: Element get() = types.asElement(annotationType)
 
     override fun equals(other: Any?): Boolean {
@@ -530,17 +481,17 @@ private class ModelAstAnnotation(
     override fun toString(): String {
         return "@$annotationType(${
             kmAnnotation.arguments.toList()
-                    .joinToString(separator = ", ") { (name, value) -> "$name=${value.value}" }
+                .joinToString(separator = ", ") { (name, value) -> "$name=${value.value}" }
         })"
     }
 }
 
 private class ModelAstParam(
-        provider: ModelAstProvider,
-        override val element: VariableElement,
-        val kmValueParameter: KmValueParameter
+    provider: ModelAstProvider,
+    override val element: VariableElement,
+    val kmValueParameter: KmValueParameter
 ) : AstParam(),
-        ModelAstElement, ModelAstProvider by provider {
+    ModelAstElement, ModelAstProvider by provider {
 
     override val name: String get() = kmValueParameter.name
 
@@ -577,15 +528,6 @@ private fun collectModifiers(flags: Flags?): Set<AstModifier> {
     return result
 }
 
-private fun String.typeAliasTypeName(): ClassName {
-    val lastDot = lastIndexOf('.')
-    return if (lastDot < 0) {
-        ClassName("", this)
-    } else {
-        ClassName(substring(0, lastDot), substring(lastDot + 1))
-    }
-}
-
 val AstClass.element: TypeElement
     get() {
         require(this is ModelAstClass)
diff --git a/kotlin-inject-compiler-kapt/src/main/kotlin/me/tatarka/inject/compiler/kapt/Util.kt b/kotlin-inject-compiler-kapt/src/main/kotlin/me/tatarka/inject/compiler/kapt/Util.kt
index a8b0887..170fc17 100644
--- a/kotlin-inject-compiler-kapt/src/main/kotlin/me/tatarka/inject/compiler/kapt/Util.kt
+++ b/kotlin-inject-compiler-kapt/src/main/kotlin/me/tatarka/inject/compiler/kapt/Util.kt
@@ -1,16 +1,17 @@
 package me.tatarka.inject.compiler.kapt
 
 import com.squareup.kotlinpoet.*
+import com.squareup.kotlinpoet.ClassName
 import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
-import kotlinx.metadata.KmClassifier
-import kotlinx.metadata.KmFunction
-import kotlinx.metadata.KmProperty
-import kotlinx.metadata.KmType
+import kotlinx.metadata.*
 import kotlinx.metadata.jvm.*
 import javax.lang.model.element.Element
 import javax.lang.model.element.ExecutableElement
+import javax.lang.model.element.Modifier
 import javax.lang.model.element.TypeElement
-import javax.lang.model.type.TypeMirror
+import javax.lang.model.type.*
+import javax.lang.model.util.SimpleTypeVisitor7
+import javax.lang.model.util.TypeKindVisitor8
 
 fun Element.hasAnnotation(className: String): Boolean {
     return annotationMirrors.any { it.annotationType.toString() == className }
@@ -114,47 +115,47 @@ fun ExecutableElement.matches(signature: JvmMethodSignature): Boolean {
     }
     //ex: (Lme/tatarka/inject/test/Bar;)V
     var index = 1 // skip initial (
-    val parameterTypes = mutableListOf<String>()
+    val parameterTypes = mutableListOf<ClassName>()
     val desc = signature.desc
     loop@while (true) {
         when (desc[index]) {
             'L' -> {
                 index++
                 val className = desc.substring(index, desc.indexOf(';', index))
-                parameterTypes.add(className)
+                parameterTypes.add(ClassName.bestGuess(className.replace('/', '.')))
                 index += className.length
             }
             'Z' -> {
                 index++
-                parameterTypes.add("Boolean")
+                parameterTypes.add(BOOLEAN)
             }
             'B' -> {
                 index++
-                parameterTypes.add("Byte")
+                parameterTypes.add(BYTE)
             }
             'C' -> {
                 index++
-                parameterTypes.add("Char")
+                parameterTypes.add(CHAR)
             }
             'S' -> {
                 index++
-                parameterTypes.add("Short")
+                parameterTypes.add(SHORT)
             }
             'I' -> {
                 index++
-                parameterTypes.add("Int")
+                parameterTypes.add(INT)
             }
             'J' -> {
                 index++
-                parameterTypes.add("Long")
+                parameterTypes.add(LONG)
             }
             'F' -> {
                 index++
-                parameterTypes.add("Float")
+                parameterTypes.add(FLOAT)
             }
             'D' -> {
                 index++
-                parameterTypes.add("Double")
+                parameterTypes.add(DOUBLE)
             }
             else -> break@loop
         }
@@ -163,7 +164,7 @@ fun ExecutableElement.matches(signature: JvmMethodSignature): Boolean {
         return false
     }
     for (i in 0 until parameterTypes.size) {
-        if (!parameters[i].asType().matches(parameterTypes[i])) {
+        if (!parameters[i].asType().matches(parameterTypes[i], emptyList())) {
             return false
         }
     }
@@ -173,18 +174,123 @@ fun ExecutableElement.matches(signature: JvmMethodSignature): Boolean {
 private fun TypeMirror.matches(type: KmType): Boolean {
     return when (val classifier = type.classifier) {
         is KmClassifier.Class -> {
-            matches(classifier.name)
+            matches(classifier.asClassName(), type.arguments)
         }
         is KmClassifier.TypeParameter -> false
         is KmClassifier.TypeAlias -> false
     }
 }
 
-private fun TypeMirror.matches(typeName: String): Boolean {
-    return asTypeName().javaToKotlinType().toString() == typeName.replace("/", ".")
+private fun TypeMirror.matches(typeName: ClassName, args: List<KmTypeProjection>): Boolean {
+    return accept(object : TypeKindVisitor8<Boolean, Void?>() {
+        override fun visitDeclared(declaredType: DeclaredType, _p: Void?): Boolean {
+            if ((declaredType.asElement() as TypeElement).asClassName().kotlinClassName() != typeName) {
+                return false
+            }
+            val typeArgs = declaredType.typeArguments
+            if (typeArgs.size != args.size) {
+                return false
+            }
+            for (i in 0 until typeArgs.size) {
+                if (!declaredType.typeArguments[i].matches(args[i].type!!)) {
+                    return false
+                }
+            }
+            return true
+        }
+    }, null)
 }
 
 private fun ExecutableElement.nameMatches(signature: JvmMemberSignature?): Boolean {
     if (signature == null) return false
     return simpleName.contentEquals(signature.name)
 }
+
+fun DeclaredType.rawTypeName(kmType: KmType?): ClassName {
+    val className =
+        (kmType?.classifier as? KmClassifier.Class)?.asClassName()
+            ?: (this.asElement() as TypeElement).asClassName()
+    return className.kotlinClassName()
+}
+
+fun KmClassifier.Class.asClassName() = ClassName.bestGuess(name.replace('/', '.'))
+
+fun ClassName.kotlinClassName(): ClassName {
+    return when (canonicalName) {
+        "java.util.Map" -> ClassName("kotlin.collections", "Map")
+        "java.util.Set" -> ClassName("kotlin.collections", "Set")
+        "java.lang.String" -> ClassName("kotlin", "String")
+        else -> this
+    }
+}
+
+fun TypeMirror.asTypeName(kmType: KmType?): TypeName {
+    return accept(object : SimpleTypeVisitor7<TypeName, Void?>() {
+        override fun visitPrimitive(t: PrimitiveType, p: Void?): TypeName {
+            return when (t.kind) {
+                TypeKind.BOOLEAN -> BOOLEAN
+                TypeKind.BYTE -> BYTE
+                TypeKind.SHORT -> SHORT
+                TypeKind.INT -> INT
+                TypeKind.LONG -> LONG
+                TypeKind.CHAR -> CHAR
+                TypeKind.FLOAT -> FLOAT
+                TypeKind.DOUBLE -> DOUBLE
+                else -> throw AssertionError()
+            }
+        }
+
+        override fun visitDeclared(t: DeclaredType, p: Void?): TypeName {
+            val isNullable = kmType?.let { Flag.Type.IS_NULLABLE(it.flags) } == true
+            val rawType: ClassName = t.rawTypeName(kmType).copy(nullable = isNullable) as ClassName
+            val enclosingType = t.enclosingType
+            val enclosing = if (enclosingType.kind != TypeKind.NONE &&
+                Modifier.STATIC !in t.asElement().modifiers
+            )
+                enclosingType.accept(this, null) else
+                null
+            if (t.typeArguments.isEmpty() && enclosing !is ParameterizedTypeName) {
+                return rawType
+            }
+
+            val typeArgumentNames = mutableListOf<TypeName>()
+            for (i in 0 until t.typeArguments.size) {
+                val typeArg = t.typeArguments[i]
+                val kmArg = kmType?.arguments?.getOrNull(i)?.type
+                typeArgumentNames += typeArg.asTypeName(kmArg)
+            }
+
+            return if (enclosing is ParameterizedTypeName)
+                enclosing.nestedClass(rawType.simpleName, typeArgumentNames) else
+                rawType.parameterizedBy(typeArgumentNames)
+        }
+
+        override fun visitError(t: ErrorType, p: Void?): TypeName {
+            return visitDeclared(t, p)
+        }
+
+        override fun visitArray(t: ArrayType, p: Void?): ParameterizedTypeName {
+            return ARRAY.parameterizedBy(t.componentType.asTypeName())
+        }
+
+        override fun visitTypeVariable(
+            t: TypeVariable,
+            p: Void?
+        ): TypeName {
+            return t.asTypeVariableName()
+        }
+
+        override fun visitWildcard(t: WildcardType, p: Void?): TypeName {
+            return t.asWildcardTypeName()
+        }
+
+        override fun visitNoType(t: NoType, p: Void?): TypeName {
+            if (t.kind == TypeKind.VOID) return UNIT
+            return super.visitUnknown(t, p)
+        }
+
+        override fun defaultAction(e: TypeMirror?, p: Void?): TypeName {
+            throw IllegalArgumentException("Unexpected type mirror: " + e!!)
+        }
+    }, null)
+}
diff --git a/kotlin-inject-compiler-ksp/src/main/kotlin/me/tatarka/inject/compiler/ksp/KSAst.kt b/kotlin-inject-compiler-ksp/src/main/kotlin/me/tatarka/inject/compiler/ksp/KSAst.kt
index 1a0b30c..e8c5752 100644
--- a/kotlin-inject-compiler-ksp/src/main/kotlin/me/tatarka/inject/compiler/ksp/KSAst.kt
+++ b/kotlin-inject-compiler-ksp/src/main/kotlin/me/tatarka/inject/compiler/ksp/KSAst.kt
@@ -45,6 +45,16 @@ interface KSAstProvider : AstProvider {
         }))
     }
 
+    override fun AstElement.toTrace(): String {
+        require(this is KSAstAnnotated)
+        val source = declaration.location as? FileLocation
+        return if (source != null) {
+            "${source.filePath}:${source.lineNumber}: ${toString()}"
+        } else {
+            toString()
+        }
+    }
+
     override fun TypeSpec.Builder.addOriginatingElement(astClass: AstClass): TypeSpec.Builder = this
 }
 
@@ -62,8 +72,9 @@ object KSAstMessenger : Messenger {
     }
 
     fun finalize() {
+        // ksp won't error out if no exception is thrown
         if (errorMessages.isNotEmpty()) {
-            throw IllegalStateException(errorMessages.joinToString(separator = "\n"))
+            throw IllegalStateException()
         }
     }
 
@@ -174,17 +185,18 @@ private class KSAstConstructor(
         declaration.parameters.map { param -> KSAstParam(this, declaration, param) }
     }
 
-    override fun toString(): String = StringBuilder().apply {
-        append(parent.name)
-        append("(")
-        append(parameters.joinToString(", "))
-        append(")")
-    }.toString()
+    override fun equals(other: Any?): Boolean {
+        return other is KSAstConstructor && declaration == other.declaration
+    }
+
+    override fun hashCode(): Int {
+        return declaration.hashCode()
+    }
 }
 
 private class KSAstEmptyConstructor(
     provider: KSAstProvider,
-    parent: AstClass
+    private val parent: AstClass
 ) : AstConstructor(parent), KSAstProvider by provider {
 
     override val parameters: List<AstParam> = emptyList()
@@ -192,6 +204,14 @@ private class KSAstEmptyConstructor(
     override fun hasAnnotation(className: String): Boolean = false
 
     override fun typeAnnotatedWith(className: String): AstClass? = null
+
+    override fun equals(other: Any?): Boolean {
+        return other is KSAstEmptyConstructor && parent == other.parent
+    }
+
+    override fun hashCode(): Int {
+        return parent.hashCode()
+    }
 }
 
 private class KSAstFunction(provider: KSAstProvider, override val declaration: KSFunctionDeclaration) : AstFunction(),
@@ -228,13 +248,13 @@ private class KSAstFunction(provider: KSAstProvider, override val declaration: K
         return MemberName(packageName, declaration.simpleName.toString())
     }
 
-    override fun toString(): String = StringBuilder().apply {
-        append("fun ")
-        append(name)
-        append("(")
-        append(parameters.joinToString(", "))
-        append(")")
-    }.toString()
+    override fun equals(other: Any?): Boolean {
+        return other is KSAstFunction && declaration == other.declaration
+    }
+
+    override fun hashCode(): Int {
+        return declaration.hashCode()
+    }
 }
 
 private class KSAstProperty(provider: KSAstProvider, override val declaration: KSPropertyDeclaration) : AstProperty(),
@@ -266,6 +286,14 @@ private class KSAstProperty(provider: KSAstProvider, override val declaration: K
         val packageName = declaration.qualifiedName?.getQualifier().orEmpty()
         return MemberName(packageName, declaration.simpleName.toString())
     }
+
+    override fun equals(other: Any?): Boolean {
+        return other is KSAstProperty && declaration == other.declaration
+    }
+
+    override fun hashCode(): Int {
+        return declaration.hashCode()
+    }
 }
 
 private class KSAstType(provider: KSAstProvider) : AstType(), KSAstAnnotated, KSAstProvider by provider {


Test patch:
diff --git a/compiler-tests/src/test/kotlin/me/tatarka/inject/test/FailureTest.kt b/compiler-tests/src/test/kotlin/me/tatarka/inject/test/FailureTest.kt
index caaa0e1..7404fd7 100644
--- a/compiler-tests/src/test/kotlin/me/tatarka/inject/test/FailureTest.kt
+++ b/compiler-tests/src/test/kotlin/me/tatarka/inject/test/FailureTest.kt
@@ -1,6 +1,5 @@
 package me.tatarka.inject.test
 
-import assertk.all
 import assertk.assertThat
 import assertk.assertions.contains
 import assertk.assertions.isFailure
@@ -54,33 +53,35 @@ class FailureTest(private val target: Target) {
     fun fails_if_component_is_not_abstract() {
         assertThat {
             projectCompiler.source(
-                "test.kt", """
+                "MyComponent.kt", """
                     import me.tatarka.inject.annotations.Component
                     
                     @Component class MyComponent
                 """.trimIndent()
             ).compile()
-        }.isFailure().output().contains("@Component class: MyComponent must be abstract")
+        }.isFailure().output()
+            .contains("@Component class: MyComponent must be abstract", "MyComponent.${target.sourceExt()}")
     }
 
     @Test
     fun fails_if_component_is_private() {
         assertThat {
             projectCompiler.source(
-                "test.kt", """
+                "MyComponent.kt", """
                     import me.tatarka.inject.annotations.Component
                     
                     @Component private abstract class MyComponent
                 """.trimIndent()
             ).compile()
-        }.isFailure().output().contains("@Component class: MyComponent must not be private")
+        }.isFailure().output()
+            .contains("@Component class: MyComponent must not be private", "MyComponent.${target.sourceExt()}")
     }
 
     @Test
     fun fails_if_provides_is_private() {
         assertThat {
             projectCompiler.source(
-                "test.kt", """
+                "MyComponent.kt", """
                     import me.tatarka.inject.annotations.Component
                     import me.tatarka.inject.annotations.Provides
                     
@@ -89,14 +90,14 @@ class FailureTest(private val target: Target) {
                     }
                 """.trimIndent()
             ).compile()
-        }.isFailure().output().contains("@Provides method must not be private")
+        }.isFailure().output().contains("@Provides method must not be private", "MyComponent.${target.sourceExt()}")
     }
 
     @Test
     fun fails_if_provides_is_abstract() {
         assertThat {
             projectCompiler.source(
-                "test.kt", """
+                "MyComponent.kt", """
                     import me.tatarka.inject.annotations.Component
                     import me.tatarka.inject.annotations.Provides
                     
@@ -105,14 +106,15 @@ class FailureTest(private val target: Target) {
                     }
                 """.trimIndent()
             ).compile()
-        }.isFailure().output().contains("@Provides method must have a concrete implementation")
+        }.isFailure().output()
+            .contains("@Provides method must have a concrete implementation", "MyComponent.${target.sourceExt()}")
     }
 
     @Test
     fun fails_if_provides_returns_unit() {
         assertThat {
             projectCompiler.source(
-                "test.kt", """
+                "MyComponent.kt", """
                     import me.tatarka.inject.annotations.Component
                     import me.tatarka.inject.annotations.Provides
                     
@@ -121,14 +123,14 @@ class FailureTest(private val target: Target) {
                     }
                 """.trimIndent()
             ).compile()
-        }.isFailure().output().contains("@Provides method must return a value")
+        }.isFailure().output().contains("@Provides method must return a value", "MyComponent.${target.sourceExt()}")
     }
 
     @Test
     fun fails_if_the_same_type_is_provided_more_than_once() {
         assertThat {
             projectCompiler.source(
-                "test.kt", """
+                "MyComponent.kt", """
                     import me.tatarka.inject.annotations.Component
                     import me.tatarka.inject.annotations.Provides
                     
@@ -138,17 +140,15 @@ class FailureTest(private val target: Target) {
                     }
                 """.trimIndent()
             ).compile()
-        }.isFailure().output().all {
-            contains("Cannot provide: String")
-            contains("as it is already provided")
-        }
+        }.isFailure().output()
+            .contains("Cannot provide: String", "as it is already provided", "MyComponent.${target.sourceExt()}")
     }
 
     @Test
     fun fails_if_type_cannot_be_provided() {
         assertThat {
             projectCompiler.source(
-                "test.kt", """
+                "MyComponent.kt", """
                     import me.tatarka.inject.annotations.Component
                     
                     @Component abstract class MyComponent {
@@ -156,17 +156,15 @@ class FailureTest(private val target: Target) {
                     }
                 """.trimIndent()
             ).compile()
-        }.isFailure().output().all {
-            contains("Cannot find an @Inject constructor or provider for: String")
-            contains("provideString()")
-        }
+        }.isFailure().output()
+            .contains("Cannot find an @Inject constructor or provider for: String", "MyComponent.${target.sourceExt()}")
     }
 
     @Test
     fun fails_if_type_cannot_be_provided_to_constructor() {
         assertThat {
             projectCompiler.source(
-                    "test.kt", """
+                "MyComponent.kt", """
                     import me.tatarka.inject.annotations.Component
                     import me.tatarka.inject.annotations.Inject
                     
@@ -177,18 +175,14 @@ class FailureTest(private val target: Target) {
                     }
                 """.trimIndent()
             ).compile()
-        }.isFailure().output().all {
-            contains("Cannot find an @Inject constructor or provider for: String")
-            contains("Foo")
-            contains("bar")
-        }
+        }.isFailure().output().contains("Cannot find an @Inject constructor or provider for: String")
     }
 
     @Test
     fun fails_if_type_cannot_be_provided_to_provides() {
         assertThat {
             projectCompiler.source(
-                    "test.kt", """
+                "MyComponent.kt", """
                     import me.tatarka.inject.annotations.Component
                     import me.tatarka.inject.annotations.Provides
                     
@@ -201,17 +195,15 @@ class FailureTest(private val target: Target) {
                     }
                 """.trimIndent()
             ).compile()
-        }.isFailure().output().all {
-            contains("Cannot find an @Inject constructor or provider for: String")
-            contains("bar")
-        }
+        }.isFailure().output()
+            .contains("Cannot find an @Inject constructor or provider for: String", "MyComponent.${target.sourceExt()}")
     }
 
     @Test
     fun fails_if_component_does_not_have_scope_to_provide_dependency() {
         assertThat {
             projectCompiler.source(
-                "test.kt", """
+                "MyComponent.kt", """
                     import me.tatarka.inject.annotations.Component
                     import me.tatarka.inject.annotations.Scope
                     import me.tatarka.inject.annotations.Inject
@@ -224,6 +216,33 @@ class FailureTest(private val target: Target) {
                     }
                 """.trimIndent()
             ).compile()
-        }.isFailure().output().contains("Cannot find component with scope: @MyScope to inject Foo")
+        }.isFailure().output()
+            .contains("Cannot find component with scope: @MyScope to inject Foo", "MyComponent.${target.sourceExt()}")
+    }
+
+    @Test
+    fun fails_if_simple_cycle_is_detected() {
+        assertThat {
+            projectCompiler.source(
+                "MyComponent.kt", """
+                    import me.tatarka.inject.annotations.Component
+                    import me.tatarka.inject.annotations.Inject
+                    
+                    @Inject class A(val b: B)
+                    @Inject class B(val a: A)
+                    
+                    @Component abstract class MyComponent {
+                        abstract val a: A
+                    }
+                """.trimIndent()
+            ).compile()
+        }.isFailure().output().contains(
+            "Cycle detected", "B(a: A)", "A(b: B)"
+        )
     }
+}
+
+private fun Target.sourceExt() = when (this) {
+    Target.kapt -> "java"
+    Target.ksp -> "kt"
 }
\ No newline at end of file
diff --git a/integration-tests-kapt/src/test/kotlin/me/tatarka/inject/test/InjectFunctionTest.kt b/integration-tests-kapt/src/test/kotlin/me/tatarka/inject/test/InjectFunctionTest.kt
index 8945275..dea63ed 100644
--- a/integration-tests-kapt/src/test/kotlin/me/tatarka/inject/test/InjectFunctionTest.kt
+++ b/integration-tests-kapt/src/test/kotlin/me/tatarka/inject/test/InjectFunctionTest.kt
@@ -29,4 +29,6 @@ class InjectFunctionTest {
         assertThat(component.bar()).isEqualTo("test")
     }
 
-}
\ No newline at end of file
+}
+
+
diff --git a/integration-tests/src/test/kotlin/me/tatarka/inject/test/CycleErrorTest.kt b/integration-tests/src/test/kotlin/me/tatarka/inject/test/CycleErrorTest.kt
new file mode 100644
index 0000000..22d2127
--- /dev/null
+++ b/integration-tests/src/test/kotlin/me/tatarka/inject/test/CycleErrorTest.kt
@@ -0,0 +1,10 @@
+package me.tatarka.inject.test
+
+import me.tatarka.inject.annotations.Inject
+
+@Inject class A(val b: B)
+@Inject class B(val a: A)
+
+//@Component abstract class MyComponent {
+//    abstract val a: A
+//}
diff --git a/integration-tests/src/test/kotlin/me/tatarka/inject/test/LazyComponentTest.kt b/integration-tests/src/test/kotlin/me/tatarka/inject/test/LazyComponentTest.kt
index 0d2805a..32eaf22 100644
--- a/integration-tests/src/test/kotlin/me/tatarka/inject/test/LazyComponentTest.kt
+++ b/integration-tests/src/test/kotlin/me/tatarka/inject/test/LazyComponentTest.kt
@@ -2,16 +2,26 @@ package me.tatarka.inject.test
 
 import assertk.assertThat
 import assertk.assertions.isSameAs
-import me.tatarka.inject.annotations.Inject
 import me.tatarka.inject.annotations.Component
+import me.tatarka.inject.annotations.Inject
+import me.tatarka.inject.annotations.Provides
 import kotlin.test.Test
 
 @Inject class LazyFoo
 
+class LazyBar(val foo: Lazy<LazyFoo>)
+
 @Component abstract class LazyComponent {
     abstract val foo: Lazy<LazyFoo>
 }
 
+@Component abstract class LazyProvidesComponent {
+    abstract val bar: LazyBar
+
+    @Provides
+    fun bar(foo: Lazy<LazyFoo>) = LazyBar(foo)
+}
+
 class LazyComponentTest {
 
     @Test
@@ -22,4 +32,13 @@ class LazyComponentTest {
 
         assertThat(foo.value).isSameAs(foo.value)
     }
+
+    @Test
+    fun generates_a_component_that_provides_a_lazy_dep_to_a_provides() {
+        val component = LazyProvidesComponent::class.create()
+
+        val bar = component.bar
+
+        assertThat(bar.foo.value).isSameAs(bar.foo.value)
+    }
 }
diff --git a/integration-tests/src/test/kotlin/me/tatarka/inject/test/RecursiveTest.kt b/integration-tests/src/test/kotlin/me/tatarka/inject/test/RecursiveTest.kt
new file mode 100644
index 0000000..c5fbdf9
--- /dev/null
+++ b/integration-tests/src/test/kotlin/me/tatarka/inject/test/RecursiveTest.kt
@@ -0,0 +1,104 @@
+package me.tatarka.inject.test
+
+import assertk.assertThat
+import assertk.assertions.isSameAs
+import me.tatarka.inject.annotations.Inject
+import kotlin.reflect.KClass
+import kotlin.test.Test
+
+class LazyCycleFoo(val bar: LazyCycleBar)
+
+class LazyCycleBar(val foo: Lazy<LazyCycleFoo>)
+
+class FBar(val foo: () -> FFoo)
+
+class FFoo(val bar: FBar)
+
+@Inject
+class CycleFoo(val bar: CycleBar)
+
+@Inject
+class CycleBar(val foo: () -> CycleFoo)
+
+abstract class CycleComponent {
+    abstract val bar: CycleBar
+}
+
+//TODO: actually generate this
+class InjectCycleComponent : CycleComponent() {
+    override val bar: CycleBar
+        get() {
+            lateinit var bar: CycleBar
+            return CycleBar { CycleFoo(bar) }.also { bar = it }
+        }
+}
+
+fun KClass<CycleComponent>.create() = InjectCycleComponent()
+
+abstract class LazyCycleComponent {
+    abstract val bar: LazyCycleBar
+
+    fun bar(foo: Lazy<LazyCycleFoo>) = LazyCycleBar(foo)
+
+    fun foo(bar: LazyCycleBar) = LazyCycleFoo(bar)
+}
+
+//TODO: actually generate this
+class InjectLazyCycleComponent : LazyCycleComponent() {
+    override val bar: LazyCycleBar
+        get() {
+            lateinit var bar: LazyCycleBar
+            return bar(lazy { foo(bar) }).also { bar = it }
+        }
+}
+
+fun KClass<LazyCycleComponent>.create() = InjectLazyCycleComponent()
+
+abstract class FunctionCycleComponent {
+    abstract val bar: FBar
+
+    // scoped?
+    fun bar(foo: () -> FFoo) = FBar(foo)
+
+    // scoped?
+    fun foo(bar: FBar) = FFoo(bar)
+}
+
+//TODO: actually generate this
+class InjectFunctionCycleComponent : FunctionCycleComponent() {
+    override val bar: FBar
+        get() {
+            lateinit var bar: FBar
+            return bar { foo(bar) }.also { bar = it }
+        }
+}
+
+fun KClass<FunctionCycleComponent>.create() = InjectFunctionCycleComponent()
+
+class RecursiveTest {
+
+    @Test
+    fun generates_a_component_that_provides_data_recursively() {
+        val component = CycleComponent::class.create()
+        val bar = component.bar
+
+        assertThat(bar).isSameAs(bar.foo().bar)
+    }
+
+    @Test
+    fun generates_a_component_that_provides_a_lazy_dea_recursively() {
+        val component = LazyCycleComponent::class.create()
+        val bar = component.bar
+
+        assertThat(bar).isSameAs(bar.foo.value.bar)
+    }
+
+    @Test
+    fun generates_a_component_that_provides_a_function_dea_recursively() {
+        val component = FunctionCycleComponent::class.create()
+        val bar = component.bar
+
+        assertThat(bar).isSameAs(bar.foo().bar)
+    }
+}
+