В разработке с использованием Kotlin (или Java) для создания классов по верхнеуровневому описанию часто используется маркировка аннотациями (например, для моделей таблиц баз данных, сетевых запросов или инъекции зависимостей) и подключение процессоров аннотаций, которые также могут генерировать код, доступный из основного проекта. Запуск процессоров аннотаций выполняется внутри gradle (для Java-проектов через annotationProcessor, для Kotlin - kapt) и встраивается как зависимость для целей сборки проекта. И конечно же, как и для любого другого кода, для процессора аннотаций необходимо иметь возможность разрабатывать тесты. В этой статье мы рассмотрим основы использования кодогенерации (с использованием kapt) и разработки тестов для созданных генераторов кода. Во второй части статьи речь пойдет о разработке процессоров на основе Kotlin Symbol Processing (KSP) и созданию тестов для них. Начнем с классического механизма кодогенерации kapt (Kotlin Annotation Processing Tool). kapt встраивается в gradle (как плагин) или в maven (через добавление <goals><goal>kapt</goal></goals> в описание execution в конфигурации проекта). В общем виде конфигурация проекта с kapt может быть такой: После подключения кодогенерации на kapt становится возможным подключать процессоры через команду kapt, например подключен кодогенерацию Autovalue для создания иммутабельных классов (фактически в Kotlin они могут быть реализованы через data‑классы и AutoValue решает ту же задачу для Java и во многом похож на Lombok, но работает иначе). Kapt-процессор работает аналогично процессору аннотаций для Java, но при этом сначала исходный текст Kotlin преобразуется в Java-код и потом передается генератору. Это в целом снижает скорость кодогенерации (даже по сравнению с проектом на Java) и для решения этой проблемы и создавался альтернативный механизм KSP, о котором мы поговорим далее. В принципе кодогенерация может создавать код не только на Java, но и на Kotlin или любом другом языке, но многие используемые с kapt генераторы разработаны изначально для Java (например, Room, Hilt и т.п.). Добавим простой класс для описания пользователей с автоматическим определением идентификатора: Для выполнения кодогенерации запустим задачу gradle (: Сгенерированный код в большинстве размещается в build-каталоге (build/generated/source/kapt/main) и представляет из себя исходный текст на Java (кроме создания get-методов, также переопределяет equals, hashCode и toString). Отдельно импортировать его нет необходимости, поскольку размещается в том же пакете, где находится исходный аннотированный класс. Созданный класс будет помечен аннотацией @Generated с указанием класса процессора, который создал этот класс: Теперь сделаем пример кода для использования сгенерированного класса: Результатом будет строковое представление списка: Теперь разберемся с созданием собственного кодогенератора. За основу будем использовать шаблон из трех модулей (модуль с приложением, который будет использовать процессор аннотаций, модуль с аннотацией и модуль процессора). Определим аннотацию для использования в кодогенераторе: И реализуем сам процессор, который будет определяться в методе process в классе-расширения от javax.annotation.processing.AbstractProcessor: Здесь используется библиотека auto-service для автоматической регистрации класса, как процессора кодогенерации (подключается в build.gradle.kts): Метод process будет вызываться при обнаружении аннотаций, перечисленных в @SupportedAnnotationTypes и может иметь доступ к определениям исходного кода через реализацию RoundEnvironment (получает в roundEnv). Также внутри AbstractProcessor есть доступ к processingEnv, через который можно получать аргументы для kapt (через options), создавать файлы (через поле filer) и выводить сообщения в IDE и консоль gradle (для сообщения указывается тип из перечисления Diagnostics.Kind: ERROR - при ошибке, WARNING отображается как информационное сообщение, OTHER - для любого другого типа сообщения, не прерывающего выполнение кодогенерации). Через roundEnv можно получить информацию об аннотированных определениях (может быть перед пакетом, интерфейсом/классом, функцией/методом, или определением переменной), каждое определение представлено реализацией интерфейса Element и позволяет получить метаинформацию об определении: simpleName - название (без пакета) kind - тип элемента (определены в ElementKind) getAnnotation(type) - получение объекта аннотации (вместе с аргументами, если определены) modifiers - модификаторы определения (например, private или static) enclosingElement - дает доступ к элементу верхнего уровня (например, определению класса для аннотированного метода) enclosedElements - возвращает список вложенных элементов (например, определений свойств и методов для аннотированного класса) Определим простой класс (аннотация @JvmField здесь используется для исключения автоматической генерации get-методов). и создадим процессор, который будет обнаруживать и отображать все найденные свойства класса: Теперь добавим генерацию кода, для этого получим объект Filer и через него мы можем создать байт-код (createClassFile), ресурс (createResource) или сгенерировать новый файл с исходными текстами (createSourceFile). Далее к созданному файлу можно получить доступ через writer и записать туда сгенерированный исходный текст (после завершения работы, созданный файл будет проверен на корректность синтаксиса). Например, мы хотим добавить поле id с автоматическим увеличением, для этого сначала подготовим шаблон исходного кода (на Java, но можно и на Kotlin):  используя шаблон и информацию из обнаруженных объектов (название пакета извлекается из enclosingElement для аннотированного класса, название и сигнатуры определения полей из enclosedElements от класса) Здесь дополнительно заменяются модификаторы доступа на public (чтобы тест в дальнейшем мог прочитать поля, альтернативно можно добавить генерацию get-методов). Также важно, чтобы сам класс и конструктор были public, иначе возникнет ошибка на этапе создания объекта через рефлексию. Аналогично можно сгенерировать любые структуры данных и фрагменты кода.  Для генерации кода также можно использовать библиотеки, например JavaPoet дает возможность представлять код в виде дерева объектов и генерировать форматированный  код на языке Java.  Теперь перейдем к тестированию разработанного кодогенератора. Для этого подключим библиотеку kotlin-compile-testing и добавим наш проект для  Библиотека позволяет выполнить программную компиляцию заданного фрагмента кода на Java или Kotlin (при этом можно добавлять процессоры аннотаций, в том числе KSP). Важно добавить в компиляцию файл с определением аннотации, поскольку при сборке библиотека не знает о существовании gradle-проектов и работает непосредственно с фрагментом кода.  Начнем с простой проверки небольшого класса без использования кодогенерации: Полученный объект result содержит информацию о сгенерированных файлах в поле generatedFiles (в данной случае только исходный текст, байт-код и META-INF), также можно узнать результат компиляции (exitCode), получить список файлов, созданных процессорами аннотаций (sourcesGeneratedByAnnotationProcessor), а также получить доступ к загрузчику классов для рефлексии по созданному классу и создания его экземпляров через конструкторы и newInstance. Добавим тесты сигнатуры метода sum и проверим функциональность созданного класса: Тут важно помнить, что создание объектов, выполнение методов и доступ к свойств учитывает модификаторы доступности и, поскольку код теста сейчас находится в другом пакете, то нужно следить чтобы соответствующие модификаторы были public.  Теперь перейдем к тестированию нашего кодогенератора. Для добавления процессоров аннотаций в объекте класса KotlinCompilation (или JavaCompilation) есть список в свойстве annotationProcessors: Здесь исходный текст с определением аннотации (в kapt-example-core) компилируется вместе с нашим фрагментом, чтобы корректно сработал import и применение аннотации. Дальнейший тест выполняется аналогично предыдущему примеру: Исходные тексты проекта можно найти в репозитории https://github.com/dzolotov/kapt-template (ветка codegen-test). Мы рассмотрели основные вопросы по разработке процессоров аннотаций с возможностью кодогенерации для Java или Kotlin-проектов и способы тестирования корректности их работы. Во второй части статьи мы изучим новый подход к генерации кода на Kotlin с использованием Kotlin Symbol Processing (KSP) и, конечно, научимся разрабатывать тесты для KSP-процессоров.  В завершение приглашаю всех на бесплатный вебинар в рамках которого научимся проверять готовность мобильного приложения для использования людьми с ограничениями здоровья. Также готовность автоматически проверять соответствие требованиям визуальной контрастности, адаптации верстки под увеличенный шрифт, наличие семантической разметки для вспомогательных инструментов для приложений Android (XML и Compose) и iOS (Flutter и KMM). Научимся использовать инструменты автоматических проверок и создавать собственные валидаторы для реализации сложных визуальных проверок. Зарегистрироваться на бесплатный вебинар  Flutter Developer@YandexPro, DevOps, IT Architect