KSPことはじめ

KSPについて導入を学んだので記事にします。
公式ドキュメントを読んでのまとめです。KotlinPoetとの併用を一部書きますが、詳しくはドキュメントをみてくださいm(_ _)m

https://kotlinlang.org/docs/ksp-overview.html

https://github.com/google/ksp

https://square.github.io/kotlinpoet/



# 概要

軽量なコンパイラ・プラグインを開発するためのAPI

クラス、クラス・メンバー、関数、関連するパラメータにアクセスし、解析してコードを生成することができる。

# メリット

1. KSPはKotlinのコンパイラと直接統合されいるため、Javaのアノテーションプロセッサやkaptよりも高速に動作
2. Kotlinの型システムにより、型の安全性を保持しながらコード生成や解析を行うことができるとのこと。

## デメリット

- コンパイラの背景知識や、特定のコンパイラの実装の詳細にある程度精通している必要がある
- プラグインは特定のコンパイラのバージョンと密接に結びついていることが多いため、コンパイラのバージョンアップは都度プラグインを更新を必要とすることが多い

（ が、KSPはコンパイラの変更を隠蔽するように設計されているとのことなので、メンテのコストは他のコンパイラプラグインより低いことを謳っている　）

## 他のコンパイラプラグインとの比較

- kotlinc

[kotlincとの比較](https://kotlinlang.org/docs/ksp-why-ksp.html#comparison-to-kotlinc-compiler-plugins)。KSPはkotlincのサブセット。kotlincではできてKSPではできないことがある代わりに、一般的なユースケースを満たそうとしている。

例えば、kotlincでは、コードの改変ができるがKSPではできない。KSPで提供している主な機能は、アノテーションなどのシンボルからコードを生成することに絞られている

KSPはソースコードの式レベルの情報を調査したり、ソースコードを動的に変更したりすることはできず、そもそもそれを目的にしてない。

- reflection

**[reflectionとの比較](https://kotlinlang.org/docs/ksp-why-ksp.html#comparison-to-reflection)** 。KSPは型参照を明示的に解決する必要がある。reflectionより型安全によっている。

- kapt

[kaptとの比較](https://kotlinlang.org/docs/ksp-why-ksp.html#comparison-to-kapt)。kaptはJavaアノテーションプロセッサを使用しているのでJVMに縛られる。KSPはJVMに縛られないビルドパフォーマンスの向上、より慣用的なKotlin API、Kotlin専用のシンボルを理解できる。

要するに、kaptよりもKotlinに最適化されている。

# 使用方法

**エントリポイントとなるSymbolProcessorProviderを定義**

```kotlin
interface SymbolProcessorProvider {
    fun create(environment: SymbolProcessorEnvironment): SymbolProcessor
}
```

このProviderは[METAINF](https://github.com/google/ksp/blob/main/examples/playground/test-processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider)を定義してコンパイラに伝えるとコード生成を行なってくれる。


**SymbolProcessorを実装する**

```kotlin
interface SymbolProcessor {
    fun process(resolver: Resolver): List<KSAnnotated>
    fun finish() {}
    fun onError() {}
}
```

processに処理を記述する

- Resolverは[シンボル](https://kotlinlang.org/docs/ksp-overview.html#how-ksp-looks-at-source-files)、ソースコード内の各要素（クラス、関数、プロパティ、型パラメータ、コンストラクタなど。）にアクセスするためのインターフェース
- 以下のように、MyAnnotaionがついたシンボル( KSAnnotated）を取得したりできる
    
```kotlin
override fun process(resolver: Resolver): List<KSAnnotated> {
    val symbols = resolver.getSymbolsWithAnnotation("com.example.MyAnnotation")
    symbols.forEach { symbol ->
        // 何らかの処理
    }
    return emptyList() // この例では空のリストを返していますが、実際には処理すべきアノテーションを含むシンボルのリストを返すことができます。
}
 ```
    

シンボルは`KSNode`　という基底インターフェースがあり、

```kotlin
interface KSNode {
    val origin: Origin
    val location: Location
    val parent: KSNode?
    fun <D, R> accept(visitor: KSVisitor<D, R>, data: D): R
}
```

対象のsymbolを[KSVisiter](https://github.com/google/ksp/blob/e9352b4ba64b2a800cd2b1ffe50ba795a3dfde5f/api/src/main/kotlin/com/google/devtools/ksp/symbol/KSVisitor.kt#L22)を用いてコード生成処理をacceptできる。これは以下サンプルのBuilderVisitorが良い例で、サンプルではクラスのビルダーをKSPで生成している。

https://github.com/google/ksp/blob/main/examples/playground/test-processor/src/main/kotlin/BuilderProcessor.kt


ちなみに、ここの設計思想は[Visiterパターン](https://ja.wikipedia.org/wiki/Visitor_%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3#:~:text=Visitor%20%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3%E3%81%AF%E3%80%81%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E6%8C%87%E5%90%91,%E8%BF%BD%E5%8A%A0%E3%81%99%E3%82%8B%E3%81%93%E3%81%A8%E3%81%8C%E3%81%A7%E3%81%8D%E3%82%8B%E3%80%82)　 というGofのものがベース

KSVisiterの訪問処理をざっくりまとめると以下になる

```kotlin
defaultのハンドラ。
defaultHandler(node: KSNode, data: D): R

アノテーションが付与されたシンボルを訪問する際の処理。
visitAnnotated(annotated: KSAnnotated, data: D): R

クラス宣言を訪問する際の処理。
visitClassDeclaration(classDeclaration: KSClassDeclaration, data: D): R

関数宣言を訪問する際の処理。
visitFunctionDeclaration(function: KSFunctionDeclaration, data: D): R

プロパティ宣言を訪問する際の処理。
visitPropertyDeclaration(property: KSPropertyDeclaration, data: D): R

タイプエイリアスを訪問する際の処理。
visitTypeAlias(typeAlias: KSTypeAlias, data: D): R

タイプパラメータを訪問する際の処理。
visitTypeParameter(typeParameter: KSTypeParameter, data: D): R

関数やコンストラクタの値パラメータを訪問する際の処理。
visitValueParameter(valueParameter: KSValueParameter, data: D): R
```

各シンボルへの参照方法は、公式で[ダイアグラム](https://kotlinlang.org/docs/ksp-additional-details.html)をまとめてくれているので参考になる

![Something went wrong]()

## Kotlin Poet

[KotlinPoet - KotlinPoet](https://square.github.io/kotlinpoet/)

というKotlinのコードをプログラム的に生成するための強力なライブラリがあり、以下のようなに（比較的）型安全にコード生成用のコードが書ける


```kotlin
val helloWorld = TypeSpec.classBuilder("HelloWorld")
    .addFunction(FunSpec.builder("printHello")
        .addStatement("println(%S)", "Hello, KotlinPoet!")
        .build())
    .build()

val file = FileSpec.builder("com.example", "HelloWorld")
    .addType(helloWorld)
    .build()

file.writeTo(System.out)

// 上記から以下を生成できる

package com.example

class HelloWorld {
    fun printHello() {
        println("Hello, KotlinPoet!")
    }
}
```

これと組み合わせると、Visiter内を型安全に生成コードを書きつつ、可読性が上がる。

サンプルのBuilderVisitorの`visitFunctionDeclaration`をKotlin Poetで書き直してみると以下になる

```kotlin
val parent = function.parentDeclaration as KSClassDeclaration
val packageName = parent.containingFile!!.packageName.asString()
val className = "${parent.simpleName.asString()}Builder"

val fileSpecBuilder = FileSpec.builder(packageName, className)
    .addImport("HELLO", "")

val classBuilder = TypeSpec.classBuilder(className)

function.parameters.forEach { parameter ->
    val name = parameter.name!!.asString()
    val typeName = parameter.type.resolve().toTypeName()

    // Property
    classBuilder.addProperty(
        PropertySpec.builder(name, typeName.copy(nullable = true))
            .mutable(true)
            .initializer("null")
            .build()
    )

    // Function
    classBuilder.addFunction(
        FunSpec.builder("with${name.replaceFirstChar { it.uppercase() }}")
            .returns(ClassName(packageName, className))
            .addParameter(name, typeName)
            .addStatement("this.$name = $name")
            .addStatement("return this")
            .build()
    )
}

// Build function
classBuilder.addFunction(
    FunSpec.builder("build")
        .returns(ClassName(packageName, parent.simpleName.asString()))
        .addStatement(
            "return %T(${function.parameters.joinToString { "${it.name!!.asString()}!!" }})",
            ClassName(packageName, parent.simpleName.asString())
        )
        .build()
)

fileSpecBuilder.addType(classBuilder.build())

val fileSpec = fileSpecBuilder.build()
fileSpec.writeTo(codeGenerator, Dependencies(true, function.containingFile!!))
```

## 終わり