kotlinreflectionkotlin-reflect

Convert `Type` to a `KType`


Is there a way to get a Kotlin KType from a java.lang.reflect.Type?

Background: I'm working on some code that gets properties/fields from a class and passes them to methods (think JUnit 4 Theories for anyone familiar with those). Because I need Java compatibility, I can't always use pure Kotlin reflection because calls like MyClass::class.memberProperties don't work with raw Java fields.

Another option would be to cast my KTypes to java.lang.reflect.Types (via .javaType), butKType provides a convenient isSupertypeOf(...) function that allows comparison between two KTypes and I haven't found similar functionality for Java's Type.

Very Rough Example:

Kotlin Code:

class KotlinTestClass {
    fun test(someParam: List<String>) {
        someParam.forEach { println(it) }
    }

    class CompatibleTypes {
        val exact: List<String> = listOf("foo")
        val mutable: MutableList<String> = mutableListOf("bar")
        val implementation: ImmutableList<String> = immutableListOf("baz")
    }

    class NonCompatibleTypes {
        val wrongParam: List<Int> = listOf(42)
        val wrongType: Map<String, String> = mapOf("hello" to "world")
    }
}

Java code:

public class JavaTestClass {
    public static class CompatibleTypes {
        public static final List<String> exact = List.of("foo");
        public static final ImmutableList<String> implementation = ImmutableList.of("baz");
    }

    public static class NonCompatibleTypes {
        public static final List<Integer> wrongParam = List.of(42);
        public static final Map<String,String> wrongType = Map.of("hello", "world");
    }
}

Kotlin Test Code:

fun main(args: Array<String>) {
    val parameterType = KotlinTestClass::class.functions.first { it.name == "test" }.parameters[1].type

    println("Target: $parameterType")

    println("--------------------------------------")
    //These should all print "true"
    println("Compatible Kotlin types:")
    KotlinTestClass.CompatibleTypes::class.memberProperties.asSequence()
        .map { it.returnType }
        .forEach { println(" - ${parameterType.isSupertypeOf(it)}  ($it)") }

    println("--------------------------------------")
    println("Compatible Java types:")
    //These should all print "true"
    //This code doesn't work because the fields aren't valid Kotlin properties
    JavaTestClass.CompatibleTypes::class.memberProperties.forEach { println(it) }
    JavaTestClass.CompatibleTypes::class.java.declaredFields.asSequence()
        .map { it.kotlinProperty?.returnType }
        .filterNotNull()
        .forEach { println(" - ${parameterType.isSupertypeOf(it)}  ($it)") }

    println("--------------------------------------")
    println("Non-Compatible Kotlin types:")
    //These should all print "false"
    KotlinTestClass.NonCompatibleTypes::class.memberProperties.asSequence()
        .map { it.returnType }
        .forEach { println(" - ${parameterType.isSupertypeOf(it)}  ($it)") }

    println("--------------------------------------")
    println("Non-Compatible Java types:")
    //These should all print "false"
    //This code doesn't work because the fields aren't valid Kotlin properties
    JavaTestClass.NonCompatibleTypes::class.java.declaredFields.asSequence()
        .map { it.kotlinProperty?.returnType }
        .filterNotNull()
        .forEach { println(" - ${parameterType.isSupertypeOf(it)}  ($it)") }
}

Solution

  • Assuming the Type comes from a declaration in Java, the following works for simple classes, parameterised types, raw types, and array types. There might be some edge cases that I didn't handle, though.

    The idea is basically to check for each of the things that the Type could be (Class, ParameterizedType, TypeVariable, etc) and handle each case, and recursively traverse the "type tree" if needed.

    fun convert(type: Type) = when(type) {
        // simple arrays, simple classes, or raw types
        is Class<*> -> when {
            // we takeUnless it.isPrimitive here because primitve arrays are mapped to their own Kotlin classes, not Array<T>
            type.isArray -> type.kotlin.createType(listOfNotNull(type.componentType?.takeUnless { it.isPrimitive }?.toKTypeProjection()))
            else -> type.kotlin.createType(List(type.typeParameters.size) { KTypeProjection.STAR })
        }
    
        is ParameterizedType -> (type.rawType as Class<*>).kotlin.createType(
            type.actualTypeArguments.map { it.toKTypeProjection() }
        )
    
        // arrays of parameterised types and type parameters
        is GenericArrayType -> Array::class.createType(listOf(type.genericComponentType.toKTypeProjection()))
        is TypeVariable<*> -> type.toKType()
        else -> throw NotImplementedError()
    }
    
    fun Type.toKTypeProjection(): KTypeProjection = when (this) {
        is WildcardType -> when {
            // lowerBounds and upperBounds should contain at most one element in Java
            lowerBounds.isNotEmpty() -> KTypeProjection(KVariance.IN, convert(lowerBounds[0]))
            upperBounds.isNotEmpty() -> KTypeProjection(KVariance.OUT, convert(upperBounds[0]))
            else -> KTypeProjection(KVariance.INVARIANT, convert(this))
        }
        else -> KTypeProjection(KVariance.INVARIANT, convert(this))
    }
    
    fun TypeVariable<*>.toKType() = when (val decl = this.genericDeclaration) {
        // here we need to find the class/method that declares the type variable
        is Class<*> -> decl.kotlin.typeParameters.first { it.name == this.name }.createType()
        is Method -> decl.kotlinFunction!!.typeParameters.first { it.name == this.name }.createType()
        else -> throw NotImplementedError()
    }