Невошедшее. Mobuis. Frontend Plugin

Невошедшее. Mobuis. Frontend Plugin

Askhar Aydarov

Эта статья дополняет доклад на Mobius. Для лучшего понимания лучше начать с доклада...

В докладе речь шла о реализации компиляторного плагина для проверки стабильности параметров функции для обнаружения таких @Composable функций, которые стали перезаупскаемыми, но не стали пропускаемыми. И там же я показал реализацию backend плагина, а в попытках сделать ошибку ближе к разработчику решил написать frontend плагин, так как были следующие причины:

  1. Инвестиция в будущее. Думалось, что мы сейчас потерпим с реализацией под backend, а потом уйдем от этой реализации на frontend, так как есть вторая причина - поддержка в IDEA.
  2. Поддержка в IDEA. На KotlinConf 2023 показывали, как можно просто прописать компиляторный плагин в build.gradle.kts и IDEA сама подхватит все изменения, предупреждения и ошибки, которые генерируют frontend плагины. И это все без сборки проекта.
  3. Относительно сильно распиарен.
  4. 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.

Опубликовано в Полуночные Зарисовки

Report Page