🌟

KSPやってみた

2021/11/24に公開

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配化に作られてしまう問題を解決できてないけど、
やりたい事が実現できたソースコードです。
https://github.com/sobaya-0141/Sample2109/pull/1

参考

https://github.com/google/ksp
https://speakerdeck.com/star_zero/droidkaigi-2021
https://star-zero.medium.com/kspを使ってコード生成してみる-1c2f421aaaa9

Discussion