KSP (Kotlin Symbol Processing) を使ってコード生成する方法について簡単に説明していきます。まとまりがない感じになっちゃって長くなりましたが、ぜひ実際に動かしてもらえると良いかなと思います。
注意: これを書いてる時点ではAlphaリリースされたばかりなので変更される可能性が高いです
KSP (Kotlin Symbol Processing) とは?
簡単に言うと、KSPはKAPTと似たような機能を提供しつつビルド速度が向上したものになります。
仕組みとしては、Kotlin Compiler Pluginのサブセットのようなものになっていて、Kotlin Compiler Pluginより簡単に実装できるようになっています。Kotlin Compiler Pluginではコードの改変なども出来たりしますが、KSPでコードファイルを追加するしかできないようになっています。
Multiplatformもサポートしています。(Alphaの段階で動くかはまだ試してないです)
これまでのAnnotation Processorと異なりKotlinのコードを理解できるので、Nullable/NonNullやsuspend functionなのかも簡単に分かるようになっています。
まだ有名ライブラリの対応されていないですが、今後対応されていくと思います。Roomでは2.3.0-beta02から実験的にサポートされました。
セットアップ
KSPでコード生成を実装する側はdependenciesにKSPのライブラリを追加します。
dependencies {
implementation("com.google.devtools.ksp:symbol-processing-api:1.4.30-1.0.0-alpha02")
}
KSPを使う側は以下のような感じで、KSPのプラグインを適用してdependenciesでkspで指定する感じです。
plugins {
id("com.google.devtools.ksp") version "1.4.30-1.0.0-alpha02"
}
dependencies {
ksp(...)
}
詳しくはQuickstartのドキュメントの方を見てもらえると。
SymbolProcessor
KSPを実装するには、 SymbolProcessor
を継承したクラスを作成します。
init
と process
と finish
というメソッドを実装する必要があります。( finish
は実装しなくても良くなる予定です。 > PullRequest )
init
Prcessorが初期化処理になります。渡されてくる CodeGenerator や KSPLogger などを変数に設定する感じになると思います。
また、Gradleに設定されたオプションもここで取得できるようになっっています。
ksp {
arg("option1", "value1")
arg("option2", "value2")
}
上のようにGradleにオプションを設定したものを取得するには以下のように取得できます。
process
ここで実際に既存のコードを解析して、それを元に新たにコードを生成していきます。引数で渡されてくる Resolver
を使ってコードを取得することが可能です。
いくつか実装例を後述しています。
finish
finishは必要であれば実装する感じになりそうです。あんまり実装する機会はなさそうなので、オプショナルになる予定です。PullRequest
META-INF.services
Annotation Processorと同じように META-INF.services
の設定が必要です。
com.google.devtools.ksp.processing.SymbolProcessor
を作って、そこに実行するクラスを指定します。
サンプルをいくつか
サンプルをいくつか見ながら少し説明していきたいと思います。
単純なコード生成
これは単純にコードを生成する例です。コードを見ればだいたい分かると思います。
CodeGenerator
を使ってコードファイルを生成できます。
注意としては、コード生成した場合は再び process
メソッドが呼ばれるので、フラグを使って何度も実行されるのを防ぎます。この制御がないとコードが繰り返し生成されて終了しなくなります。
アノテーションからコード生成する
Resolver.getSymbolsWithAnnotation
を使ってアノテーションがついてるコード情報を取得できます。
アノテーション以外にも、すべてのファイルを取得する Resolver.getAllFiles
や特定のクラスから取得する Resolver.getClassDeclarationByName
もあります。
この例ではクラスをつけられたアノテーションです。そのため filterIsInstance
をつかって絞り込んでいます。 KSClassDeclaration
はクラスを表すものになります。
何かを元にコードを生成するときは CodeGenerator.createNewFile
に渡してる Dependencies
が重要になります。
Dependenciesについて
CodeGenerator.createNewFile
に渡す Dependencies
は Incremental Processing に重要なものになります。これを正しく設定しないと意図した挙動にならないことになります。
第1引数のBooleanについて、すいませんが、まだぼくの理解足らず挙動の違いが分かりませんでした。
第2引数(可変長引数)にはそのコードファイルの生成の元となったファイル(KSFile
)を渡します。KSClassDeclaration
などの KSDeclaration
には containingFile
があるので、それから KSFile
が取得できます。
例えば、複数のクラスから1つのコードファイルを生成する場合は、その複数クラスの KSFile
を指定する必要があります。
ドキュメントがあるのでそちらも見てもらえると。
https://github.com/google/ksp/blob/master/docs/incremental.md
別のコード生成されたものに依存してる場合
例えば、以下のように別のProcessorで生成されたものを使用してるコードを元にコードを生成したい場合です(うまく説明できてないかも)
Processorの処理タイミングでは、Sampleクラスがまだ生成されてない可能性があります。それをうまく処理するには validate
メソッドが用意されてるので、それを使用します。
validate
で処理しなかったものは戻り値として返すことで、再び呼ばれたときに処理できるようになります。
Visitorパターンで処理する
KSVisitor
を使うことでVisitorパターンでコード解析することもできます。(以下の例では KSVistorVoid
を使ってる)
順番に定義を訪問していき処理をすることができます。
使用するときは accept
を使って呼び出します。
必要に応じて使用していくと良いと思います。
書いてこなかったですが、 KSClassDeclaration.getAllFunctions
のように直接メソッドを取得することもできます。
まとめ
うまくまとめられていないですが、個人的にKSPはAnnotation Processorよりだいぶ書きやすい印象です。Kotlinのことも理解してくれてるので、特に難しいことをすることなくコードを解析できます。
ビルド速度も改善されますし、今後KSPが増えてくれると嬉しいです。