1
1

KSP2 で main 関数から Processorを動かしてみる

Posted at

KSP2 で KSPは Compiler Plugin ではなくなり、AndroidLintやIntelljIdeaなど と共有されているKotlin Compiler APIs 上で 動くようになったとのこと

これによって 内部的には 以下のメリットがあるらしい

  • プログラムのライフサイクルを細かく制御
  • KSPの実装が単純化-効率化

クライアント視点だと、以下が大きな変更点

  • 別のプログラムから、Processorへのエントリーポイントの作成が容易になった

これは特にテストで有用で、Processorをプログラムから呼び出せて 生成されるファイルのPathも指定できるようになる。

ということで、main() 関数から Processor を動かしてみる。

Processor

シンプルにアノテーションをみつけて、シンボル情報をもとに別のクラスを作る みたいなことをやっている。
ここはKSPとKSP2で変わらない

class TestProcessor(
    private val codeGenerator: CodeGenerator,
    private val kspLogger: KSPLogger,
) : SymbolProcessor {
    override fun process(resolver: Resolver): List<KSAnnotated> {
        resolver
            .getSymbolsWithAnnotation(GreetingAnnotation::class.qualifiedName!!)
            .filterIsInstance<KSClassDeclaration>()
            .forEach { ksClassDeclaration ->
                FileSpec
                    .builder(ksClassDeclaration.packageName.getQualifier(), ksClassDeclaration.simpleName.asString())
                    .addType(
                        TypeSpec
                            .classBuilder("generate" + ksClassDeclaration.simpleName.asString())
                            .addFunction(
                                FunSpec
                                    .builder("test")
                                    .addStatement("println(%S)", "Hello, World!")
                                    .build(),
                            ).build(),
                    ).build()
                    .writeTo(codeGenerator, Dependencies(false, ksClassDeclaration.containingFile!!))
                kspLogger.info("info: ${ksClassDeclaration.containingFile!!}")
            }
        return emptyList()
    }
}

ProcessorProviderを サービスプロバイダに設定する。Processor専用のモジュールを作る、といったやり方は変わらず。

└── src
    └── main
        ├── kotlin
        │   └── TestProcessor.kt
        └── resources
            └── META-INF
                └── services
                    └── com.google.devtools.ksp.processing.SymbolProcessorProvider

KSPに処理してもらうファイル

@GreetingAnnotation
class Greeting(
    val name: String,
    val age: Int,
    val f: () -> Unit
) {
    fun normalFunction(): String = name + age
    fun returnFunctionFunction(): () -> Unit = f
}

👇 こんなのを作ってもらう

public class generateGreeting {
  public fun test() {
    println("Hello, World!")
  }
}

mainから動かしてみる

以下の依存

implementation("com.google.devtools.ksp:symbol-processing-api:$kspVersion")
implementation("com.google.devtools.ksp:symbol-processing-common-deps:$kspVersion")
implementation("com.google.devtools.ksp:symbol-processing-aa-embeddable:$kspVersion")
implementation(project(":test-processor"))
ksp(project(":test-processor"))

symbol-processing-common-deps は doc には載ってないが、KSPJvmConfig といった Processor を動かす際の設定 を記述するクラスのimport に必要。

main はこうなる

import com.google.devtools.ksp.impl.KotlinSymbolProcessing
import com.google.devtools.ksp.processing.KSPJvmConfig
import com.google.devtools.ksp.processing.KspGradleLogger
import com.google.devtools.ksp.processing.SymbolProcessorProvider
import java.io.File
import java.net.URLClassLoader
import java.util.ServiceLoader

fun main() {
    // Step 1: Load processors
    val classpath = listOf("test-processor/build/classes/kotlin/main")
    val processorClassloader = URLClassLoader(classpath.map { File(it).toURI().toURL() }.toTypedArray())
    val processorProviders = ServiceLoader.load(
        processorClassloader.loadClass("com.google.devtools.ksp.processing.SymbolProcessorProvider"),
        processorClassloader
    ).toList() as List<SymbolProcessorProvider>

    // Step 2: Implement or use KSPLogger
    val logger = KspGradleLogger(KspGradleLogger.LOGGING_LEVEL_INFO)

    // Step 3: Fill KSPConfig
    val kspConfig = KSPJvmConfig.Builder().apply {
        moduleName = "mainsrc"
        sourceRoots = listOf(File("mainsrc/src/main/kotlin"))
        kotlinOutputDir = File("mainsrc/build/generated/ksp/main/kotlin")
        javaOutputDir = File("mainsrc/build/generated/ksp/main/java")
        jvmTarget = "1.8"
        projectBaseDir = File(".")
        outputBaseDir = File("mainsrc/build/generated/ksp/main")
        cachesDir = File("mainsrc/build/generated/ksp/main/caches")
        classOutputDir = File("mainsrc/build/generated/ksp/main/classes")
        resourceOutputDir = File("mainsrc/build/generated/ksp/main/resources")
        apiVersion = "2.0.0-1.0.23"
        languageVersion = "2.0"
    }.build()

    // Step 4: Run KotlinSymbolProcessing
    val exitCode = KotlinSymbolProcessing(kspConfig, processorProviders, logger).execute()

    println("KSP finished with exit code: $exitCode")
}
  1. Processor のclass path は , Processor のソースファイルではなく, classPath である点に注意(たいてい build/classes/kotlin/main 配下にあるはず)

  2. loggerはKSLoggerでもよい

  3. KSPConfig: かなり柔軟に色々指定できる

  4. 実行

これだけで mainから呼び出しが可能

終わり

ほか、細かなAPI差異は以下を参照

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1