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

https://github.com/google/ksp/blob/main/docs/ksp2.md

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

これによって 内部的には 以下のメリットがあるらしい
- プログラムのライフサイクルを細かく制御
- KSPの実装が単純化-効率化

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

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

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


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

### Processor

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

```kotlin
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に処理してもらうファイル

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

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

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

### mainから動かしてみる

https://github.com/google/ksp/blob/main/docs/ksp2entrypoints.md

以下の依存
```gradle
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 はこうなる
```kotlin
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差異は以下を参照 

https://github.com/google/ksp/blob/main/docs/ksp2api.md