Site icon AppTractor

Что такое Kotlin Symbol Processing (KSP)

Kotlin Symbol Processing (KSP) — это упрощенный API для создания плагинов к компиляторам, который позволяет использовать возможности Kotlin, сохраняя при этом минимальную кривую обучения. По сравнению с KAPT процессоры аннотаций, использующие KSP, могут работать в 2 раза быстрее.

Плагины компилятора и почему KSP

Плагины компилятора — это мощные средства метапрограммирования, которые могут значительно улучшить процесс написания кода. Плагины компиляторов вызывают компиляторы непосредственно как библиотеки для анализа и редактирования исходного кода программ. Эти модули также могут генерировать выходные данные для различных целей. Например, они могут генерировать шаблонный код, и даже генерировать полные реализации для специально помеченных элементов программы, таких как Parcelable. Плагины имеют множество других применений и даже могут использоваться для реализации и тонкой настройки функций, не предусмотренных непосредственно в языке.

Хотя плагины компилятора очень мощные, за эту мощь приходится платить. Чтобы написать даже простейший подключаемый модуль, необходимо обладать определенными знаниями о компиляторе, а также знать детали реализации конкретного компилятора. Другая практическая проблема заключается в том, что плагины  часто тесно связаны с конкретными версиями компиляторов, а это означает, что вам придется обновлять свой модуль каждый раз, когда вы захотите поддержать новую версию компилятора.

Kotlin Symbol Processing спроектирован таким образом, чтобы скрыть изменения в компиляторе, что минимизирует усилия по обслуживанию процессоров, использующих его. KSP не привязан к JVM, что позволяет в будущем легче адаптировать его к другим платформам. KSP также призван минимизировать время сборки. Для некоторых процессоров, например Glide, KSP сокращает время полной компиляции до 25% по сравнению с kapt.

Сама платформа реализована в виде плагина компилятора. В репозитории Maven компании Google имеются готовые пакеты, которые можно загрузить и использовать, не прибегая к самостоятельной сборке проекта.

Обзор

KSP API обрабатывает код на языке Kotlin на идиоматическом уровне. KSP понимает специфические для Kotlin особенности, такие как функции расширения, дисперсия деклараций и локальные функции. Кроме того, он явно моделирует типы и обеспечивает базовую проверку типов, такую как эквивалентность и совместимость по назначению.

API моделирует структуры Kotlin-программ на уровне символов в соответствии с грамматикой Kotlin. Когда плагины на базе KSP обрабатывают исходники, такие конструкции, как классы, члены классов, функции и связанные с ними параметры, становятся доступными для процессоров, а такие вещи, как блоки if и циклы for, — нет.

Концептуально Kotlin Symbol Processing похож на KType в рефлексии Kotlin. API позволяет процессорам переходить от объявлений классов к соответствующим типам с определенными аргументами типа и наоборот. Также можно заменять аргументы типа, указывать вариации, применять проекции звезд и отмечать нулевые возможности типов.

Еще один способ думать о KSP — как о препроцессорном фреймворке для программ на языке Kotlin. Если рассматривать плагины на базе KSP как символьные процессоры, или просто процессоры, то поток данных при компиляции можно описать следующими шагами:

  1. Процессоры читают и анализируют исходники программы и ресурсы.
  2. Процессоры генерируют код или другие формы вывода.
  3. Компилятор Kotlin компилирует исходные программы вместе со сгенерированным кодом.

В отличие от полноценного плагина компилятора, процессоры не могут модифицировать код. Плагин компилятора, изменяющий семантику языка, иногда может быть очень запутанным. KSP позволяет избежать этого, рассматривая исходник программы как доступный только для чтения.

Обзор KSP можно также получить из этого видеоролика:

Как KSP смотрит на исходные файлы

Большинство процессоров ориентируются по различным программным структурам входного исходного кода. Прежде чем перейти к использованию API, давайте посмотрим, как может выглядеть файл с точки зрения KSP:

KSFile
  packageName: KSName
  fileName: String
  annotations: List<KSAnnotation>  (File annotations)
  declarations: List<KSDeclaration>
    KSClassDeclaration // class, interface, object
      simpleName: KSName
      qualifiedName: KSName
      containingFile: String
      typeParameters: KSTypeParameter
      parentDeclaration: KSDeclaration
      classKind: ClassKind
      primaryConstructor: KSFunctionDeclaration
      superTypes: List<KSTypeReference>
      // contains inner classes, member functions, properties, etc.
      declarations: List<KSDeclaration>
    KSFunctionDeclaration // top level function
      simpleName: KSName
      qualifiedName: KSName
      containingFile: String
      typeParameters: KSTypeParameter
      parentDeclaration: KSDeclaration
      functionKind: FunctionKind
      extensionReceiver: KSTypeReference?
      returnType: KSTypeReference
      parameters: List<KSValueParameter>
      // contains local classes, local functions, local variables, etc.
      declarations: List<KSDeclaration>
    KSPropertyDeclaration // global variable
      simpleName: KSName
      qualifiedName: KSName
      containingFile: String
      typeParameters: KSTypeParameter
      parentDeclaration: KSDeclaration
      extensionReceiver: KSTypeReference?
      type: KSTypeReference
      getter: KSPropertyGetter
        returnType: KSTypeReference
      setter: KSPropertySetter
        parameter: KSValueParameter

В этом представлении перечислены общие вещи, которые объявлены в файле: классы, функции, свойства и т.д.

SymbolProcessorProvider: точка входа

KSP ожидает реализацию интерфейса SymbolProcessorProvider для инициирования SymbolProcessor:

interface SymbolProcessorProvider {
    fun create(environment: SymbolProcessorEnvironment): SymbolProcessor
}

SymbolProcessor определяется как:

interface SymbolProcessor {
    fun process(resolver: Resolver): List<KSAnnotated> // Let's focus on this
    fun finish() {}
    fun onError() {}
}

Resolver предоставляет SymbolProcessor доступ к деталям компилятора, таким как символы. Процессор, который находит все функции верхнего уровня и нелокальные функции в классах верхнего уровня, может выглядеть следующим образом:

class HelloFunctionFinderProcessor : SymbolProcessor() {
    // ...
    val functions = mutableListOf<KSClassDeclaration>()
    val visitor = FindFunctionsVisitor()

    override fun process(resolver: Resolver) {
        resolver.getAllFiles().forEach { it.accept(visitor, Unit) }
    }

    inner class FindFunctionsVisitor : KSVisitorVoid() {
        override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) {
            classDeclaration.getDeclaredFunctions().forEach { it.accept(this, Unit) }
        }

        override fun visitFunctionDeclaration(function: KSFunctionDeclaration, data: Unit) {
            functions.add(function)
        }

        override fun visitFile(file: KSFile, data: Unit) {
            file.declarations.forEach { it.accept(this, Unit) }
        }
    }
    // ...

    class Provider : SymbolProcessorProvider {
        override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor = TODO()
    }
}

Примеры использования KSP

Получить все функции члена:

fun KSClassDeclaration.getDeclaredFunctions(): List<KSFunctionDeclaration> =
    declarations.filterIsInstance<KSFunctionDeclaration>()

Проверка того, является ли класс или функция локальной:

fun KSDeclaration.isLocal(): Boolean =
    parentDeclaration != null && parentDeclaration !is KSClassDeclaration

Найти реальное объявление класса или интерфейса, на которое указывает псевдоним типа:

fun KSTypeAlias.findActualType(): KSClassDeclaration {
    val resolvedType = this.type.resolve().declaration
    return if (resolvedType is KSTypeAlias) {
        resolvedType.findActualType()
    } else {
        resolvedType as KSClassDeclaration
    }
}

Сбор подавленных имен в аннотации к файлу:

// @file:kotlin.Suppress("Example1", "Example2")
fun KSFile.suppressedNames(): List<String> {
    val ignoredNames = mutableListOf<String>()
    annotations.filter {
        it.shortName.asString() == "Suppress" && it.annotationType.resolve()?.declaration?.qualifiedName?.asString() == "kotlin.Suppress"
    }.forEach {
        val argValues: List<String> = it.arguments.flatMap { it.value }
        ignoredNames.addAll(argValues)
    }
    return ignoredNames
}

Дополнительно

Exit mobile version