KSPやってみた
KSPを触ってみて色々と苦労したのでメモです。
最初に実現しようとした事は達成できてないオチもあります。
KSPを使うモジュール
build.gradle
plugins {
id 'com.android.library'
id 'kotlin-android'
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
id "com.google.devtools.ksp" version "1.5.31-1.0.0"
}
dependencies {
implementation project(":Wasabi")
ksp project(":Wasabi")
}
これだけです。
Wasabiと言うモジュールがKSPの処理が書かれてるモジュールです。
KSP本体モジュール
build.gradle
plugins {
id 'org.jetbrains.kotlin.jvm'
id "com.google.devtools.ksp" version "1.5.31-1.0.0"
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib"
implementation "com.google.devtools.ksp:symbol-processing-api:1.5.31-1.0.0"
}
本当にこれだけです。
Android関係無いのでAndroid系の情報も書かれてません
META-INF/services
モジュール内のファイルはこのような構成になっています。
META-INF/service配下にProviderとProcessorがどこにいるか指定するファイルを配置します。
※ここが公式などを見てもファイル名が違い動かなくて苦しんだポイント
com.google.devtools.ksp.processing.SymbolProcessorProvider
と
javax.annotation.processing.Processor
と言うファイルを用意します。
ファイルの中身はそれぞれ
sobaya.lib.wasabi.WasabiProvider
sobaya.lib.wasabi.WasabiProcessor
となります。
Provider
package sobaya.lib.wasabi
import com.google.devtools.ksp.processing.SymbolProcessor
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
import com.google.devtools.ksp.processing.SymbolProcessorProvider
class WasabiProvider : SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
return WasabiProcessor(environment.codeGenerator, environment.logger)
}
}
こちらはProcessorのインスタンスを返してあげるだけです。
Processor
override fun process(resolver: Resolver): List<KSAnnotated> {
if (invoked) {
return emptyList()
}
val functions = ArrayList<String>()
val viewModels = getViewModels(resolver)
val states = getStates(resolver)
states.forEach { (viewModelPath, fieldName) ->
val viewModel = viewModels.find {viewModelPath.contains(it) }
viewModel?.let {
functions.add(makeFunction(viewModel, fieldName))
}
}
val file = codeGenerator.createNewFile(
Dependencies(false),
"sobaya.wasabi",
"StateExtensions"
)
file.write(functions.joinToString("\n").toByteArray())
file.close()
invoked = true
return emptyList()
}
長いのでまずは本編だけ記載しました。
functions
にKSPで作りたい関数の文字列が入ってきます。
codeGenerator.createNewFile(
Dependencies(false),
"sobaya.wasabi",
"StateExtensions"
)
functionsに入っている文字列を↑のファイルに書き込みます。
この例だとsobaya.wasabi.StateExtensions.kt
ファイルに書き込まれます。
今回は省略していますが、第四引数に拡張子を指定するとテキストファイルなども作成可能です。
クラスにつけるAnnotationから情報取特
val viewModelSymbol = resolver.getSymbolsWithAnnotation("sobaya.lib.wasabi.ViewModel")
viewModelSymbol.filterIsInstance<KSClassDeclaration>().forEach {
viewModels.add("${it.packageName.asString()}.${it.simpleName.asString()}")
}
getSymbolsWithAnnotation
に自作したアノテーションを伝えてアノテーションが付与されている場所を取得します。
filterIsInstance<KSClassDeclaration>
今回はクラスに付けるのでKSClassDeclarationでフィルター
it.packageName.asString()
でパッケージ名
it.simpleName.asString()
でクラス名が取得できます。
フィールドにつけるAnnotationから情報取得
val stateSymbol = resolver.getSymbolsWithAnnotation("sobaya.lib.wasabi.State")
stateSymbol.filterIsInstance<KSPropertyDeclaration>().forEach { it ->
it.annotations.forEach {
val filePath = (it.parent?.location as FileLocation).filePath
val file = File(filePath)
val fieldName = it.parent.toString()
states.add(file.path.replace("/", ".") to fieldName)
}
}
getSymbolsWithAnnotation
でアノテーションの情報を取得するのは共通です。
KSPropertyDeclaration
今回は変数の情報が欲しいのでこちらでフィルタリングします。
KSPropertyDeclaration.annotationsの内容を確認し、
(it.parent?.location as FileLocation).filePath
で何のファイルのどこに書かれているか取得
it.parent.toString()
で変数名が取得できます。
例)
@Test
val test = Data()
ならtest
が取得可能
型も取得できますが、今回は使わなかったのでメモするの忘れました。
最後に
プロジェクトをreBuildすると指定したファイルが勝手に作成されます。
今回はViewModelのState管理でdata classを使う時に
state.value = state.value.copy(list = it)
と書くのが面倒なので楽にする機能を作ろうと思ったのですが、
余裕で失敗しました。
でも、ビルド時に機能追加できるのは面白いのでアイデア次第で夢が広がります。
↑コードの掃除やdebug配化に作られてしまう問題を解決できてないけど、
やりたい事が実現できたソースコードです。
参考
Discussion