【kotlinx-metadata】ClassNameからClass(KClass)を取得する

# TL;DR
- `ClassName`の形式を適切に変換することで、`Class.forName`で`Class`を取得できる
- ただし、`kotlin.Int`など、`Class.forName`では`Class`を取得できない内容は個別対応が必要
- （特に引数の型情報を取得する場合などで）`Java Reflection`から情報を取得すると、[`value class`など`Kotlin`上の定義と`Java`上の実態が食い違う場合](https://speakerdeck.com/k163377/java-reflectionkarajian-tavalue-class)に対応できない

# やること
`kotlinx-metadata`では、`Kotlin`上の型を`ClassName`で取得できます。
この`ClassName`の実体は文字列であり、実際の処理を行っていく上では`Class`及び`KClass`を取得する必要が有るため、この取得をやります。

ここで、[`KClass`には`Class.forName`相当の内容が存在していない](https://youtrack.jetbrains.com/issue/KT-10440)ため、`KClass`を取得する場合は`Class`を取得した上で`Class.kotlin`を呼び出す必要が有ります。

## Class.forNameでのClass取得
`ClassName`の実体はパッケージを`/`区切り、クラスの関係を`.`区切りで表した文字列です[^className]。
例えば、`kotlin.collections.Map.Entry`は`kotlin/collections/Map.Entry`という文字列になります。

[^className]: 0.3.0時点では`typealias`として定義されており、[将来的には`value class`になる](https://github.com/JetBrains/kotlin/blob/92d200e093c693b3c06e53a39e0b0973b84c7ec5/libraries/kotlinx-metadata/src/kotlinx/metadata/ClassName.kt)そうです。

`Class.forName`で要求される形式は、パッケージが`.`区切り、クラスが`$`区切りの形式です。
つまり、以下のような変換処理で対応できます。

```kotlin
/**
 * ClassName -> fully qualified name(= Class.forNameの引数の形式)の変換
 */
fun ClassName.toFqName(): String = this
    .replace(".", "$")
    .replace("/", ".")
```

## Class.forNameではClassを取得できない内容への対応
上記のやり方では、`kotlin.Int`など、`Kotlin`上での名前で`Class.forName`しても`ClassNotFoundException`になってしまう内容には対応できません。
そのような内容については手動でマップ処理を書く必要が有ります。

## このようなやり方が必要な理由
素朴に考えると「引数等に関しては`Java Reflection`から情報を取得できるのでは？」と思われるかもしれませんが、実際にはこれは難しいです。
理由は、`value class`を用いた場合には、`Kotlin`上の型や名前と`Java`上の型や名前が食い違い、かつ`Java Reflection`だけでは`Kotlin`上の元の定義を参照できないためです。

この辺りの話は下記の資料に詳しくまとめています。

https://speakerdeck.com/k163377/java-reflectionkarajian-tavalue-class

# サンプルコード
ここまでの内容をまとめた`ClassName` -> `Class`の変換処理は以下の通りです。
`Class.forName`では`Class`を取得できない内容への対応を想定した`Map`類は、`kotlin.jvm.internal.ClassReference`に定義された`classFqNames`を参考に作成していますが、一部抜けが有るかもしれません。

```kotlin
import kotlinx.metadata.*

/**
 * ClassName -> Java Primitive型のClassのマップ
 */
val kotlinizedPrimitiveClassMap: Map<ClassName, Class<*>> = mapOf(
    "kotlin/Boolean" to Boolean::class.javaPrimitiveType!!,
    "kotlin/Char" to Char::class.javaPrimitiveType!!,
    "kotlin/Byte" to Byte::class.javaPrimitiveType!!,
    "kotlin/Short" to Short::class.javaPrimitiveType!!,
    "kotlin/Int" to Int::class.javaPrimitiveType!!,
    "kotlin/Float" to Float::class.javaPrimitiveType!!,
    "kotlin/Long" to Long::class.javaPrimitiveType!!,
    "kotlin/Double" to Double::class.javaPrimitiveType!!
)

/**
 * kotlin.Intなど、KClass.qualifiedNameとClass.nameが食い違うクラスに関するClassName -> Classのマップ
 */
val kotlinizedClassMap: Map<ClassName, Class<*>> = mapOf(
    // primitive wrapper系
    "kotlin/Boolean" to Boolean::class.javaObjectType,
    "kotlin/Char" to Char::class.javaObjectType,
    "kotlin/Byte" to Byte::class.javaObjectType,
    "kotlin/Short" to Short::class.javaObjectType,
    "kotlin/Int" to Int::class.javaObjectType,
    "kotlin/Float" to Float::class.javaObjectType,
    "kotlin/Long" to Long::class.javaObjectType,
    "kotlin/Double" to Double::class.javaObjectType,
    // collection系
    "kotlin/collections/Iterable" to Iterable::class.java,
    "kotlin/collections/Iterator" to Iterator::class.java,
    "kotlin/collections/Collection" to Collection::class.java,
    "kotlin/collections/List" to List::class.java,
    "kotlin/collections/Set" to Set::class.java,
    "kotlin/collections/ListIterator" to ListIterator::class.java,
    "kotlin/collections/Map" to Map::class.java,
    "kotlin/collections/Map.Entry" to Map.Entry::class.java,
    // array系
    "kotlin/BooleanArray" to BooleanArray::class.java,
    "kotlin/CharArray" to CharArray::class.java,
    "kotlin/ByteArray" to ByteArray::class.java,
    "kotlin/ShortArray" to ShortArray::class.java,
    "kotlin/IntArray" to IntArray::class.java,
    "kotlin/FloatArray" to FloatArray::class.java,
    "kotlin/LongArray" to LongArray::class.java,
    "kotlin/DoubleArray" to DoubleArray::class.java,
    "kotlin/Array" to Array::class.java,
    // その他系
    "kotlin/Any" to Any::class.java,
    "kotlin/String" to String::class.java,
    "kotlin/CharSequence" to CharSequence::class.java,
    "kotlin/Throwable" to Throwable::class.java,
    "kotlin/Cloneable" to Cloneable::class.java,
    "kotlin/Number" to Number::class.java,
    "kotlin/Comparable" to Comparable::class.java,
    "kotlin/Enum" to Enum::class.java,
    "kotlin/Annotation" to Annotation::class.java
)

/**
 * ClassName -> fully qualified name(= Class.forNameの引数の形式)の変換
 */
fun ClassName.toFqName(): String = this
    .replace(".", "$")
    .replace("/", ".")

/**
 * ClassName -> Classの変換
 */
fun ClassName.toClass(isNullable: Boolean): Class<*> = kotlinizedPrimitiveClassMap
    .takeIf { !isNullable }?.let { _ -> kotlinizedPrimitiveClassMap[this] } // non-nullならprimitive型から見る
    ?: kotlinizedClassMap[this] // nullableもしくは非primitive型の場合mapから検索
    ?: Class.forName(this.toFqName()) // mapに該当する内容が無ければClass.forNameで取得
```

## 利用例
以下は`Sample`クラスに定義した`func`関数について、引数の型を`print`するサンプルです。

`func`関数の引数の型は以下のパターンを想定しています。

- `primitive`型
- ラッパー型
- `value class`
- `typealias`付き
- `static inner class`
- `inner class`
- `List`(`Collection`)
- `primitive`配列
- `generic`配列
- `vararg`引数

ただし、型引数が絡むパターンについてはより複雑になるため、ここでは簡単のため対応していません。

```kotlin
package com.wrongwrong

import kotlinx.metadata.*
import kotlinx.metadata.jvm.KotlinClassHeader
import kotlinx.metadata.jvm.KotlinClassMetadata

private typealias TypeAlias = Int

@JvmInline
value class Value(val value: Int)

class Sample {
    class StaticClass
    inner class InnerClass

    fun func(
        primitiveType: Int,
        objectType: Int?,
        value: Value,
        aliased: TypeAlias,
        static: StaticClass,
        inner: InnerClass,
        list: List<*>,
        primitiveArray: IntArray,
        genericArray: Array<Int>,
        vararg vararg: Int
    ) {}
}

fun main() {
    val javaClazz: Class<Sample> = Sample::class.java

    // kotlinx.metadataでの情報読み出し
    val kmClass: KmClass = (javaClazz.toKotlinClassMetadata() as KotlinClassMetadata.Class).toKmClass()
    val targetFunction: KmFunction = kmClass.functions.first { it.name == "func" }
    val valueParameters: List<KmValueParameter> = targetFunction.valueParameters
    val parameterClasses: List<Class<*>> = valueParameters.map {
        val type = it.type!!

        (type.classifier as KmClassifier.Class).name
            .toClass(Flag.Type.IS_NULLABLE(type.flags))
    }

    parameterClasses.forEachIndexed { idx, it -> println("${valueParameters[idx].name}: ${it.name}") }
}

fun Class<*>.toKotlinClassMetadata(): KotlinClassMetadata? = this.getAnnotation(Metadata::class.java).let {
    val header = KotlinClassHeader(
        it.kind,
        it.metadataVersion,
        it.data1,
        it.data2,
        it.extraString,
        it.packageName,
        it.extraInt
    )

    KotlinClassMetadata.read(header)
}
```

### 実行結果
実行結果は以下の通りです。

```
primitiveType: int
objectType: java.lang.Integer
value: com.wrongwrong.Value
aliased: int
static: com.wrongwrong.Sample$StaticClass
inner: com.wrongwrong.Sample$InnerClass
list: java.util.List
primitiveArray: [I
genericArray: [Ljava.lang.Object;
vararg: [I
```

ここから以下のことが読み取れます。

- `primitive`型とラッパー型が区別できている
- `value class`について`Kotlin`上の型が取得出来ている
- `typealias`が付いていても問題なく処理できている
- ネストしたクラスが問題無く処理できている
- `List`が問題無く処理できている
- 配列が問題無く処理できている
- `vararg`の場合配列として処理されている

# 終わりに
この記事では`ClassName`から`Class`(`KClass`)を取得する方法をまとめました。
特に`Class.forName`では`Class`を取得できない内容への対応に関してはもう少し何とかならないかと思っているので、何かご存じの方がいらっしゃればコメント頂ければ幸いです。