Upgrade to Pro — share decks privately, control downloads, hide ads and more …

KSPを使ってコード生成

 KSPを使ってコード生成

Takuji Nishibayashi

December 05, 2023
Tweet

More Decks by Takuji Nishibayashi

Other Decks in Technology

Transcript

  1. KSP を使ってコード生成
    関西モバイルアプリ研究会 A
    @takuji31

    View full-size slide

  2. 自己紹介
    西林 拓志(にしばやし たくじ)
    Twitter/GitHub takuji31
    株式会社はてな
    Android アプリケーションエンジ
    ニア
    Android (2009〜)
    Kotlin (2014〜)
    2

    View full-size slide

  3. KSP 使ってますか?
    3

    View full-size slide

  4. 今日は KSP のプロセッサーの作り方(≠ 使い方)について話
    します
    4

    View full-size slide

  5. Kotlin Symbol Processor
    6

    View full-size slide

  6. コードにつけられたアノテーションを処理する
    8

    View full-size slide

  7. Kotlin friendly
    9

    View full-size slide

  8. Incremental proccessing
    10

    View full-size slide

  9. KMP 対応
    11

    View full-size slide

  10. Supported libs
    Dagger / Hilt (alpha)
    Room
    Moshi
    Glide
    etc.
    12

    View full-size slide

  11. プロセッサーの
    作り方
    13

    View full-size slide

  12. source code
    //
    これを
    @SimpleGeneration
    interface Hoge
    //
    こうだ
    abstract class AbstractHoge: Hoge
    14

    View full-size slide

  13. build.gradle.kts
    // ...
    dependencies {
    // KSP
    のAPI
    implementation("com.google.devtools.ksp:symbol-processing-api:1.9.21-1.0.15")
    // KotlinPoet
    implementation("com.squareup:kotlinpoet:1.15.3")
    // KotlinPoet
    のKSP
    用拡張
    implementation("com.squareup:kotlinpoet-ksp:1.15.3")
    }
    15

    View full-size slide

  14. SymbolProcessor
    class ExampleSymbolProcessor(
    private val codeGenerator: CodeGenerator,
    private val logger: KSPLogger
    ) : SymbolProcessor {
    override fun process(resolver: Resolver): List {
    resolver
    .getSymbolsWithAnnotation(SimpleGeneration::class.qualifiedName!!)
    .filterIsInstance()
    .forEach { it.accept(SimpleGenerationVisitor(codeGenerator, logger), Unit) }
    return emptyList()
    }
    }
    16

    View full-size slide

  15. Visitor
    class SimpleGenerationVisitor(
    private val codeGenerator: CodeGenerator,
    private val logger: KSPLogger
    ) : KSVisitorVoid() {
    override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) {
    if (classDeclaration.classKind != ClassKind.INTERFACE) {
    logger.error("Only interface allowed", classDeclaration)
    return
    }
    val packageName = classDeclaration.packageName.asString()
    val className = ClassName(packageName, "Abstract" + classDeclaration.simpleName.asString())
    val typeSpec = TypeSpec.classBuilder(className)
    .addModifiers(KModifier.ABSTRACT)
    .addSuperinterface(classDeclaration.toClassName())
    FileSpec.builder(packageName, className.simpleName)
    .addType(typeSpec.build())
    .build()
    .writeTo(
    codeGenerator,
    Dependencies(
    aggregating = false,
    classDeclaration.containingFile!!
    )
    )
    }
    } 17

    View full-size slide

  16. SymbolProcessorProvider
    class ExampleSymbolProcessorProvider : SymbolProcessorProvider {
    override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
    return ExampleSymbolProcessor(environment.codeGenerator, environment.logger)
    }
    }
    18

    View full-size slide

  17. Service Provider
    // resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider
    jp.takuji31.kotlinfest2022.compiler.ExampleSymbolProcessorProvider
    19

    View full-size slide

  18. Incremental
    Processing
    20

    View full-size slide

  19. 生成するファイルの依存を定義すれば Processor 側で適
    切に処理される
    21

    View full-size slide

  20. Isolated
    Dependencies(aggregating = false, classDeclaration.containingFile!!)
    24

    View full-size slide

  21. Aggregated
    25

    View full-size slide

  22. Aggregated (集約)
    val dependencies: Array = // ...
    Dependencies(aggregating = true, *dependencies.mapNotNull { it.containingFile })
    27

    View full-size slide

  23. 依存をちゃんと指定しないと「なぜかコード生成されな
    い」みたいな事態になる
    28

    View full-size slide

  24. tschuchortdev/kotlin-compile-testing
    30

    View full-size slide

  25. テストコード
    val source = SourceFile.kotlin(
    "ExampleClass.kt", """
    package jp.takuji31.kotlinfest2022.compiler
    import jp.takuji31.kotlinfest2022.compiler.annotation.SimpleGeneration
    @SimpleGeneration
    interface SimpleInterface {
    fun printHelloWorld()
    }
    """.trimIndent()
    )
    31

    View full-size slide

  26. テストコード
    val compilation = KotlinCompilation().apply {
    sources = listOf(source)
    inheritClassPath = true
    symbolProcessorProviders = listOf(ExampleSymbolProcessorProvider())
    kspWithCompilation = true
    }
    val result = compilation.compile()
    assertThat(result.exitCode)
    .isEqualTo(KotlinCompilation.ExitCode.OK)
    32

    View full-size slide

  27. ドキュメント/サンプルコード
    https://kotlinlang.org/docs/ksp-overview.html
    公式ドキュメント
    https://github.com/google/ksp/tree/main/examples/playground
    公式サンプル
    https://github.com/takuji31/kotlinfest2022-ksp-example
    今回のスライドに出てきたソースコード
    https://github.com/takuji31/navigation-compose-screen
    複雑な例
    https://speakerdeck.com/takuji31/kotlin-symbol-processing-api-ksp-woshi-tute-
    kotlin-a-purikesiyonnokai-fa-woxiao-lu-hua-suru
    Kotlin Fest 2022 で発表した時のスライド
    もう少し踏み込んだ話はこちら 33

    View full-size slide

  28. Enjoy KSP Life!
    34

    View full-size slide