KSP2で遊ぶ

KSP2における変更

android-developers.googleblog.com

上のAndroid Developersのブログ記事にて様々な変更点が紹介されています。その中でも特にスタンドアローンに実行できる点が気になったため試してみました。

エントリーポイントのあるライブラリとして実装されているため、デバッグやテストが容易になっているらしいです。

テストを書いてみる

試しにコード生成の単体テストを書いてみます。

依存関係を追加する

まずは依存関係を追加します。

implementation("com.google.devtools.ksp:symbol-processing-aa-embeddable:<version>")

// もし足りない場合は以下の依存関係も追加する
implementation("com.google.devtools.ksp:symbol-processing-api:<version>")
implementation("com.google.devtools.ksp:symbol-processing-common-deps:<version>")

実行したいモジュールに任意のバージョンのsymbol-processing-aa-embeddableを追加します(執筆時点の最新は2.0.0-RC2-1.0.20)。 筆者の手元の環境では、KSPLoggerとKSPJvmConfigの情報が足りなかったため、symbol-processing-apiとsymbol-processing-common-depsも追加しています。

テストを書く

Processorを書いている前提で簡単なテストコードを書いてみます。

以下が最終的なテストコードです。

class SampleTest {
    // 生成ファイル用のDirの生成
    @JvmField
    @Rule
    val kotlinOutputDirFolder = TemporaryFolder()
    // その他のファイル用のDir生成
    @JvmField
    @Rule
    val dummyFolder = TemporaryFolder()

    @Test
    fun sample_test() {
        // 生成ファイル用のFileインスタンス生成
        val annotationFolder = kotlinOutputDirFolder.newFolder(
            "com", "sample", "annotation"
        )
        val generatedFile = File(annotationFolder, "GeneratedFile.kt")

        // Processorが処理するファイルの準備
        val projectFile = File(dummyFolder.root, "Project.kt")
        projectFile.writeText(
            """
            package com.sample.ksp

            @TargetAnnotation
            Interface Hoge

            annotation class TargetAnnotation()
            """.trimIndent(),
        )

        // Configの生成
        val kspConfig = KSPJvmConfig.Builder().apply {
            kotlinOutputDir = kotlinOutputDirFolder.root
            javaOutputDir = dummyFolder.newFolder("java")
            outputBaseDir = dummyFolder.newFolder("base")
            resourceOutputDir = dummyFolder.newFolder("resource")
            cachesDir = dummyFolder.newFolder("cache")
            classOutputDir =  dummyFolder.newFolder("class")
            jvmTarget = JvmTarget.DEFAULT.description
            moduleName = "<module_name>"
            sourceRoots = listOf(projectFile)
            javaSourceRoots = listOf()
            commonSourceRoots = listOf()
            projectBaseDir = dummyFolder.root
            languageVersion = "<version>"
            apiVersion = "<version>"
        }.build()
        val exitCode = KotlinSymbolProcessing(
            kspConfig, 
            listOf(HogeProcessorProvider()),
            TestKSPLogger()
        ).execute()

        assertTrue(
            exitCode == KotlinSymbolProcessing.ExitCode.OK 
                && generatedFile.exists()
        )
    }
}

class TestKSPLogger: KSPLogger {
    override fun error(message: String, symbol: KSNode?) { // operation }
    override fun exception(e: Throwable) { // operation }
    override fun info(message: String, symbol: KSNode?) { // operation }
    override fun logging(message: String, symbol: KSNode?) { // operation }
    override fun warn(message: String, symbol: KSNode?) { // operation }
}

順番に見ていきます。

フォルダを用意する

Configを作成する際、ファイルの読み込み先と出力先となるディレクトリを渡す必要があるようです。今回はTemporaryFolderを使用してフォルダを生成しました。

@JvmField
@Rule
val dummyFolder = TemporaryFolder()

テストメソッド毎にフォルダを再作成できるように、Ruleアノテーションを付けておきます。 また、Kotlinで使用する場合はJvmFieldアノテーションも必要みたいです。

読み込むファイルを用意する

SymbolProcessorの処理対象となるファイルを用意します。

val projectFile = File(dummyFolder.root, "Project.kt")
projectFile.writeText(
    """
    package com.example.ksp

    @TargetAnnotation
    Interface Hoge

    annotation class TargetAnnotation()
    """.trimIndent(),
)

Configを用意する

諸々の設定情報を渡すためにKSPJvmConfigを用意する必要があります。

val kspConfig = KSPJvmConfig.Builder().apply {
    kotlinOutputDir = kotlinOutputDirFolder.root
    javaOutputDir = dummyFolder.newFolder("java")
    outputBaseDir = dummyFolder.newFolder("base")
    resourceOutputDir = dummyFolder.newFolder("resource")
    cachesDir = dummyFolder.newFolder("cache")
    classOutputDir =  dummyFolder.newFolder("class")
    jvmTarget = JvmTarget.DEFAULT.description
    moduleName = "<module_name>"
    sourceRoots = listOf(projectFile)
    javaSourceRoots = listOf()
    commonSourceRoots = listOf()
    projectBaseDir = dummyFolder.root
    languageVersion = "<version>"
    apiVersion = "<version>"
}.build()

先ほど作成した出力先のフォルダをkotlinOutputDirに、読み込むファイルをsourceRootsに設定します。残りはよしなに埋めておきます。

Loggerを用意する

KotlinSymbolProcessingにKSPLoggerを実装したクラスを渡す必要があります。

class TestKSPLogger: KSPLogger {
    override fun error(message: String, symbol: KSNode?) { // operation }
    override fun exception(e: Throwable) { // operation }
    override fun info(message: String, symbol: KSNode?) { // operation }
    override fun logging(message: String, symbol: KSNode?) { // operation }
    override fun warn(message: String, symbol: KSNode?) { // operation }
}

KSPの処理を実行する

作成しておいたConfigとSymbolProcessorProvider、Loggerを渡して、executeメソッドを呼びます。今回は、返り値のExitCodeと生成ファイルを用いてassertしておきます。

val exitCode = KotlinSymbolProcessing(
    kspConfig, 
    listOf(HogeProcessorProvider()),
    TestKSPLogger()
).execute()
assertTrue(
    exitCode == KotlinSymbolProcessing.ExitCode.OK 
        && generatedFile.exists()
)

参考リンク

Is there any guidance or example project for how to use KSP2 kotlinlang #ksp

Prepare for K2 by ZacSweers · Pull Request #196 · ZacSweers/kotlin-compile-testing · GitHub