Repository: evant/kotlin-inject
Pull request #83 (2020-10-18 20:09:37)

Support annotating constructors (primary and secondary) with `@Inject`
Fixes https://github.com/evant/kotlin-inject/issues/10.


Patch:
diff --git a/README.md b/README.md
index 323c7d8..d750126 100644
--- a/README.md
+++ b/README.md
@@ -362,8 +362,8 @@ You can provide some additional options to the processor.
 - `me.tatarka.inject.generateCompanionExtensions=true`
 
   This will generate the `create()` methods on the companion object instead of the component's class. This allows you to do
-  `MyComponent.create()` instead of `MyComponent::class.create()`. However, due to a kotlin limitiation you will have to
-  explitilcy specify a companion object for your component.
+  `MyComponent.create()` instead of `MyComponent::class.create()`. However, due to a kotlin limitation you will have to
+  explicitly specify a companion object for your component.
   
   ```kotlin
   @Component abstract class MyComponent {
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 487734f..47314ce 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
@@ -1,8 +1,6 @@
 package me.tatarka.inject.compiler
 
 import com.squareup.kotlinpoet.*
-import kotlin.contracts.InvocationKind
-import kotlin.contracts.contract
 import kotlin.reflect.KClass
 
 interface AstProvider {
@@ -58,6 +56,8 @@ abstract class AstClass : AstElement(), AstHasModifiers {
 
     abstract val primaryConstructor: AstConstructor?
 
+    abstract val constructors: List<AstConstructor>
+
     abstract val methods: List<AstMethod>
 
     abstract val type: AstType
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 e4c491f..ca52982 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
@@ -628,13 +628,28 @@ fun AstElement.isComponent() = hasAnnotation<Component>()
 
 fun AstMethod.isProvides(): Boolean = hasAnnotation<Provides>()
 
-fun AstClass.isInject(options: Options): Boolean {
-    if (options.enableJavaxAnnotations) {
-        if (primaryConstructor?.hasAnnotation("javax.inject.Inject") == true) {
-            return true
+fun AstClass.findInjectConstructors(messenger: Messenger, options: Options): AstConstructor? {
+    val injectCtors = constructors.filter {
+        if (options.enableJavaxAnnotations) {
+            it.hasAnnotation("javax.inject.Inject") || it.hasAnnotation<Inject>()
+        } else {
+            it.hasAnnotation<Inject>()
+        }
+    }
+
+    return when {
+        hasAnnotation<Inject>() && injectCtors.isNotEmpty() -> {
+            messenger.error("Cannot annotate constructor with @Inject in an @Inject-annotated class", this)
+            null
+        }
+        hasAnnotation<Inject>() -> primaryConstructor
+        injectCtors.size > 1 -> {
+            messenger.error("Class cannot contain multiple @Inject-annotated constructors", this)
+            null
         }
+        injectCtors.isNotEmpty() -> injectCtors.first()
+        else -> null
     }
-    return hasAnnotation<Inject>()
 }
 
 fun AstElement.qualifier(options: Options): AstAnnotation? {
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 38ac25e..8ef359d 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
@@ -230,7 +230,8 @@ class TypeCollector private constructor(private val provider: AstProvider, priva
             return result
         }
         val astClass = key.type.toAstClass()
-        if (astClass.isInject(options)) {
+        val injectCtor = astClass.findInjectConstructors(messenger, options)
+        if (injectCtor != null) {
             val scope = astClass.scopeType(options)
             val scopedComponent = if (scope != null) scopedAccessors[scope] else null
             if (scope != null && scopedComponent == null) {
@@ -238,7 +239,7 @@ class TypeCollector private constructor(private val provider: AstProvider, priva
                 return null
             }
             return TypeCreator.Constructor(
-                astClass.primaryConstructor!!,
+                injectCtor,
                 accessor = scopedComponent?.accessor,
                 scopedComponent = scopedComponent?.type
             )
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 b091c57..eb8e208 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
@@ -3,12 +3,10 @@ package me.tatarka.inject.compiler.kapt
 import com.squareup.kotlinpoet.*
 import com.squareup.kotlinpoet.ClassName
 import kotlinx.metadata.*
-import kotlinx.metadata.jvm.JvmMethodSignature
 import kotlinx.metadata.jvm.annotations
 import kotlinx.metadata.jvm.getterSignature
 import kotlinx.metadata.jvm.signature
 import me.tatarka.inject.compiler.*
-import java.util.*
 import javax.annotation.processing.Messager
 import javax.annotation.processing.ProcessingEnvironment
 import javax.lang.model.element.*
@@ -164,6 +162,7 @@ private class PrimitiveModelAstClass(
     override val companion: AstClass? = null
     override val superTypes: List<AstClass> = emptyList()
     override val primaryConstructor: AstConstructor? = null
+    override val constructors: List<AstConstructor> = emptyList()
     override val methods: List<AstMethod> = emptyList()
 
     override fun asClassName(): ClassName = throw UnsupportedOperationException()
@@ -232,20 +231,35 @@ private class ModelAstClass(
 
     override val primaryConstructor: AstConstructor?
         get() {
-            return ElementFilter.constructorsIn(element.enclosedElements).mapNotNull { constructor ->
-                //TODO: not sure how to match constructors
+            if (kmClass == null) return null
+
+            val primaryKmCtor = kmClass.constructors.find(KmConstructor::isPrimary)
+            val primaryKmCtorSignature = primaryKmCtor?.signature?.simpleSig ?: return null
+
+            return ElementFilter.constructorsIn(element.enclosedElements)
+                .find { it.simpleSig == primaryKmCtorSignature }
+                ?.let { ModelAstConstructor(this, this, it, primaryKmCtor) }
+        }
+
+    override val constructors: List<AstConstructor>
+        get() {
+            if (kmClass == null) return emptyList()
+
+            val kmCtors = kmClass.constructors.associateBy { it.signature?.simpleSig }
+            return ElementFilter.constructorsIn(element.enclosedElements).map { constructor ->
                 ModelAstConstructor(
                     this,
                     this,
                     constructor,
-                    kmClass?.constructors?.first()
+                    kmCtors[constructor.simpleSig]
                 )
-            }.firstOrNull()
+            }
         }
 
     override val methods: List<AstMethod>
         get() {
-            val kmClass = kmClass ?: return emptyList()
+            if (kmClass == null) return emptyList()
+
             val methods = mutableMapOf<String, ExecutableElement>()
 
             for (method in ElementFilter.methodsIn(element.enclosedElements)) {
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 a9251bd..647d7c6 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
@@ -316,6 +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
+inline fun KmConstructor.isPrimary() = Flag.Constructor.IS_PRIMARY(flags)
 
 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 91539a4..a2db777 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
@@ -1,5 +1,6 @@
 package me.tatarka.inject.compiler.ksp
 
+import com.google.devtools.ksp.*
 import com.squareup.kotlinpoet.ClassName
 import com.squareup.kotlinpoet.MemberName
 import com.squareup.kotlinpoet.ParameterSpec
@@ -18,11 +19,6 @@ import me.tatarka.inject.compiler.AstProperty
 import me.tatarka.inject.compiler.AstProvider
 import me.tatarka.inject.compiler.AstType
 import me.tatarka.inject.compiler.Messenger
-import com.google.devtools.ksp.findActualType
-import com.google.devtools.ksp.getDeclaredFunctions
-import com.google.devtools.ksp.getDeclaredProperties
-import com.google.devtools.ksp.isAbstract
-import com.google.devtools.ksp.isPrivate
 import com.google.devtools.ksp.processing.KSPLogger
 import com.google.devtools.ksp.processing.Resolver
 import com.google.devtools.ksp.symbol.*
@@ -176,6 +172,9 @@ private class KSAstClass(provider: KSAstProvider, override val declaration: KSCl
             return declaration.primaryConstructor?.let { KSAstConstructor(this, this, it) }
         }
 
+    override val constructors: List<AstConstructor>
+        get() = declaration.getConstructors().map { KSAstConstructor(this, this, it) }
+
     override val methods: List<AstMethod>
         get() {
             return mutableListOf<AstMethod>().apply {
diff --git a/kotlin-inject-runtime/src/main/kotlin/me/tatarka/inject/annotations/Annotations.kt b/kotlin-inject-runtime/src/main/kotlin/me/tatarka/inject/annotations/Annotations.kt
index a2b58b2..a372096 100644
--- a/kotlin-inject-runtime/src/main/kotlin/me/tatarka/inject/annotations/Annotations.kt
+++ b/kotlin-inject-runtime/src/main/kotlin/me/tatarka/inject/annotations/Annotations.kt
@@ -4,7 +4,7 @@ import kotlin.annotation.AnnotationRetention.RUNTIME
 import kotlin.annotation.AnnotationTarget.*
 
 @Retention(RUNTIME)
-@Target(CLASS, FUNCTION)
+@Target(CLASS, FUNCTION, CONSTRUCTOR)
 annotation class Inject
 
 @Retention(RUNTIME)


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 5c18a0e..e6a001c 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
@@ -163,6 +163,49 @@ class FailureTest(val target: Target) {
         }.isFailure().output().contains("Cannot find an @Inject constructor or provider for: String")
     }
 
+    @Test
+    fun fails_if_inject_annotated_class_has_inject_annotated_constructors() {
+        assertThat {
+            projectCompiler.source(
+                "MyComponent.kt", """
+                    import me.tatarka.inject.annotations.Component
+                    import me.tatarka.inject.annotations.Inject
+                    
+                    @Inject class Foo @Inject constructor(bar: String)
+                    
+                    @Component abstract class MyComponent {
+                        abstract val foo: Foo
+                    }
+                """.trimIndent()
+            ).compile()
+        }.isFailure()
+            .output()
+            .contains("Cannot annotate constructor with @Inject in an @Inject-annotated class")
+    }
+
+    @Test
+    fun fails_if_class_has_multiple_inject_annotated_constructors() {
+        assertThat {
+            projectCompiler.source(
+                "MyComponent.kt", """
+                    import me.tatarka.inject.annotations.Component
+                    import me.tatarka.inject.annotations.Inject
+                    
+                    class Foo @Inject constructor(bar: String) {
+                        
+                        @Inject constructor() : this("test")
+                    }
+                    
+                    @Component abstract class MyComponent {
+                        abstract val foo: Foo
+                    }
+                """.trimIndent()
+            ).compile()
+        }.isFailure()
+            .output()
+            .contains("Class cannot contain multiple @Inject-annotated constructors")
+    }
+
     @Test
     fun fails_if_type_cannot_be_provided_to_provides() {
         assertThat {
diff --git a/integration-tests/common/src/test/kotlin/me/tatarka/inject/test/InjectConstructorTest.kt b/integration-tests/common/src/test/kotlin/me/tatarka/inject/test/InjectConstructorTest.kt
new file mode 100644
index 0000000..472eab0
--- /dev/null
+++ b/integration-tests/common/src/test/kotlin/me/tatarka/inject/test/InjectConstructorTest.kt
@@ -0,0 +1,39 @@
+package me.tatarka.inject.test
+
+import assertk.assertThat
+import assertk.assertions.isNotNull
+import me.tatarka.inject.annotations.Component
+import me.tatarka.inject.annotations.Inject
+import org.junit.Test
+
+@Component
+abstract class InjectCtorComponent {
+
+    abstract val primaryInject: FooWithPrimaryInject
+
+    abstract val secondaryInject: FooWithSecondaryInject
+}
+
+class FooWithPrimaryInject @Inject constructor()
+
+class FooWithSecondaryInject(private val prop: String) {
+
+    @Inject constructor() : this("prop")
+}
+
+class InjectConstructorTest {
+
+    @Test
+    fun class_with_inject_annotated_primary_constructor_can_be_provided() {
+        val component = InjectCtorComponent::class.create()
+
+        assertThat(component.primaryInject).isNotNull()
+    }
+
+    @Test
+    fun class_with_inject_annotated_secondary_constructor_can_be_provided() {
+        val component = InjectCtorComponent::class.create()
+
+        assertThat(component.secondaryInject).isNotNull()
+    }
+}
\ No newline at end of file