Repository: yandex/yatagan (kotlin)

Issue:
Allow for @Builder's factory methods to return supertypes
In Yatagan component's factory must return type of the component. It will be nice to support the same possibility in Yatagan
```kotlin
interface PublicComponent

@Component
internal interface ComponentInternal : PublicComponent {

    @Component.Builder
    interface Factory {

        // Work in Dagger. Yatagan doesn't work
        fun create(): PublicComponent
    }
}
```

```kotlin
interface PublicComponent

@Component
internal interface ComponentInternal : PublicComponent {

    @Component.Builder
    interface Factory {

        // Both Dagger and Yatagan work
        fun create(): ComponentInternal
    }
}
```

Pull request:
Dagger allows `create(): ComponentBase` in `DerivedComponent`'s `Builder`.
 this allows to inherit such `create()` methods from builder's superinterfaces.

Fixes #40

Patch:
diff --git a/core/model/impl/src/main/kotlin/ComponentFactoryWithBuilderModelImpl.kt b/core/model/impl/src/main/kotlin/ComponentFactoryWithBuilderModelImpl.kt
index ac173dd4..e9219721 100644
--- a/core/model/impl/src/main/kotlin/ComponentFactoryWithBuilderModelImpl.kt
+++ b/core/model/impl/src/main/kotlin/ComponentFactoryWithBuilderModelImpl.kt
@@ -25,6 +25,7 @@ import com.yandex.yatagan.core.model.HasNodeModel
 import com.yandex.yatagan.core.model.NodeModel
 import com.yandex.yatagan.lang.BuiltinAnnotation
 import com.yandex.yatagan.lang.LangModelFactory
+import com.yandex.yatagan.lang.Method
 import com.yandex.yatagan.lang.Type
 import com.yandex.yatagan.lang.TypeDeclaration
 import com.yandex.yatagan.lang.TypeDeclarationKind
@@ -40,14 +41,16 @@ import com.yandex.yatagan.validation.format.reportWarning
 internal class ComponentFactoryWithBuilderModelImpl private constructor(
     private val factoryDeclaration: TypeDeclaration,
 ) : ComponentFactoryModelBase(), ComponentFactoryWithBuilderModel {
+    private val methodParser by lazy { MethodParser() }
+
     override val createdComponent: ComponentModel by lazy {
         val declaration = factoryDeclaration.enclosingType
             ?: LangModelFactory.createNoType("missing-component-type").declaration
         ComponentModelImpl(declaration)
     }
 
-    override val factoryMethod = factoryDeclaration.methods.find {
-        it.isAbstract && it.returnType == createdComponent.type
+    override val factoryMethod by lazy {
+        methodParser.factoryMethods.maxOrNull()
     }
 
     override val type: Type
@@ -62,47 +65,8 @@ internal class ComponentFactoryWithBuilderModelImpl private constructor(
         return visitor.visitComponentFactory(this)
     }
 
-    override val builderInputs: Collection<BuilderInputModel> = factoryDeclaration.methods.filter {
-        it.isAbstract && it != factoryMethod && it.parameters.count() == 1
-    }.map { method ->
-        object : BuilderInputModel {
-            override val payload: ComponentFactoryModel.InputPayload by lazy(LazyThreadSafetyMode.PUBLICATION) {
-                InputPayload(
-                    param = method.parameters.first(),
-                    hasBindsInstance = { method.getAnnotation(BuiltinAnnotation.BindsInstance) != null },
-                )
-            }
-            override val name get() = method.name
-            override val builderSetter get() = method
-
-            override fun validate(validator: Validator) {
-                validator.child(payload)
-                if (method.parameters.first().getAnnotation(BuiltinAnnotation.BindsInstance) != null) {
-                    validator.reportWarning(Strings.Warnings.ignoredBindsInstance())
-                }
-                if (!method.returnType.isVoid && !method.returnType.isAssignableFrom(factoryDeclaration.asType())) {
-                    validator.reportError(Strings.Errors.invalidBuilderSetterReturn(
-                        creatorType = factoryDeclaration.asType(),
-                    ))
-                }
-            }
-
-            override fun toString(childContext: MayBeInvalid?) = modelRepresentation(
-                modelClassName = "creator-setter",
-                representation = {
-                    append("$name(")
-                    if (childContext != null) {
-                        appendChildContextReference(reference = method.parameters.first())
-                    } else {
-                        append(method.parameters.first())
-                    }
-                    append("): ")
-                    append(method.returnType)
-                }
-            )
-        }
-    }.toList()
-
+    override val builderInputs: Collection<BuilderInputModel>
+        get() = methodParser.builderInputs
 
     override fun toString(childContext: MayBeInvalid?) = modelRepresentation(
         modelClassName = "component-creator",
@@ -118,17 +82,89 @@ internal class ComponentFactoryWithBuilderModelImpl private constructor(
             validator.reportError(Strings.Errors.nonInterfaceCreator())
         }
 
-        for (method in factoryDeclaration.methods) {
-            if (method == factoryMethod || method.parameters.count() == 1 || !method.isAbstract)
-                continue
+        for (method in methodParser.unknown) {
             validator.reportError(Strings.Errors.unknownMethodInCreator(method = method))
         }
+
+        if (methodParser.factoryMethods.size > 1) {
+            validator.reportError(Strings.Errors.duplicateFactoryMethodsInACreator()) {
+                for (method in methodParser.factoryMethods.sorted()) {
+                    addNote(Strings.Notes.conflictingFactory(method))
+                }
+            }
+        }
     }
 
     override fun <R> accept(visitor: ComponentFactoryModel.Visitor<R>): R {
         return visitor.visitWithBuilder(this);
     }
 
+    private inner class MethodParser {
+        val factoryMethods = arrayListOf<Method>()
+        val builderInputs = arrayListOf<BuilderInputModelImpl>()
+        val unknown = arrayListOf<Method>()
+
+        init {
+            for (method in factoryDeclaration.methods) {
+                when {
+                    // Non-abstract method - skip
+                    !method.isAbstract -> continue
+
+                    // Factory method
+                    method.returnType.isAssignableFrom(createdComponent.type) -> {
+                        factoryMethods += method
+                    }
+
+                    // Builder setter
+                    method.parameters.count() == 1 -> {
+                        builderInputs += BuilderInputModelImpl(method)
+                    }
+
+                    else -> {
+                        unknown += method
+                    }
+                }
+            }
+        }
+    }
+
+    private inner class BuilderInputModelImpl(private val method: Method) : BuilderInputModel {
+        override val payload: ComponentFactoryModel.InputPayload by lazy(LazyThreadSafetyMode.PUBLICATION) {
+            InputPayload(
+                param = method.parameters.first(),
+                hasBindsInstance = { method.getAnnotation(BuiltinAnnotation.BindsInstance) != null },
+            )
+        }
+        override val name get() = method.name
+        override val builderSetter get() = method
+
+        override fun validate(validator: Validator) {
+            validator.child(payload)
+            if (method.parameters.first().getAnnotation(BuiltinAnnotation.BindsInstance) != null) {
+                validator.reportWarning(Strings.Warnings.ignoredBindsInstance())
+            }
+            if (!method.returnType.isVoid && !method.returnType.isAssignableFrom(factoryDeclaration.asType())) {
+                validator.reportError(Strings.Errors.invalidBuilderSetterReturn(
+                    creatorType = factoryDeclaration.asType(),
+                ))
+            }
+        }
+
+        override fun toString(childContext: MayBeInvalid?) = modelRepresentation(
+            modelClassName = "creator-setter",
+            representation = {
+                append("$name(")
+                if (childContext != null) {
+                    appendChildContextReference(reference = method.parameters.first())
+                } else {
+                    append(method.parameters.first())
+                }
+                append("): ")
+                append(method.returnType)
+            }
+        )
+    }
+
     companion object Factory : ObjectCache<TypeDeclaration, ComponentFactoryWithBuilderModelImpl>() {
         operator fun invoke(
             factoryDeclaration: TypeDeclaration,
diff --git a/core/model/impl/src/main/kotlin/ComponentModelImpl.kt b/core/model/impl/src/main/kotlin/ComponentModelImpl.kt
index 2ac2dfb7..9fd75980 100644
--- a/core/model/impl/src/main/kotlin/ComponentModelImpl.kt
+++ b/core/model/impl/src/main/kotlin/ComponentModelImpl.kt
@@ -100,7 +100,9 @@ internal class ComponentModelImpl private constructor(
 
     override val factory: ComponentFactoryWithBuilderModel? by lazy {
         declaration.nestedClasses
-            .find { ComponentFactoryWithBuilderModelImpl.canRepresent(it) }?.let {
+            .filter { ComponentFactoryWithBuilderModelImpl.canRepresent(it) }
+            .maxOrNull()
+            ?.let {
                 ComponentFactoryWithBuilderModelImpl(factoryDeclaration = it)
             }
     }
diff --git a/lang/rt/src/main/kotlin/reflectionUtils.kt b/lang/rt/src/main/kotlin/reflectionUtils.kt
index ca05db29..ed1472c9 100644
--- a/lang/rt/src/main/kotlin/reflectionUtils.kt
+++ b/lang/rt/src/main/kotlin/reflectionUtils.kt
@@ -288,6 +288,7 @@ fun Class<*>.boxed(): Class<*> {
         java.lang.Long.TYPE -> java.lang.Long::class.java
         java.lang.Float.TYPE -> java.lang.Float::class.java
         java.lang.Double.TYPE -> java.lang.Double::class.java
+        java.lang.Void.TYPE -> java.lang.Void::class.java
         else -> throw AssertionError("Not reached")
     }
 }
diff --git a/validation/format/src/main/kotlin/Strings.kt b/validation/format/src/main/kotlin/Strings.kt
index 3d09b4d5..5973491f 100644
--- a/validation/format/src/main/kotlin/Strings.kt
+++ b/validation/format/src/main/kotlin/Strings.kt
@@ -219,6 +219,10 @@ object Strings {
         fun multipleCreators() =
             "Multiple component factories detected declared".toError()
 
+        @Covered
+        fun duplicateFactoryMethodsInACreator() =
+            "Multiple factory methods declared in a component creator".toError()
+
         @Covered
         fun unknownMethodInComponent(method: Method) = buildRichString {
             color = TextColor.Inherit
@@ -676,12 +680,19 @@ object Strings {
 
         @Covered
         fun factoryMethodDeclaredHere(factory: ComponentFactoryModel) = buildRichString {
+            color = TextColor.Inherit
             append("Factory method declared here: ").append(factory)
         }.toNote()
 
         @Covered
         fun duplicateChildComponentFactory(factory: ComponentFactoryModel) = buildRichString {
+            color = TextColor.Inherit
             append("Duplicate declared as `").append(factory).append('`')
         }.toNote()
+
+        fun conflictingFactory(factoryMethod: Method) = buildRichString {
+            color = TextColor.Inherit
+            append("Duplicate factory: `").append(factoryMethod).append('`')
+        }.toNote()
     }
 }


Test patch:
diff --git a/testing/tests/src/test/kotlin/ComponentCreatorFailureTest.kt b/testing/tests/src/test/kotlin/ComponentCreatorFailureTest.kt
index 0d5ffb56..48280a80 100644
--- a/testing/tests/src/test/kotlin/ComponentCreatorFailureTest.kt
+++ b/testing/tests/src/test/kotlin/ComponentCreatorFailureTest.kt
@@ -160,6 +160,16 @@ class ComponentCreatorFailureTest(
                 @Component.Builder interface Builder1 { fun create(): WithTwoBuilder }
                 @Component.Builder interface Builder2 { fun create(): WithTwoBuilder }
             }
+
+            interface Base
+
+            @Component
+            interface BuilderWithTwoCreate : Base {
+                @Component.Builder interface Builder { 
+                    fun create1(): BuilderWithTwoCreate
+                    fun create2(): Base
+                }
+            }
         """.trimIndent())
 
         compileRunAndValidate()
diff --git a/testing/tests/src/test/kotlin/ComponentHierarchyKotlinTest.kt b/testing/tests/src/test/kotlin/ComponentHierarchyKotlinTest.kt
index bba9d02f..1776c19d 100644
--- a/testing/tests/src/test/kotlin/ComponentHierarchyKotlinTest.kt
+++ b/testing/tests/src/test/kotlin/ComponentHierarchyKotlinTest.kt
@@ -512,6 +512,12 @@ class ComponentHierarchyKotlinTest(
             import com.yandex.yatagan.*
             import javax.inject.*
 
+            interface ComponentBase {
+                interface Builder {
+                    fun create(): ComponentBase
+                }
+            }
+
             @Module class MyModule {
                 @get:Provides val i: Int = 2
             }
@@ -535,8 +541,8 @@ class ComponentHierarchyKotlinTest(
                 val i: Int
             }
 
-            @Component interface ComponentBaz {
-                @Component.Builder interface Builder { fun create(): ComponentBaz }
+            @Component interface ComponentBaz : ComponentBase {
+                @Component.Builder interface Builder : ComponentBase.Builder
             }
             @Component.Builder interface Builder { fun create(): ComponentBaz }
             @Component(isRoot = false) interface ComponentQu {
diff --git a/testing/tests/src/test/resources/golden/ComponentCreatorFailureTest/invalid-component-creator.golden.txt b/testing/tests/src/test/resources/golden/ComponentCreatorFailureTest/invalid-component-creator.golden.txt
index 496082fb..f55589f5 100644
--- a/testing/tests/src/test/resources/golden/ComponentCreatorFailureTest/invalid-component-creator.golden.txt
+++ b/testing/tests/src/test/resources/golden/ComponentCreatorFailureTest/invalid-component-creator.golden.txt
@@ -46,6 +46,15 @@ Encountered:
         ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
+error: Multiple factory methods declared in a component creator
+NOTE: Duplicate factory: `test.BuilderWithTwoCreate.Builder::create2(): test.Base`
+NOTE: Duplicate factory: `test.BuilderWithTwoCreate.Builder::create1(): test.BuilderWithTwoCreate`
+Encountered:
+  in graph for root-component test.BuilderWithTwoCreate
+  here: component-creator test.BuilderWithTwoCreate.Builder
+        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
 error: Setter method in component creator must return either `void` or creator type itself (`test.MyComponent.Foo`)
 Encountered:
   in graph for root-component test.MyComponent