Repository: evant/kotlin-inject
Pull request #77 (2020-09-30 19:43:24)

Support injecting suspend functions


Patch:
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 241d063..ae10b3e 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
@@ -510,7 +510,7 @@ private class ModelAstType(
     override fun isUnit(): Boolean = type is NoType
 
     override fun isFunction(): Boolean {
-        return element.toString().matches(Regex("kotlin\\.jvm\\.functions\\.Function[0-9]+.*"))
+        return kmType?.isFunction() == true
     }
 
     override fun isTypeAlis(): Boolean {
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 7022c45..36d64ec 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
@@ -2,17 +2,34 @@
 
 package me.tatarka.inject.compiler.kapt
 
-import com.squareup.kotlinpoet.*
 import com.squareup.kotlinpoet.ClassName
+import com.squareup.kotlinpoet.LambdaTypeName
 import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
-import kotlinx.metadata.*
-import kotlinx.metadata.jvm.*
+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.jvm.JvmMethodSignature
+import kotlinx.metadata.jvm.KotlinClassHeader
+import kotlinx.metadata.jvm.KotlinClassMetadata
 import me.tatarka.inject.compiler.HashCollector
 import me.tatarka.inject.compiler.collectHash
 import me.tatarka.inject.compiler.eqv
 import me.tatarka.inject.compiler.eqvItr
-import javax.lang.model.element.*
-import javax.lang.model.type.*
+import javax.lang.model.element.AnnotationMirror
+import javax.lang.model.element.Element
+import javax.lang.model.element.ExecutableElement
+import javax.lang.model.element.PackageElement
+import javax.lang.model.element.TypeElement
+import javax.lang.model.type.ArrayType
+import javax.lang.model.type.DeclaredType
+import javax.lang.model.type.TypeKind
+import javax.lang.model.type.TypeMirror
 
 fun Element.hasAnnotation(className: String): Boolean {
     return annotationMirrors.any { it.annotationType.toString() == className }
@@ -115,11 +132,14 @@ val ExecutableElement.simpleSig: String
 val JvmMethodSignature.simpleSig: String
     get() = name + desc.substring(0, desc.lastIndexOf(')') + 1)
 
-fun KmClassifier.asClassName() = when (this) {
-    is KmClassifier.Class -> name.asClassName()
-    is KmClassifier.TypeAlias -> name.asClassName()
-    is KmClassifier.TypeParameter -> null
-}
+private fun KmClassifier.asClassName() = name?.asClassName()
+
+private val KmClassifier.name: String?
+    get() = when (this) {
+        is KmClassifier.Class -> name
+        is KmClassifier.TypeAlias -> name
+        is KmClassifier.TypeParameter -> null
+    }
 
 fun kotlinx.metadata.ClassName.asClassName(): ClassName {
     val split = lastIndexOf('/')
@@ -190,14 +210,42 @@ fun KmType.asTypeName(): TypeName? {
         return abbreviatedType.asTypeName()
     }
     val isNullable = Flag.Type.IS_NULLABLE(flags)
-    val className = classifier.asClassName() ?: return null
-    return if (arguments.isEmpty()) {
-        className
+    return if (isFunction()) {
+        if (isSuspendFunction()) {
+            val returnType = arguments[arguments.size -2].type!!.arguments[0]
+            val parameters = arguments.dropLast(2)
+            LambdaTypeName.get(
+                parameters = parameters.map { it.type!!.asTypeName()!! }.toTypedArray(),
+                returnType = returnType.type!!.asTypeName()!!
+            ).copy(suspending = true)
+        } else {
+            val returnType = arguments.last()
+            val parameters = arguments.dropLast(1)
+            LambdaTypeName.get(
+                parameters = parameters.map { it.type!!.asTypeName()!! }.toTypedArray(),
+                returnType = returnType.type!!.asTypeName()!!
+            )
+        }
     } else {
-        className.parameterizedBy(arguments.map { it.type!!.asTypeName()!! })
+        val className = classifier.asClassName() ?: return null
+        if (arguments.isEmpty()) {
+            className
+        } else {
+            className.parameterizedBy(arguments.map { it.type!!.asTypeName()!! })
+        }
     }.copy(nullable = isNullable)
 }
 
+fun KmType.isFunction(): Boolean {
+    return classifier.name?.matches(Regex("kotlin/Function[0-9]+")) == true
+}
+
+private fun KmType.isSuspendFunction(): Boolean {
+    return isFunction() &&
+            arguments.size >= 2 &&
+            arguments[arguments.size - 2].type?.classifier?.name == "kotlin/coroutines/Continuation"
+}
+
 fun AnnotationMirror.eqv(other: AnnotationMirror): Boolean {
     if (annotationType != other.annotationType) {
         return false
@@ -270,4 +318,4 @@ 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
\ 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 5f16f73..ee43abe 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
@@ -377,8 +377,8 @@ private class KSAstType(provider: KSAstProvider) : AstType(), KSAstAnnotated, KS
     }
 
     override fun isFunction(): Boolean {
-        val name = type.actualType().declaration.qualifiedName ?: return false
-        return name.getQualifier() == "kotlin" && name.getShortName().matches(Regex("Function[0-9]+"))
+        val actualType = type.actualType()
+        return actualType.isFunction() || actualType.isSuspendingFunction()
     }
 
     override fun isTypeAlis(): Boolean {
diff --git a/kotlin-inject-compiler-ksp/src/main/kotlin/me/tatarka/inject/compiler/ksp/Util.kt b/kotlin-inject-compiler-ksp/src/main/kotlin/me/tatarka/inject/compiler/ksp/Util.kt
index 9d26b2a..69a8c45 100644
--- a/kotlin-inject-compiler-ksp/src/main/kotlin/me/tatarka/inject/compiler/ksp/Util.kt
+++ b/kotlin-inject-compiler-ksp/src/main/kotlin/me/tatarka/inject/compiler/ksp/Util.kt
@@ -1,14 +1,5 @@
 package me.tatarka.inject.compiler.ksp
 
-import com.squareup.kotlinpoet.ClassName
-import com.squareup.kotlinpoet.KModifier
-import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
-import com.squareup.kotlinpoet.TypeName
-import com.squareup.kotlinpoet.TypeVariableName
-import me.tatarka.inject.compiler.HashCollector
-import me.tatarka.inject.compiler.collectHash
-import me.tatarka.inject.compiler.eqv
-import me.tatarka.inject.compiler.eqvItr
 import com.google.devtools.ksp.symbol.AnnotationUseSiteTarget
 import com.google.devtools.ksp.symbol.KSAnnotated
 import com.google.devtools.ksp.symbol.KSAnnotation
@@ -19,9 +10,20 @@ import com.google.devtools.ksp.symbol.KSType
 import com.google.devtools.ksp.symbol.KSTypeAlias
 import com.google.devtools.ksp.symbol.KSTypeParameter
 import com.google.devtools.ksp.symbol.KSTypeReference
+import com.google.devtools.ksp.symbol.Modifier
 import com.google.devtools.ksp.symbol.Nullability
 import com.google.devtools.ksp.symbol.Variance
 import com.google.devtools.ksp.visitor.KSDefaultVisitor
+import com.squareup.kotlinpoet.ClassName
+import com.squareup.kotlinpoet.KModifier
+import com.squareup.kotlinpoet.LambdaTypeName
+import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
+import com.squareup.kotlinpoet.TypeName
+import com.squareup.kotlinpoet.TypeVariableName
+import me.tatarka.inject.compiler.HashCollector
+import me.tatarka.inject.compiler.collectHash
+import me.tatarka.inject.compiler.eqv
+import me.tatarka.inject.compiler.eqvItr
 
 fun KSAnnotated.annotationAnnotatedWith(
     className: String,
@@ -67,6 +69,22 @@ fun KSTypeReference.memberOf(enclosingClass: KSClassDeclaration): KSTypeReferenc
 }
 
 fun KSType.asTypeName(): TypeName {
+    val isFunction = isFunction()
+    val isSuspending = isSuspendingFunction()
+    if (isFunction || isSuspending) {
+        val returnType = arguments.last()
+        val parameters = arguments.dropLast(1)
+        return LambdaTypeName.get(
+            parameters = parameters.map { it.type!!.resolve().asTypeName() }.toTypedArray(),
+            returnType = returnType.type!!.resolve().asTypeName()
+        ).let {
+            if (isSuspending) {
+                it.copy(suspending = true)
+            } else {
+                it
+            }
+        }
+    }
 
     return declaration.accept(object : KSDefaultVisitor<Unit, TypeName>() {
 
@@ -140,3 +158,14 @@ fun KSType.eqvHashCode(collector: HashCollector = HashCollector()): Int = collec
 fun KSTypeReference.eqvHashCode(collector: HashCollector): Int = collectHash(collector) {
     resolve().eqvHashCode(collector)
 }
+
+
+fun KSType.isFunction(): Boolean {
+    val name = declaration.qualifiedName ?: return false
+    return name.getQualifier() == "kotlin" && name.getShortName().matches(Regex("Function[0-9]+"))
+}
+
+fun KSType.isSuspendingFunction(): Boolean {
+    val name = declaration.qualifiedName ?: return false
+    return name.getQualifier() == "kotlin.coroutines" && name.getShortName().matches(Regex("SuspendFunction[0-9]+"))
+}


Test patch:
diff --git a/integration-tests/common/src/test/kotlin/me/tatarka/inject/test/SuspendTest.kt b/integration-tests/common/src/test/kotlin/me/tatarka/inject/test/SuspendTest.kt
new file mode 100644
index 0000000..0ce3813
--- /dev/null
+++ b/integration-tests/common/src/test/kotlin/me/tatarka/inject/test/SuspendTest.kt
@@ -0,0 +1,30 @@
+package me.tatarka.inject.test
+
+import assertk.assertThat
+import assertk.assertions.isInstanceOf
+import kotlinx.coroutines.runBlocking
+import me.tatarka.inject.annotations.Component
+import me.tatarka.inject.annotations.Provides
+import kotlin.test.Test
+
+@Component abstract class SuspendFunctionComponent {
+    abstract val foo: () -> IFoo
+
+    abstract val suspendFoo: suspend () -> IFoo
+
+    val suspendFooFunction: suspend () -> IFoo
+        @Provides get() = { Foo() }
+
+    val fooFunction: () -> IFoo
+        @Provides get() = { Foo() }
+}
+
+class SuspendTest {
+    @Test
+    fun generates_a_component_that_provides_a_suspend_function(): Unit = runBlocking {
+        val component = SuspendFunctionComponent::class.create()
+
+        assertThat(component.suspendFoo()).isInstanceOf(Foo::class)
+    }
+}
+
diff --git a/integration-tests/kapt/build.gradle b/integration-tests/kapt/build.gradle
index 1f5b273..e031a3c 100644
--- a/integration-tests/kapt/build.gradle
+++ b/integration-tests/kapt/build.gradle
@@ -16,6 +16,7 @@ dependencies {
     testImplementation "org.jetbrains.kotlin:kotlin-test"
     testImplementation "org.jetbrains.kotlin:kotlin-test-junit"
     testImplementation 'org.jetbrains.kotlin:kotlin-reflect'
+    testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9'
     testImplementation 'javax.inject:javax.inject:1'
 
     testImplementation 'com.willowtreeapps.assertk:assertk-jvm:0.22'
diff --git a/integration-tests/ksp/build.gradle b/integration-tests/ksp/build.gradle
index 2a898d0..7b5131b 100644
--- a/integration-tests/ksp/build.gradle
+++ b/integration-tests/ksp/build.gradle
@@ -13,6 +13,7 @@ dependencies {
     testImplementation "org.jetbrains.kotlin:kotlin-test"
     testImplementation "org.jetbrains.kotlin:kotlin-test-junit"
     testImplementation 'org.jetbrains.kotlin:kotlin-reflect'
+    testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9'
     testImplementation 'javax.inject:javax.inject:1'
 
     testImplementation 'com.willowtreeapps.assertk:assertk-jvm:0.22'