Repository: evant/kotlin-inject
Pull request #33 (2020-08-07 01:29:44)

More robust kapt type matching


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 f2071d2..f1f0d08 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
@@ -2,21 +2,16 @@ 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.*
 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.*
 import javax.lang.model.type.*
 import javax.lang.model.util.ElementFilter
 import javax.lang.model.util.Elements
-import javax.lang.model.util.SimpleTypeVisitor7
 import javax.lang.model.util.Types
 import javax.tools.Diagnostic
 import kotlin.reflect.KClass
@@ -115,6 +110,31 @@ private class ModelBasicElement(provider: ModelAstProvider, override val element
     override val simpleName: String get() = element.simpleName.toString()
 }
 
+private class PrimitiveModelAstClass(
+    override val type: ModelAstType
+) : AstClass(), ModelAstProvider by type {
+    override val packageName: String = "kotlin"
+    override val name: String = type.toString()
+    override val modifiers: Set<AstModifier> = emptySet()
+    override val companion: AstClass? = null
+    override val superTypes: List<AstClass> = emptyList()
+    override val primaryConstructor: AstConstructor? = null
+    override val methods: List<AstMethod> = emptyList()
+
+    override fun asClassName(): ClassName = throw UnsupportedOperationException()
+
+    override fun equals(other: Any?): Boolean {
+        return other is PrimitiveModelAstClass && type == other.type
+    }
+
+    override fun hashCode(): Int {
+        return type.hashCode()
+    }
+
+    override fun hasAnnotation(className: String): Boolean = false
+    override fun typeAnnotatedWith(className: String): AstClass? = null
+}
+
 private class ModelAstClass(
     provider: ModelAstProvider,
     override val element: TypeElement,
@@ -214,19 +234,14 @@ private class ModelAstConstructor(
     ModelAstElement, ModelAstProvider by provider {
 
     override val parameters: List<AstParam> by lazy {
-        element.parameters.mapNotNull { element ->
-            if (kmConstructor != null) {
-                for (parameter in kmConstructor.valueParameters) {
-                    if (element.simpleName.contentEquals(parameter.name)) {
-                        return@mapNotNull ModelAstParam(
-                            this,
-                            element,
-                            parameter
-                        )
-                    }
-                }
-            }
-            null
+        val params = element.parameters
+        val kmParams = kmConstructor!!.valueParameters
+        params.zip(kmParams).map { (param, kmParam) ->
+            ModelAstParam(
+                this,
+                param,
+                kmParam
+            )
         }
     }
 
@@ -440,7 +455,14 @@ private class ModelAstType(
     override fun asElement(): AstBasicElement =
         ModelBasicElement(this, element)
 
-    override fun toAstClass(): AstClass = (element as TypeElement).toAstClass()
+    override fun toAstClass(): AstClass {
+        return when (type) {
+            is PrimitiveType -> PrimitiveModelAstClass(this)
+            is ArrayType -> PrimitiveModelAstClass(this)
+            is DeclaredType, is TypeVariable -> (types.asElement(type) as TypeElement).toAstClass()
+            else -> throw IllegalStateException("unknown type: $type")
+        }
+    }
 
     override fun asTypeName(): TypeName {
         return type.asTypeName(kmType)
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 170fc17..ff13b2e 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
@@ -11,20 +11,19 @@ import javax.lang.model.element.Modifier
 import javax.lang.model.element.TypeElement
 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 }
 }
 
 fun Element.typeAnnotatedWith(className: String) =
-        annotationMirrors.find {
-            it.annotationType.asElement().hasAnnotation(className)
-        }?.annotationType?.asElement() as? TypeElement
+    annotationMirrors.find {
+        it.annotationType.asElement().hasAnnotation(className)
+    }?.annotationType?.asElement() as? TypeElement
 
 fun TypeName.javaToKotlinType(): TypeName = if (this is ParameterizedTypeName) {
     (rawType.javaToKotlinType() as ClassName).parameterizedBy(
-            *typeArguments.map { it.javaToKotlinType() }.toTypedArray()
+        *typeArguments.map { it.javaToKotlinType() }.toTypedArray()
     )
 } else if (this is WildcardTypeName) {
     if (inTypes.isNotEmpty()) {
@@ -54,14 +53,14 @@ val TypeElement.metadata: KotlinClassMetadata?
     get() {
         val meta = getAnnotation(Metadata::class.java) ?: return null
         val header = KotlinClassHeader(
-                kind = meta.kind,
-                bytecodeVersion = meta.bytecodeVersion,
-                data1 = meta.data1,
-                data2 = meta.data2,
-                extraInt = meta.extraInt,
-                extraString = meta.extraString,
-                metadataVersion = meta.metadataVersion,
-                packageName = meta.packageName
+            kind = meta.kind,
+            bytecodeVersion = meta.bytecodeVersion,
+            data1 = meta.data1,
+            data2 = meta.data2,
+            extraInt = meta.extraInt,
+            extraString = meta.extraString,
+            metadataVersion = meta.metadataVersion,
+            packageName = meta.packageName
         )
         return KotlinClassMetadata.read(header)
     }
@@ -78,35 +77,13 @@ fun KotlinClassMetadata.toKmPackage() = when (this) {
 }
 
 fun ExecutableElement.matches(property: KmProperty): Boolean {
-    if (!nameMatches(property.getterSignature)) {
-        return false
-    }
-    val receiverType = property.receiverParameterType
-    return if (receiverType == null) {
-        parameters.isEmpty()
-    } else {
-        parameters.size == 1 && parameters[0].asType().matches(receiverType)
-    }
+    val sig = property.getterSignature
+    return sig != null && matches(sig)
 }
 
 fun ExecutableElement.matches(function: KmFunction): Boolean {
-    if (!nameMatches(function.signature)) {
-        return false
-    }
-    val parameterTypes = mutableListOf<KmType>()
-    function.receiverParameterType?.let { parameterTypes.add(it) }
-    for (param in function.valueParameters) {
-        param.type?.let { parameterTypes.add(it) }
-    }
-    if (parameterTypes.size != parameters.size) {
-        return false
-    }
-    for (i in 0 until parameterTypes.size) {
-        if (!parameters[i].asType().matches(parameterTypes[i])) {
-            return false
-        }
-    }
-    return true
+    val sig = function.signature
+    return sig != null && matches(sig)
 }
 
 fun ExecutableElement.matches(signature: JvmMethodSignature): Boolean {
@@ -115,90 +92,75 @@ fun ExecutableElement.matches(signature: JvmMethodSignature): Boolean {
     }
     //ex: (Lme/tatarka/inject/test/Bar;)V
     var index = 1 // skip initial (
-    val parameterTypes = mutableListOf<ClassName>()
     val desc = signature.desc
-    loop@while (true) {
-        when (desc[index]) {
+
+    fun match(param: TypeMirror): Boolean {
+        if (index == -1) {
+            return false
+        }
+        val c = desc[index]
+        index++
+        return when (c) {
             'L' -> {
-                index++
                 val className = desc.substring(index, desc.indexOf(';', index))
-                parameterTypes.add(ClassName.bestGuess(className.replace('/', '.')))
-                index += className.length
+                    .replace('/', '.')
+                index += className.length + 1 // ;
+                if (param is DeclaredType) {
+                    param.asElement().toString() == className
+                } else {
+                    false
+                }
             }
             'Z' -> {
-                index++
-                parameterTypes.add(BOOLEAN)
+                param.kind == TypeKind.BOOLEAN
             }
             'B' -> {
-                index++
-                parameterTypes.add(BYTE)
+                param.kind == TypeKind.BYTE
             }
             'C' -> {
-                index++
-                parameterTypes.add(CHAR)
+                param.kind == TypeKind.CHAR
             }
             'S' -> {
-                index++
-                parameterTypes.add(SHORT)
+                param.kind == TypeKind.SHORT
             }
             'I' -> {
-                index++
-                parameterTypes.add(INT)
+                param.kind == TypeKind.INT
             }
             'J' -> {
-                index++
-                parameterTypes.add(LONG)
+                param.kind == TypeKind.LONG
             }
             'F' -> {
-                index++
-                parameterTypes.add(FLOAT)
+                param.kind == TypeKind.FLOAT
             }
             'D' -> {
-                index++
-                parameterTypes.add(DOUBLE)
+                param.kind == TypeKind.DOUBLE
+            }
+            '[' -> {
+                if (param is ArrayType) {
+                    match(param.componentType)
+                } else {
+                    false
+                }
             }
-            else -> break@loop
+            ')' -> {
+                index = -1 // mark end
+                false
+            }
+            else -> throw IllegalStateException("Unknown descriptor: $c in {$desc}")
         }
     }
-    if (parameters.size != parameterTypes.size) {
-        return false
-    }
-    for (i in 0 until parameterTypes.size) {
-        if (!parameters[i].asType().matches(parameterTypes[i], emptyList())) {
+
+    for (param in parameters) {
+        if (!match(param.asType())) {
             return false
         }
     }
-    return true
-}
-
-private fun TypeMirror.matches(type: KmType): Boolean {
-    return when (val classifier = type.classifier) {
-        is KmClassifier.Class -> {
-            matches(classifier.asClassName(), type.arguments)
-        }
-        is KmClassifier.TypeParameter -> false
-        is KmClassifier.TypeAlias -> false
+    // ensure we consumed all params
+    if (desc[index] != ')') {
+        return false
     }
-}
 
-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)
+    return true
 }
 
 private fun ExecutableElement.nameMatches(signature: JvmMemberSignature?): Boolean {
@@ -269,8 +231,18 @@ fun TypeMirror.asTypeName(kmType: KmType?): TypeName {
             return visitDeclared(t, p)
         }
 
-        override fun visitArray(t: ArrayType, p: Void?): ParameterizedTypeName {
-            return ARRAY.parameterizedBy(t.componentType.asTypeName())
+        override fun visitArray(t: ArrayType, p: Void?): TypeName {
+            return when (t.componentType.kind) {
+                TypeKind.BOOLEAN -> BOOLEAN_ARRAY
+                TypeKind.BYTE -> BYTE_ARRAY
+                TypeKind.SHORT -> SHORT_ARRAY
+                TypeKind.INT -> INT_ARRAY
+                TypeKind.LONG -> LONG_ARRAY
+                TypeKind.CHAR -> CHAR_ARRAY
+                TypeKind.FLOAT -> FLOAT_ARRAY
+                TypeKind.DOUBLE -> DOUBLE_ARRAY
+                else -> ARRAY.parameterizedBy(t.componentType.asTypeName(kmType?.arguments?.getOrNull(0)?.type))
+            }
         }
 
         override fun visitTypeVariable(


Test patch:
diff --git a/integration-tests/src/test/kotlin/me/tatarka/inject/test/ProvidesTest.kt b/integration-tests/src/test/kotlin/me/tatarka/inject/test/ProvidesTest.kt
index 8263c64..951d774 100644
--- a/integration-tests/src/test/kotlin/me/tatarka/inject/test/ProvidesTest.kt
+++ b/integration-tests/src/test/kotlin/me/tatarka/inject/test/ProvidesTest.kt
@@ -1,9 +1,7 @@
 package me.tatarka.inject.test
 
 import assertk.assertThat
-import assertk.assertions.isNotNull
-import assertk.assertions.isSameAs
-import assertk.assertions.isTrue
+import assertk.assertions.*
 import me.tatarka.inject.annotations.Component
 import me.tatarka.inject.annotations.Inject
 import me.tatarka.inject.annotations.Provides
@@ -11,11 +9,9 @@ import kotlin.test.Test
 
 class ProvidesFoo(val bar: ProvidesBar? = null)
 
-@Inject
-class ProvidesBar
+@Inject class ProvidesBar
 
-@Component
-abstract class ProvidesFunctionComponent {
+@Component abstract class ProvidesFunctionComponent {
     var providesCalled = false
 
     abstract val foo: ProvidesFoo
@@ -24,8 +20,7 @@ abstract class ProvidesFunctionComponent {
     fun foo() = ProvidesFoo().also { providesCalled = true }
 }
 
-@Component
-abstract class ProvidesFunctionArgComponent {
+@Component abstract class ProvidesFunctionArgComponent {
     var providesCalled = false
 
     abstract val foo: ProvidesFoo
@@ -34,8 +29,7 @@ abstract class ProvidesFunctionArgComponent {
     fun foo(bar: ProvidesBar) = ProvidesFoo(bar).also { providesCalled = true }
 }
 
-@Component
-abstract class ProvidesValComponent {
+@Component abstract class ProvidesValComponent {
     var providesCalled = false
 
     abstract val foo: ProvidesFoo
@@ -45,8 +39,7 @@ abstract class ProvidesValComponent {
         get() = ProvidesFoo().also { providesCalled = true }
 }
 
-@Component
-abstract class ProvidesExtensionFunctionComponent {
+@Component abstract class ProvidesExtensionFunctionComponent {
     var providesCalled = false
 
     abstract val foo: ProvidesFoo
@@ -55,8 +48,7 @@ abstract class ProvidesExtensionFunctionComponent {
     fun ProvidesBar.provideFoo() = ProvidesFoo(this).also { providesCalled = true }
 }
 
-@Component
-abstract class ProvidesExtensionValComponent {
+@Component abstract class ProvidesExtensionValComponent {
     var providesCalled = false
 
     abstract val foo: ProvidesFoo
@@ -66,19 +58,16 @@ abstract class ProvidesExtensionValComponent {
         get() = ProvidesFoo(this).also { providesCalled = true }
 }
 
-@Component
-abstract class ProvidesValConstructorComponent(@Provides val provideFoo: ProvidesFoo) {
+@Component abstract class ProvidesValConstructorComponent(@Provides val provideFoo: ProvidesFoo) {
     abstract val foo: ProvidesFoo
 }
 
 class Foo1
 class Foo2
 
-@Inject
-class Foo3 : IFoo
+@Inject class Foo3 : IFoo
 
-@Component
-abstract class ProvidesOverloadsComponent {
+@Component abstract class ProvidesOverloadsComponent {
     abstract val foo1: Foo1
     abstract val foo2: Foo2
     abstract val foo3: Foo3
@@ -99,6 +88,93 @@ abstract class ProvidesOverloadsComponent {
         get() = this
 }
 
+class IntFoo(val int: Int)
+
+class IntArrayFoo(val intArray: IntArray)
+
+class StringArrayFoo(val stringArray: Array<String>)
+
+@Inject class BasicTypes(
+    val boolean: Boolean,
+    val byte: Byte,
+    val char: Char,
+    val short: Short,
+    val int: Int,
+    val long: Long,
+    val float: Float,
+    val double: Double,
+    val string: String,
+    val booleanArray: BooleanArray,
+    val stringArray: Array<String>,
+    val intFoo: IntFoo,
+    val intArrayFoo: IntArrayFoo,
+    val stringArrayFoo: StringArrayFoo
+)
+
+@Component abstract class ProvidesBasicTypes {
+    abstract val basicType: BasicTypes
+
+    @Provides
+    val boolean
+        get() = true
+
+    @Provides
+    val byte
+        get() = 1.toByte()
+
+    @Provides
+    val char
+        get() = 'a'
+
+    @Provides
+    val short
+        get() = 2.toShort()
+
+    @Provides
+    val int
+        get() = 3
+
+    @Provides
+    val long
+        get() = 4L
+
+    @Provides
+    val float
+        get() = 5f
+
+    @Provides
+    val double
+        get() = 6.0
+
+    @Provides
+    val string
+        get() = "b"
+
+    @Provides
+    val booleanArray
+        get() = booleanArrayOf(true)
+
+    @Provides
+    val stringArray
+        get() = arrayOf("c")
+
+    @Provides
+    val intArray
+        get() = intArrayOf(7)
+
+    @Provides
+    val Int.bind
+        get() = IntFoo(this)
+
+    @Provides
+    val IntArray.bind
+        get() = IntArrayFoo(this)
+
+    @Provides
+    val Array<String>.bind
+        get() = StringArrayFoo(this)
+}
+
 class ProvidesTest {
 
     @Test fun generates_a_component_that_provides_a_dep_from_a_function() {
@@ -152,5 +228,24 @@ class ProvidesTest {
         assertThat(component.foo3).isNotNull()
         assertThat(component.foo4).isNotNull()
     }
+
+    @Test fun generates_a_conmponent_that_provides_basic_types() {
+        val component = ProvidesBasicTypes::class.create()
+
+        assertThat(component.basicType.boolean).isTrue()
+        assertThat(component.basicType.byte).isEqualTo(1.toByte())
+        assertThat(component.basicType.char).isEqualTo('a')
+        assertThat(component.basicType.short).isEqualTo(2.toShort())
+        assertThat(component.basicType.int).isEqualTo(3)
+        assertThat(component.basicType.long).isEqualTo(4L)
+        assertThat(component.basicType.float).isEqualTo(5f)
+        assertThat(component.basicType.double).isEqualTo(6.0)
+        assertThat(component.basicType.string).isEqualTo("b")
+        assertThat(component.basicType.booleanArray.toTypedArray()).containsExactly(true)
+        assertThat(component.basicType.stringArray).containsExactly("c")
+        assertThat(component.basicType.intFoo.int).isEqualTo(3)
+        assertThat(component.basicType.intArrayFoo.intArray.toTypedArray()).containsExactly(7)
+        assertThat(component.basicType.stringArrayFoo.stringArray).containsExactly("c")
+    }
 }