Repository: evant/kotlin-inject (kotlin)

Issue:
Propigate default arguments to the create method.
If you have
```kotlin
@Component abstract class MyComponent(val foo: String = "Foo")
```
then it should generate
```kotlin
fun KClass<MyComponent>.create(foo: String = "Foo") = InjectMyComponent(foo)
```

https://github.com/evant/kotlin-inject/issues/49#issuecomment-682125868

Pull request description:
Due to a limitation in kapt/ksp we can't read default values to declare them
ourselves. This means we need to generate create/constructor for each set of
params we want to pass. For now we only create 2, one with all the params and
one with the default params omitted.

Fixes #50

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 fb8f0706..487734f3 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
@@ -195,6 +195,8 @@ abstract class AstParam : AstElement() {
 
     abstract val isPrivate: Boolean
 
+    abstract val hasDefault: Boolean
+
     abstract fun asParameterSpec(): ParameterSpec
 
     override fun toString(): String {
@@ -205,7 +207,4 @@ abstract class AstParam : AstElement() {
 interface AstHasModifiers {
     val isPrivate: Boolean
     val isAbstract: Boolean
-}
-
-fun ParameterSpec.Companion.parametersOf(constructor: AstConstructor) =
-    constructor.parameters.map { it.asParameterSpec() }
+}
\ No newline at end of file
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 2719f456..e4c491f9 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
@@ -1,15 +1,7 @@
 package me.tatarka.inject.compiler
 
-import com.squareup.kotlinpoet.ClassName
-import com.squareup.kotlinpoet.CodeBlock
-import com.squareup.kotlinpoet.FileSpec
-import com.squareup.kotlinpoet.FunSpec
-import com.squareup.kotlinpoet.KModifier
-import com.squareup.kotlinpoet.ParameterSpec
+import com.squareup.kotlinpoet.*
 import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.plusParameter
-import com.squareup.kotlinpoet.PropertySpec
-import com.squareup.kotlinpoet.TypeSpec
-import com.squareup.kotlinpoet.asClassName
 import me.tatarka.inject.annotations.Component
 import me.tatarka.inject.annotations.Inject
 import me.tatarka.inject.annotations.Provides
@@ -39,7 +31,9 @@ class InjectGenerator(provider: AstProvider, private val options: Options) :
 
         return FileSpec.builder(astClass.packageName, "Inject${astClass.name}")
             .addType(injectComponent)
-            .addFunction(createFunction)
+            .apply {
+                createFunction.forEach { addFunction(it) }
+            }
             .build()
     }
 
@@ -65,20 +59,39 @@ class InjectGenerator(provider: AstProvider, private val options: Options) :
 
                 if (constructor != null) {
                     val funSpec = FunSpec.constructorBuilder()
-                    for (parameter in constructor.parameters) {
-                        if (parameter.isComponent()) {
-                            if (!parameter.isVal) {
-                                error("@Component parameter: ${parameter.name} must be val", parameter)
-                            } else if (parameter.isPrivate) {
-                                error("@Component parameter: ${parameter.name} must not be private", parameter)
+                    val params = constructor.parameters
+                    for (param in params) {
+                        if (param.isComponent()) {
+                            if (!param.isVal) {
+                                error("@Component parameter: ${param.name} must be val", param)
+                            } else if (param.isPrivate) {
+                                error("@Component parameter: ${param.name} must not be private", param)
                             }
                         }
-
-                        val p = parameter.asParameterSpec()
-                        funSpec.addParameter(p)
-                        addSuperclassConstructorParameter("%N", p)
                     }
-                    primaryConstructor(funSpec.build())
+                    val paramSpecs = params.map { it.asParameterSpec() }
+                    val nonDefaultParamSpecs =
+                        constructor.parameters.filter { !it.hasDefault }.map { it.asParameterSpec() }
+                    if (paramSpecs.size == nonDefaultParamSpecs.size) {
+                        for (p in paramSpecs) {
+                            funSpec.addParameter(p)
+                            addSuperclassConstructorParameter("%N", p)
+                        }
+                        primaryConstructor(funSpec.build())
+                    } else {
+                        addFunction(
+                            FunSpec.constructorBuilder()
+                                .addParameters(paramSpecs)
+                                .callSuperConstructor(paramSpecs.map { CodeBlock.of("%N", it) })
+                                .build()
+                        )
+                        addFunction(
+                            FunSpec.constructorBuilder()
+                                .addParameters(nonDefaultParamSpecs)
+                                .callSuperConstructor(nonDefaultParamSpecs.map { CodeBlock.of("%1N = %1N", it) })
+                                .build()
+                        )
+                    }
                 }
 
                 try {
@@ -150,45 +163,84 @@ class InjectGenerator(provider: AstProvider, private val options: Options) :
         element: AstClass,
         constructor: AstConstructor?,
         injectComponent: TypeSpec
-    ): FunSpec {
-        val typeName = element.type.asTypeName()
-        return FunSpec.builder("create")
-            .apply {
-                if (constructor != null) {
-                    addParameters(ParameterSpec.parametersOf(constructor))
-                }
-                if (options.generateCompanionExtensions) {
-                    val companion = element.companion
+    ): List<FunSpec> {
+        fun generateCreate(
+            typeName: TypeName,
+            constructor: AstConstructor?,
+            injectComponent: TypeSpec,
+            companion: AstClass?,
+            params: List<AstParam>
+        ): FunSpec {
+            return FunSpec.builder("create")
+                .apply {
+                    if (constructor != null) {
+                        for (param in params) {
+                            addParameter(param.asParameterSpec())
+                        }
+                    }
                     if (companion != null) {
                         receiver(companion.type.asTypeName())
                     } else {
-                        error(
-                            """Missing companion for class: ${element.asClassName()}.
-                            |When you have the option me.tatarka.inject.generateCompanionExtensions=true you must declare a companion option on the component class for the extension function to apply to.
-                            |You can do so by adding 'companion object' to the class.
-                        """.trimMargin(), element
-                        )
+                        receiver(KClass::class.asClassName().plusParameter(typeName))
                     }
-                } else {
-                    receiver(KClass::class.asClassName().plusParameter(typeName))
                 }
-            }
-            .returns(typeName)
-            .apply {
-                val codeBlock = CodeBlock.builder()
-                codeBlock.add("return %N(", injectComponent)
-                if (constructor != null) {
-                    constructor.parameters.forEachIndexed { i, parameter ->
-                        if (i != 0) {
-                            codeBlock.add(", ")
+                .returns(typeName)
+                .apply {
+                    val codeBlock = CodeBlock.builder()
+                    codeBlock.add("return %N(", injectComponent)
+                    if (constructor != null) {
+                        params.forEachIndexed { i, parameter ->
+                            if (i != 0) {
+                                codeBlock.add(", ")
+                            }
+                            codeBlock.add("%L", parameter.name)
                         }
-                        codeBlock.add("%L", parameter.name)
                     }
+                    codeBlock.add(")")
+                    addCode(codeBlock.build())
+                }
+                .build()
+        }
+
+        val companion = if (options.generateCompanionExtensions) {
+            element.companion.also {
+                if (it == null) {
+                    error(
+                        """Missing companion for class: ${element.asClassName()}.
+                            |When you have the option me.tatarka.inject.generateCompanionExtensions=true you must declare a companion option on the component class for the extension function to apply to.
+                            |You can do so by adding 'companion object' to the class.
+                        """.trimMargin(), element
+                    )
                 }
-                codeBlock.add(")")
-                addCode(codeBlock.build())
             }
-            .build()
+        } else {
+            null
+        }
+        return mutableListOf<FunSpec>().apply {
+            val typeName = element.type.asTypeName()
+            val params = constructor?.parameters ?: emptyList()
+            add(
+                generateCreate(
+                    typeName,
+                    constructor,
+                    injectComponent,
+                    companion,
+                    params
+                )
+            )
+            val nonDefaultParams = constructor?.parameters?.filter { !it.hasDefault } ?: emptyList()
+            if (params.size != nonDefaultParams.size) {
+                add(
+                    generateCreate(
+                        typeName,
+                        constructor,
+                        injectComponent,
+                        companion,
+                        nonDefaultParams
+                    )
+                )
+            }
+        }
     }
 
     private fun collectTypes(
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 72872bb0..b091c576 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
@@ -636,6 +636,9 @@ private class ModelAstParam(
         return parent.properties.find { it.name == param.name }?.isPrivate() ?: false
     }
 
+    override val hasDefault: Boolean
+        get() = kmValueParameter?.hasDefault() ?: false
+
     override fun asParameterSpec(): ParameterSpec {
         return ParameterSpec(name, type.asTypeName())
     }
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 08ca487f..a9251bda 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
@@ -7,13 +7,7 @@ import com.squareup.kotlinpoet.LambdaTypeName
 import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
 import com.squareup.kotlinpoet.TypeName
 import com.squareup.kotlinpoet.asTypeName
-import kotlinx.metadata.Flag
-import kotlinx.metadata.KmClass
-import kotlinx.metadata.KmClassifier
-import kotlinx.metadata.KmConstructor
-import kotlinx.metadata.KmFunction
-import kotlinx.metadata.KmProperty
-import kotlinx.metadata.KmType
+import kotlinx.metadata.*
 import kotlinx.metadata.jvm.JvmMethodSignature
 import kotlinx.metadata.jvm.KotlinClassHeader
 import kotlinx.metadata.jvm.KotlinClassMetadata
@@ -322,4 +316,6 @@ inline fun KmProperty.isAbstract() = Flag.Common.IS_ABSTRACT(flags)
 
 inline fun KmProperty.isPrivate() = Flag.Common.IS_PRIVATE(flags)
 
-inline fun KmConstructor.isVal() = Flag.Constructor.IS_PRIMARY
\ No newline at end of file
+inline fun KmConstructor.isVal() = Flag.Constructor.IS_PRIMARY
+
+inline fun KmValueParameter.hasDefault() = Flag.ValueParameter.DECLARES_DEFAULT_VALUE(flags)
\ No newline at end of file
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 f8b27c86..91539a4d 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
@@ -440,6 +440,9 @@ private class KSAstParam(
     override val isPrivate: Boolean
         get() = parentClass?.getDeclaredProperties()?.find { it.simpleName == declaration.name }?.isPrivate() ?: false
 
+    override val hasDefault: Boolean
+        get() = declaration.hasDefault
+
     override fun asParameterSpec(): ParameterSpec {
         return ParameterSpec.builder(name, type.asTypeName())
             .build()


Test patch:
diff --git a/integration-tests/common/src/test/kotlin/me/tatarka/inject/test/DefaultArgTest.kt b/integration-tests/common/src/test/kotlin/me/tatarka/inject/test/DefaultArgTest.kt
new file mode 100644
index 00000000..d93f1b64
--- /dev/null
+++ b/integration-tests/common/src/test/kotlin/me/tatarka/inject/test/DefaultArgTest.kt
@@ -0,0 +1,23 @@
+package me.tatarka.inject.test
+
+import assertk.assertThat
+import assertk.assertions.isEqualTo
+import me.tatarka.inject.annotations.Component
+import kotlin.test.Test
+
+@Component abstract class DefaultArgComponent(val required1: String, val optional: String = "default", val required2: String)
+
+class DefaultArgTest {
+    @Test
+    fun generates_a_component_with_a_create_that_skips_default_args() {
+        val componentFull = DefaultArgComponent::class.create("one", "two", "three")
+        val componentDefault = DefaultArgComponent::class.create("one", "three")
+
+        assertThat(componentFull.required1).isEqualTo("one")
+        assertThat(componentFull.optional).isEqualTo("two")
+        assertThat(componentFull.required2).isEqualTo("three")
+        assertThat(componentDefault.required1).isEqualTo("one")
+        assertThat(componentDefault.optional).isEqualTo("default")
+        assertThat(componentDefault.required2).isEqualTo("three")
+    }
+}
\ No newline at end of file