Невошедшее. Mobuis. Frontend Plugin
Askhar AydarovЭта статья дополняет доклад на Mobius. Для лучшего понимания лучше начать с доклада...
В докладе речь шла о реализации компиляторного плагина для проверки стабильности параметров функции для обнаружения таких @Composable функций, которые стали перезаупскаемыми, но не стали пропускаемыми. И там же я показал реализацию backend плагина, а в попытках сделать ошибку ближе к разработчику решил написать frontend плагин, так как были следующие причины:
- Инвестиция в будущее. Думалось, что мы сейчас потерпим с реализацией под backend, а потом уйдем от этой реализации на frontend, так как есть вторая причина - поддержка в IDEA.
- Поддержка в IDEA. На KotlinConf 2023 показывали, как можно просто прописать компиляторный плагин в build.gradle.kts и IDEA сама подхватит все изменения, предупреждения и ошибки, которые генерируют frontend плагины. И это все без сборки проекта.
- Относительно сильно распиарен.
- Google уже поддержала его в компиляторе Compose и Android Lint. А значит есть примеры реализаций.
В отличие от backend части frontend не работает работает с IR, хоть и формирует его на выходе. Вместо этого на frontend используются другие синтаксически деревья в зависимости от версии компилятора. Для K1 - это PSI с BindingContext, где BindingContext представляет собой большую HashMap, которая содержит семантическую информацию для элементов дерева. А для K2 - это FIR, где семантика находится уже в самом дереве. И как я понимаю, разработчики компилятора избавились от HashMap в качестве варианта оптимизации. Далее мы будем рассматривать только K2 frontend.
И так как есть разница в синтаксических деревьях, есть разница и в Extension, которые используются для написания плагинов. Для backend - это был IrGenerationExtension, а для frontend - FirExtensionRegistratAdapter, который является оберткой над еще большим количеством Extension, но уже для frontend части.
Какие еще есть особенности frontend компилятора?
- Первое и самое важное - это то, что он работает в несколько этапов. И на каждой фазе резолвится только часть синтаксического дерева. А у нас плагин будет работать на этапе Checkers, на котором все фазы выполнены и FIR дерево полностью зарезолвлено, что скорееупрощает нам работу, так как на других фазах декларации могут быть не сформированы и обращение кним может вернуть либо null, либо закрашить сборку.

- Fronted так же делает desugaring кода. Причем он его делает и во время резолва fir дерева и даже перед тем, как сконвертировать FIR в IR. Например, context receivers таким образом он подставляет как параметры функции.

- Fronted позволяет расширить семантику и в Compose этим уже пользуются, определяя свои функциональные типы.

- Позволяет анализировать код. В Compose, например, реализован показ ошибки при попытке получить ссылку на @Composable функцию. И заметьте тут же используются созданные во frontend функциональные типы.

- Можно генерировать и изменять декларации. Без проблем поменять модификатор доступа переменной по какому-то правилу. Например, если у функции название начинается с Preview, и она содержит @Preview аннотацию, то можно сделать такую функцию приватной даже если этот модификатор не указан. Кроме того, сгенерированный модификатор потом уйдет на backend, в котором задача по изменениях модификатора доступа не такая простая, как может показаться.

- Ну и в конце концов происходит конвертация из FIR в IR.

В моем случае основной задачей было написать FirSimpleFunctionChecker и переписать логику определения стабильности с IR на FIR.
Но в итоге все оказалось не так просто по следующим причинам:
- нестабильное API. я писал плагин на одной версии kotlin, а у нас в проекте чуть ниже, поэтому всегда чего-то не хватало. Но это можно простить, потому что все таки экспериментальный компилятор
- Fir2IrConverter, на который у меня была вся надежда оказался довольно сложным. Пришлось довольно глубоко копаться во фронтенд компиляторе и искать как делаются некоторые вещи.
- А сложный Fir2IrConverter, потому что FIR и IR очень разные и просто так найти соответствия не получается.
- И из-за того, что FIR резолвится по фазам, то есть такое ограничение, что не рекомендуется напрямую обращаться к декларациям. Чаще всего нужно использовать символы, которые через разные extension функции по необходимости обращаются к декларациям. Но эти extension как-то хаотично приватные и публичные, поэтому некоторые вещи просто копипастились из исходников компилятора.
В итоге я написал SkippabilitySimpleFunctionChecker
И посмотрите на матрешку, которую нужно написать, чтобы его подключить:


По своему странное, но практичное, как мне кажется api.
Подключил к проекту и получил...ничего. Ну почти. Ошибка есть теперь в build панели IDEA, но никакой подсветки в коде:

Но как оказалось K2 в IDEA и Android Studio поддерживается экспериментально, и чтобы включить его нужно собрать IDEA из исходников специальной конфигурацией и выключенным kotlin.k2.only.bundled.compiler.plugins.enabled, чтобы IDEA или студия подхватили плагины.

Какой вывод? Я считаю, что frontend плагины - штука мощная и оптимизированная по сравнению с PSI с Binding Context, но пока еще нестабильная. И жаль, что с таким пиаром еще нет поддержки в редакторе кода из коробки и логику приходится дублировать написанием idea plugin.
Опубликовано в Полуночные Зарисовки