【kapt】処理対象クラスのパッケージを指定してファイルを書き出す【KotlinPoet】

`kapt` + `KotlinPoet`で、処理対象クラスのパッケージを指定してファイルを書き出します。
やり方は幾つか有りますが、この記事では`ClassName`からパッケージ名・クラス名の情報を取得し、`ProcessingEnvironment`から取得した`filer`を指定して書き出す方法を紹介します。

バージョン類は以下の通りです。

- `Kotlin`: `1.4.31`
- `KotlinPoet`: `1.7.2`
- `AutoCommon`: `0.11`
- `AutoService`: `1.0-rc7`

# サンプルコード
アノテーションを付けたクラスに対して、そのコードと同一パッケージに`hello from ${クラス名}`とプリントするコードを生成するサンプルです。
特に重要なのは`generateHelloWorld`関数です。

以下、こちらのサンプルコードを用いて解説していきます。

```kotlin
import com.google.auto.common.BasicAnnotationProcessor
import com.google.auto.service.AutoService
import com.google.common.collect.SetMultimap
import com.mapk.annotations.TargetAnnotation
import com.squareup.kotlinpoet.*
import javax.annotation.processing.Filer
import javax.annotation.processing.Processor
import javax.lang.model.SourceVersion
import javax.lang.model.element.Element
import javax.lang.model.element.ElementKind
import javax.lang.model.element.TypeElement

// 処理対象アノテーション
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class TargetAnnotation

@AutoService(Processor::class)
class Processor : BasicAnnotationProcessor() {
    override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.latestSupported()

    override fun getSupportedOptions(): Set<String> = setOf()

    override fun initSteps(): Iterable<ProcessingStep> {
        return listOf(TempProcessingStep(processingEnv.filer))
    }
}

class TempProcessingStep(private val filer: Filer) : BasicAnnotationProcessor.ProcessingStep {
    override fun annotations() = setOf(TargetAnnotation::class.java)

    override fun process(elementsByAnnotation: SetMultimap<Class<out Annotation>, Element>): Set<Element> {
        annotations()
            .flatMap { elementsByAnnotation[it] }
            .forEach {
                // 簡単のためクラスに付与する想定
                if (it.kind == ElementKind.CLASS) generateHelloWorld(it as TypeElement)
            }
        return emptySet()
    }

    // ハローワールドを出力する関数
    private fun generateHelloWorld(element: TypeElement) {
        val className = element.asType().asTypeName() as ClassName

        val funSpec = FunSpec.builder("helloWorld").apply {
            addStatement("""println("hello from ${className.simpleName}")""")
        }.build()

        val file = FileSpec.builder(className.packageName, "HelloFrom${className.simpleName}")
            .addFunction(funSpec)
            .build()

        file.writeTo(System.out) // デバッグ用にSystem.outにも出力
        file.writeTo(filer) // ファイルへ出力
    }
}
```

## ClassNameの取得
処理対象の`Element`が`TypeElement`であれば、`element.asType().asTypeName()`で取得した`TypeName`を`ClassName`にキャストできます。

```kotlin:ClassName取得部
private fun generateHelloWorld(element: TypeElement) {
    val className = element.asType().asTypeName() as ClassName
```

この`ClassElement`からはパッケージ名やクラス名が簡単に取得できます。

```kotlin:パッケージ名・ファイル名設定部
val file = FileSpec.builder(className.packageName, "HelloFrom${className.simpleName}")
```

### 補足
`TypeMirror.asTypeName()`は現在`Deprecated`されており、「`kotlinpoet-metadata`を使え」という旨の警告が出ますが、やり方が分からなかったためこの記事では無視しています。
この問題は恐らく`Kotlin`と`Java`で表現が違う型（`Int`や`String`）に対して起きるもので、今回取り扱う範囲では問題無いものと思っています。

## 出力先の指定
出力先は`ProcessingEnvironment`から取得した`Filer`を指定します。
`ProcessingEnvironment`は、`AbstractProcessor`（`BasicAnnotationProcessor`の親クラス）を継承している場合、`processingEnv`フィールドから取得できます。

```kotlin:ファイルへの書き出し部
file.writeTo(filer) // ファイルへ出力
```

これを指定した場合、利用側で特に指定が無ければ`${モジュールのroot}/build/generated/source/kapt/main/${指定したパッケージへのパス}`が出力先になります。

# 実行結果
`com.mapk.test`というパッケージに以下のような処理対象を用意して実行した結果を示します。

```kotlin
package com.mapk.test

import com.mapk.annotations.TargetAnnotation

@TargetAnnotation
object TestTarget

fun main() { helloWorld() }
```

## ビルドログ
`file.writeTo(System.out)`により、以下のように結果が確認できます。

```bash
# 略

> Task :test:processResources NO-SOURCE

> Task :test:kaptKotlin
package com.mapk.test

import kotlin.Unit

public fun helloWorld(): Unit {
  println("hello from TestTarget")
}

> Task :test:compileKotlin

# 略
```

## 生成されたコード
以下のようにコードが生成されます。
ちゃんとパッケージも指定できていることが分かります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280230/e8b1dfc6-2d64-6345-110a-9b384d38dfa4.png)

```kotlin:生成されたコード
package com.mapk.test

import kotlin.Unit

public fun helloWorld(): Unit {
  println("hello from TestTarget")
}
```

# 参考にさせて頂いた内容
- [square/moshi: A modern JSON library for Kotlin and Java\.](https://github.com/square/moshi)
- [Egorand/kotlin\-pizza\-factory: A simple example of annotation processing with Kotlin's kapt and Square's kotlinpoet\.](https://github.com/Egorand/kotlin-pizza-factory)