Kotlin Symbol Processing (KSP) для Kotlin: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс
Kotlin Symbol Processing (KSP) Kotlin: методы, примеры, установка | PythonLib

Kotlin Symbol Processing (KSP) для Kotlin: полное руководство с примерами

1. Что это и зачем нужна

Kotlin Symbol Processing (KSP) — это библиотека и компиляторный плагин для языка Kotlin, разработанная компанией JetBrains. Её основная задача — предоставить разработчикам простой, быстрый и надёжный API для анализа исходного кода Kotlin на этапе компиляции и генерации на его основе нового кода (кодогенерация). KSP является прямой альтернативой старому механизму javax.annotation.processing (Java Annotation Processing), который был создан для Java и имеет ряд ограничений при работе с Kotlin.

Зачем нужна KSP? В современной разработке на Kotlin часто требуется автоматически создавать шаблонный код: DTO-классы, мапперы, Parcelable-реализации, Room-сущности, Moshi-адаптеры и многое другое. KSP позволяет делать это на этапе компиляции, без рефлексии в рантайме, что повышает производительность приложения. В отличие от KAPT (Kotlin Annotation Processing Tool), KSP работает напрямую с AST-деревом Kotlin, минуя промежуточную Java-стадию, что даёт прирост скорости в 2–4 раза. KSP также лучше поддерживает новые языковые конструкции Kotlin (suspend-функции, inline-классы, value-классы) и не требует генерации Java-заглушек.

KSP идеально подходит для создания собственных библиотек кодогенерации, фреймворков (аналогов Dagger, Room, Glide) и инструментов метапрограммирования. Она активно используется в экосистеме Kotlin: Compose, Room, Moshi, Koin, Apollo GraphQL и многих других популярных библиотеках.

2. Установка

KSP устанавливается не через pip или npm, а через систему сборки Gradle. Для Kotlin-проекта потребуется добавить плагин KSP и его версию, соответствующую версии Kotlin.

Точная команда для добавления в build.gradle.kts (корневой проект):

// build.gradle.kts (корневой уровень)
plugins {
    id("org.jetbrains.kotlin.jvm") version "1.9.22" apply false
    id("com.google.devtools.ksp") version "1.9.22-1.0.17" apply false
}

// build.gradle.kts (модуль приложения)
plugins {
    id("org.jetbrains.kotlin.jvm")
    id("com.google.devtools.ksp")
}

dependencies {
    implementation("org.jetbrains.kotlin:kotlin-stdlib:1.9.22")
    implementation("com.squareup:kotlinpoet:1.16.0") // для генерации кода
    ksp("com.example:my-processor:1.0.0") // ваш процессор
}

Для Gradle Groovy (build.gradle):

plugins {
    id 'org.jetbrains.kotlin.jvm' version '1.9.22'
    id 'com.google.devtools.ksp' version '1.9.22-1.0.17'
}

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib:1.9.22"
    implementation "com.squareup:kotlinpoet:1.16.0"
    ksp "com.example:my-processor:1.0.0"
}
Важно: Версия плагина KSP должна строго соответствовать версии Kotlin. Например, для Kotlin 1.9.22 используется KSP 1.9.22-1.0.17. Актуальные версии смотрите на GitHub-релизах KSP.

3. Быстрый старт — минимальный рабочий пример

Создадим простейший процессор, который находит все классы с аннотацией @Builder и генерирует для них Builder-класс.

Шаг 1: Создайте модуль процессора (например, processor)

// processor/build.gradle.kts
plugins {
    id("org.jetbrains.kotlin.jvm")
    id("com.google.devtools.ksp")
}

dependencies {
    implementation("com.squareup:kotlinpoet:1.16.0")
    implementation("com.squareup:kotlinpoet-ksp:1.16.0")
    implementation("com.google.devtools.ksp:symbol-processing-api:1.9.22-1.0.17")
}

// processor/src/main/kotlin/com/example/BuilderProcessor.kt
package com.example

import com.google.devtools.ksp.processing.*
import com.google.devtools.ksp.symbol.*
import com.squareup.kotlinpoet.*
import com.squareup.kotlinpoet.ksp.toClassName

@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class Builder

class BuilderProcessorProvider : SymbolProcessorProvider {
    override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
        return BuilderProcessor(environment.codeGenerator)
    }
}

class BuilderProcessor(private val codeGenerator: CodeGenerator) : SymbolProcessor {
    override fun process(resolver: Resolver): List<KSAnnotated> {
        val symbols = resolver.getSymbolsWithAnnotation("com.example.Builder")
        symbols.filterIsInstance<KSClassDeclaration>().forEach { classDecl ->
            generateBuilder(classDecl)
        }
        return emptyList()
    }

    private fun generateBuilder(classDecl: KSClassDeclaration) {
        val className = classDecl.simpleName.asString()
        val packageName = classDecl.packageName.asString()
        val builderClassName = "${className}Builder"

        val props = classDecl.getAllProperties().map { prop ->
            prop.name to prop.type.resolve().toClassName()
        }

        val builderClass = TypeSpec.classBuilder(builderClassName)
            .primaryConstructor(
                FunSpec.constructorBuilder()
                    .addParameters(props.map { (name, type) ->
                        ParameterSpec.builder(name, type).build()
                    })
                    .build()
            )
            .addProperties(props.map { (name, type) ->
                PropertySpec.builder(name, type)
                    .initializer(name)
                    .build()
            })
            .addFunction(
                FunSpec.builder("build")
                    .returns(classDecl.toClassName())
                    .addStatement("return %T(%L)", classDecl.toClassName(), props.joinToString(", ") { it.first })
                    .build()
            )
            .build()

        val file = FileSpec.builder(packageName, builderClassName)
            .addType(builderClass)
            .build()

        file.writeTo(codeGenerator, Dependencies(false))
    }
}

Шаг 2: Используйте процессор в основном модуле

// app/src/main/kotlin/com/example/User.kt
package com.example

@Builder
data class User(val name: String, val age: Int)

// После компиляции будет сгенерирован:
// public class UserBuilder(val name: String, val age: Int) {
//     fun build(): User = User(name, age)
// }

fun main() {
    val user = UserBuilder("Alice", 30).build()
    println(user) // User(name=Alice, age=30)
}

4. Основные методы/функции/классы — минимум 8 штук

4.1 SymbolProcessor.process(resolver: Resolver): List<KSAnnotated>

Сигнатура: fun process(resolver: Resolver): List<KSAnnotated>

Что делает: Главный метод процессора. Вызывается компилятором для каждого раунда обработки. Получает объект Resolver для доступа к символам исходного кода. Возвращает список символов, которые не удалось обработать (для повторной обработки в следующем раунде).

Пример:

override fun process(resolver: Resolver): List<KSAnnotated> {
    val symbols = resolver.getSymbolsWithAnnotation("com.example.MyAnnotation")
    val deferred = mutableListOf<KSAnnotated>()
    symbols.forEach { symbol ->
        if (symbol is KSClassDeclaration) {
            try {
                processClass(symbol)
            } catch (e: Exception) {
                deferred.add(symbol) // откладываем на следующий раунд
            }
        }
    }
    return deferred
}

4.2 Resolver.getSymbolsWithAnnotation(annotationName: String): Sequence<KSAnnotated>

Сигнатура: fun getSymbolsWithAnnotation(annotationName: String): Sequence<KSAnnotated>

Что делает: Возвращает последовательность всех символов (классов, функций, свойств), аннотированных указанной аннотацией. Это основной способ найти цели для кодогенерации.

Пример:

val symbols = resolver.getSymbolsWithAnnotation("com.example.JsonSerializable")
symbols.filterIsInstance<KSClassDeclaration>().forEach { classDecl ->
    println("Found class: ${classDecl.simpleName.asString()}")
}

4.3 KSClassDeclaration.getAllProperties(): Sequence<KSPropertyDeclaration>

Сигнатура: fun getAllProperties(): Sequence<KSPropertyDeclaration>

Что делает: Возвращает все свойства (поля) класса, включая унаследованные. Полезно для генерации Builder'ов, DTO-мапперов и т.д.

Пример:

val classDecl = resolver.getClassDeclarationByName("com.example.User")
classDecl?.getAllProperties()?.forEach { prop ->
    println("Property: ${prop.simpleName.asString()} : ${prop.type.resolve().toClassName()}")
}

4.4 KSPropertyDeclaration.type: KSTypeReference

Сигнатура: val type: KSTypeReference

Что делает: Возвращает ссылку на тип свойства. Через resolve() можно получить KSType, а затем преобразовать в ClassName из KotlinPoet.

Пример:

val typeRef = prop.type
val resolvedType = typeRef.resolve()
val className = resolvedType.toClassName() // для KotlinPoet
println("Type: $className")

4.5 KSFunctionDeclaration.parameters: List<KSValueParameter>

Сигнатура: val parameters: List<KSValueParameter>

Что делает: Возвращает список параметров функции. Полезно для генерации фабрик, делегатов или обёрток.

Пример:

val functionDecl = resolver.getFunctionDeclarations().first()
functionDecl.parameters.forEach { param ->
    println("Param: ${param.name?.asString()} : ${param.type.resolve().toClassName()}")
}

4.6 CodeGenerator.createNewFile(dependencies: Dependencies, packageName: String, fileName: String, extensionName: String): OutputStream

Сигнатура: fun createNewFile(dependencies: Dependencies, packageName: String, fileName: String, extensionName: String): OutputStream

Что делает: Создаёт новый файл для сгенерированного кода. Обычно используется вместе с KotlinPoet для записи .kt-файлов.

Пример:

val file = codeGenerator.createNewFile(
    dependencies = Dependencies(false),
    packageName = "com.example.generated",
    fileName = "MyGeneratedClass",
    extensionName = "kt"
)
file.bufferedWriter().use { writer ->
    writer.write("package com.example.generated\nclass MyGeneratedClass")
}

4.7 KSAnnotation.annotationType: KSTypeReference

Сигнатура: val annotationType: KSTypeReference

Что делает: Возвращает тип аннотации. Позволяет получить аргументы аннотации через arguments.

Пример:

val annotation = classDecl.annotations.first { it.shortName.asString() == "Builder" }
val annotationType = annotation.annotationType.resolve().toClassName()
annotation.arguments.forEach { arg ->
    println("Argument: ${arg.name?.asString()} = ${arg.value}")                    

Рекомендаций