Не задумывались, почему при написании Composable без зависимости Jetpack Compose в IDE появляются сообщения об ошибках?

Без настроенной Jetpack Compose зависимости в IDE не распознается ссылка @Composable

И почему при импортировании необходимых зависимостей Compose аннотированная функция @Composable превращается в тип Composable?

Функция HelloWorld распознается как тип composable

Это происходит из-за плагинов компилятора Jetpack Compose, которыми в IDE отправляются диагностические данные для получения обратной связи от пользователей. Посмотрим, как эта диагностика извлекается компилятором Kotlin, при том что поведение во время выполнения и компиляция кода перехватываются плагинами компилятора.

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

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

Примечание: Роли, выполняемые на разных этапах для плагина компилятора Compose меняются. Функциональность компилятора со временем тоже.

Компилятор Kotlin

Обсуждения плагинов компилятора осуществляются в контексте самого компилятора. Но делать это в одиночку опасно: чтение статьи желательно продолжить, открыв эти ресурсы.

Начнем с общей архитектуры компилятора Kotlin, а подробнее  —  с фронтенда компилятора. Здесь имеется две текущие версии: новый фронтенд и старый. Возьмем новый, называемый FIR-фронтендом, то есть промежуточным представлением фронтенда, или компилятором k2. Вот он схематично:

Компилятор Kotlin  —  FIR-фронтенд. Подробнее

Если упрощенно: многократным сканированием кода компилятором создаются новые форматы данных и продолжается самодополнение с получением более полной картины.

Если еще проще, компилятором многократно выполняются:

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

Имея общее представление и карту компилятора Kotlin, посмотрим теперь, как плагины компилятора встраиваются в компилятор. А затем заглянем внутрь плагина компилятора Compose.

Архитектура плагина компилятора

Простейшее объяснение архитектуры плагинов компилятора изложено в докладе Кевина Моста:

KotlinConf 2018  —  написание первого плагина компилятора Кевином Мостом. Вот весь доклад

Вкратце резюмируем этот доклад и создадим «мысленную карту» того, как ориентироваться в плагине компилятора:

Плагин
- Gradle API, совершенно не связанный с Kotlin
- Из скрипта «build.gradle» предоставляется точка входа
- Конфигурация доступна через расширения Gradle

Подплагин
- Интерфейс между Gradle и Kotlin API
- Считываются параметры расширения Gradle
- Выписываются Kotlin SubpluginOptions
- Определяется идентификатор плагина компилятора — внутренний уникальный ключ
- Определяются загружаемые компилятором координаты плагина Maven в Kotlin

CommandLineProcessor
- Считываются аргументы kotlinc -Xplugin
- Параметры подплагина передаются по этому конвейеру
- Записываются CompilerConfigurationKeys

ComponentRegistrar
- Считываются CompilerConfigurationKeys

Расширения
- Выполняется подключение к этапам компилятора для перехвата поведения по умолчанию, а именно:
- ExpressionCodegenExpression
- ClassBuilderInterceptorExtension
- StorageComponentContainerContribtor
- IrGenerationExtension

Имея эти «карты» архитектуры плагинов компилятора и компилятора Kotlin, перейдем к коду плагина компилятора Compose.

Плагин компилятора Compose: что внутри?

Плагин компилятора Compose перемещен из Androidx в репозиторий Kotlin. Открыв файловую систему и перейдя к исходному коду, обнаруживаем такие файлы и папки:

Так быстрее определяется, где разделить эти части на обязанности фронтенда и бэкенда:

  • /analysis  —  такого рода анализ закрепляется за бэкендом, поскольку выполняется на данных промежуточного представления. Классами этой папки, похоже, определяется стабильность работы Compose. Вот документация по этому функционалу.
  • /inference  —  используется для фронтенда и бэкенда, связанных с выводом Applier. Для Applier в Compose применяются операции на основе дерева, которые появляются во время композиции. ApplierInference используется с абстрактным синтаксическим деревом фронтенда или промежуточным представлением бэкенда. В этом пакете также содержатся схемы и привязки / привязки вызовов, которыми создаются правила привязывания токенов друг к другу.
  • /k1  —  классы для перехвата исходного компилятора фронтенда K1.
  • /k2  —  классы для перехвата нового компилятора FIR-фронтенда K2.
  • /lower  —  часть бэкенда для уменьшения данных промежуточного представления, ее не рассматриваем.

Также имеется список классов, расположенных на верхнем уровне:

  • BuildMetrics.kt  —  метрики и логирование для отчетов плагина Compose.
  • ComposeFqNames.kt  —  полностью уточненные имена для классов Composable вместе с функциями-расширениями промежуточного представления.
  • ComposeIrGenerationExtension.kt  —  перехватывается генерирование промежуточного представления на бэкенде для поведений Compose.
  • ComposeNames.kt  —  словарь имен для проверки значений промежуточного представления на бэкенде.
  • ComposePlugin.kt  —  точка входа для плагина компилятора Compose. Сопоставляем это с картой архитектуры компилятора Compose.
  • WeakBindingTrace.kt  —  в документации к классу написано: «Этот класс должен иметь форму объекта BindingTrace, который мог бы существовать и проходить этап Psi2Ir -> Ir, но в настоящее время не существует». Преобразованием PSI → IR подразумевается, что этот класс относится к функциям компилятора K1.
Подробнее о компиляторах K1 и K2  —  здесь

Что перехватывается плагином компилятора Compose?

Как и другими плагинами компилятора, плагином Jetpack Compose перехватывается поведение при компиляции на фронтенде и бэкенде компилятора(-ов) Kotlin.

Если прокрутить вниз основной класс входа ComposePlugin.kt до зарегистрированных функций-расширений, где находится ComposePluginRegistrar, увидим это:

override fun ExtensionStorage.registerExtensions(configuration: CompilerConfiguration) {
if (checkCompilerConfiguration(configuration)) {
val usesK2 = configuration.languageVersionSettings.languageVersion.usesK2
val descriptorSerializerContext =
if (usesK2) null
else ComposeDescriptorSerializerContext()

registerCommonExtensions(descriptorSerializerContext) <---

IrGenerationExtension.registerExtension(
createComposeIrExtension(
configuration,
descriptorSerializerContext
)
)

if (!usesK2) {
registerNativeExtensions(descriptorSerializerContext!!)
}
}
}

Но под промежуточным представлением подразумевается бэкенд, а нужен фронтенд. Поэтому прокручиваем дальше до registerCommonExtensions(...) и до этого кода:

fun ExtensionStorage.registerCommonExtensions(
composeDescriptorSerializerContext: ComposeDescriptorSerializerContext? = null,
) {
StorageComponentContainerContributor.registerExtension(
ComposableCallChecker()
)
StorageComponentContainerContributor.registerExtension(
ComposableDeclarationChecker()
)
StorageComponentContainerContributor.registerExtension(
ComposableTargetChecker()
)
DiagnosticSuppressor.registerExtension(ComposeDiagnosticSuppressor())
@Suppress("OPT_IN_USAGE_ERROR")
TypeResolutionInterceptor.registerExtension(
ComposeTypeResolutionInterceptorExtension()
)
DescriptorSerializerPlugin.registerExtension(
ClassStabilityFieldSerializationPlugin(
composeDescriptorSerializerContext?.classStabilityInferredCollection
)
)
FirExtensionRegistrarAdapter.registerExtension(ComposeFirExtensionRegistrar())
}

Вот классы, зарегистрированные как обычные расширения во фронтенде, по тому, что ими расширяется, разберемся, где в процессе компиляции находятся эти этапы:

  • ComposableCallChecker.kt  —  расширяется FirExpressionChecker в пакете org.jetbrains.kotlin.fir.analysis.checkers.expression.
  • ComposableDeclarationChecker.kt  —  расширяется DeclarationChecker в пакете org.jetbrains.kotlin.resolve.checkers.
  • ComposableTargetChecker.kt  —  расширяется CallChecker в пакете org.jetbrains.kotlin.resolve.calls.checkers.
  • ComposableDiagnosticSupressor.kt.
  • ComposeFirExtensions.kt  —  предназначен для форматов сериализации K2, необходимых для преобразования типа KComposable в тип формата данных FIR.

Эти конкретные классы легко обнаруживаются в обоих пакетах: k1 и k2. И зарегистрированные расширения в k1 для k2 еще используются:

Скриншот k1 и k2 из плагина компилятора Сompose

Рассмотрев содержимое самого плагина компилятора Compose, выясним, что и где перехватывается при прохождении частей этапов этого компилятора.

Плагины компилятора подключаются к любой части компилятора Kotlin. Запустив код через компилятор, которым настраивается плагин Gradle, мы увидим:

  • Как код перемещается по компилятору при компилировании и уменьшении данных.
  • Как, чтобы изменить поведение при компиляции, плагином компилятора Compose перехватываются и расширяются конкретные этапы компилятора Kotlin.

Вернемся к исходному вопросу. Напишем такой код в IDE и сконфигурируем зависимость Compose в проект:

@Composable fun HelloWorld() { Text("Hello!") }

Мысленно проследим этот код по карте компилятора Kotlin, тестируя FIR-компилятор. Оставим точки отладки в классах перехватываемого плагином компилятора и отследим в стеке вызовов до места, где находятся исходные этапы компилятора.

Этап синтаксического анализа

При синтаксическом анализе проверяется синтаксическая корректность в PSI-деревьях

Сначала исходный код разбивается на лексемы, или токены, а затем преобразуется в PSI-деревья.

Во время лексического анализа плагин Compose добавляется к определению токенов, компилятору указывается «присматривать за дополнительными ключевыми словами» вроде @Composable. Эти токены разбиваются, а затем собираются в PSI-деревья.

Для просмотра, как код преобразуется в PSI-деревья, доступен плагин PSI Viewer в Android Studio и IntelliJ

В PSI указывается, какой код написан конечным пользователем и является ли синтаксис грамматически правильным, но не указывается, компилируется ли код, какие токены являются типами, какие  —  аргументами и т. д.

Кодогенерация PSI → FIR

Компилятором K2 при генерировании кода для создания RawFIR принимаются PSI-деревья. FIR расшифровывается как Frontend Intermediate Representation, то есть промежуточное представление фронтенда. Первоначально же созданная версия  —  это Raw, то есть «сырая» или необработанная версия.

Когда PSI-деревья созданы, данные компилируются компилятором в формат RawFIR

PSI-дерево, созданное из

@Composable fun HelloWorld() { Text("Hello!") }

теперь становится другим форматом данных:

RAW_FIR:
FILE: [ResolvedTo(RAW_FIR)] typeParameterOfClass2.kt
@Composable[ResolvedTo(RAW_FIR)]() public? final? [ResolvedTo(RAW_FIR)] fun HelloWorld(): R|kotlin/Unit| { LAZY_BLOCK }

Напомним: с многократным самосканированием компилятора продолжается его самодополнение. То же делается и с RawFIR при превращении в FIR.

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

Этап разрешения

Самое трудное в разрешении  —  создание взаимосвязей для элементов FIR: статического анализа, верификации типов, диагностики, сообщений-предупреждений IDE и т. д. Плагином Compose здесь выполняются сложнейшие перехваты функций, особенно средства проверки в диагностике.

Первая часть этапа разрешения  —  семантический анализ в RawFIR, при котором генерируются дополнительные реляционные данные. RawFIR дополняется этой информацией, затем проводится анализ для проверок на предмет верификации, проверки типов и т. д.

Форматом RawFIR проходится этап разрешения: сначала в части анализа RawFIR дополняется информацией, затем прохождением в части определения им обеспечивается компилируемость кода

В ComposableTypeResolutionInterceptorExtension перехватывается разрешение типов, чтобы выводить типы Composable, просматривая PSI и их дескрипторы и добавляя к дескрипторам. Если лямбда обозначена как @Composable, то выводимый для литеральной функции тип должен стать типом Kotlin @Composable:

import androidx.compose.runtime.Composable

val lambda: @Composable() -> Unit? = { }

@Composable
fun HelloWorld(content: @Composable () -> Unit) {
content
}

На этом этапе также генерируются дескрипторы с полезной информацией: разрешением типов, модификаторами, потоком управления. Дескрипторы применяются для сложных вычислений, где позже, при проверке результатов компилятора, требуется дополнительная информация об их взаимосвязях: других файлах, импортах, объявлениях, диагностике и т. д. Вот ранее сгенерированный RawFIR с дополненной информацией, созданной во время разрешения:

RAW_FIR:
FILE: [ResolvedTo(RAW_FIR)] typeParameterOfClass2.kt
@Composable[ResolvedTo(RAW_FIR)]() public? final? [ResolvedTo(RAW_FIR)] fun HelloWorld(): R|kotlin/Unit| { LAZY_BLOCK }

Теперь он становится таким:

FUN name:HelloWorld visibility:public modality:FINAL <> ($composer:androidx.compose.runtime.Composer?, $changed:kotlin.Int) returnType:kotlin.Unit
annotations:
Composable
ComposableTarget(applier = "androidx.compose.ui.UiComposable")
VALUE_PARAMETER name:$composer index:0 type:androidx.compose.runtime.Composer? [assignable]
VALUE_PARAMETER name:$changed index:1 type:kotlin.Int
BLOCK_BODY
BLOCK type=kotlin.Unit origin=null
SET_VAR '$composer: androidx.compose.runtime.Composer? [assignable] declared in home.HelloWorld' type=kotlin.Unit origin=null
CALL 'public abstract fun startRestartGroup (key: kotlin.Int): androidx.compose.runtime.Composer declared in androidx.compose.runtime.Composer' type=androidx.compose.runtime.Composer origin=null
$this: GET_VAR '$composer: androidx.compose.runtime.Composer? [assignable] declared in home.HelloWorld' type=androidx.compose.runtime.Composer? origin=null
key: CONST Int type=kotlin.Int value=-4391552
CALL 'public final fun sourceInformation (composer: androidx.compose.runtime.Composer, sourceInformation: kotlin.String): kotlin.Unit declared in androidx.compose.runtime' type=kotlin.Unit origin=null
composer: GET_VAR '$composer: androidx.compose.runtime.Composer? [assignable] declared in home.HelloWorld' type=androidx.compose.runtime.Composer? origin=null
sourceInformation: CONST String type=kotlin.String value="C(HelloWorld)105@511L19:main.kt#1wrmn"
WHEN type=kotlin.Unit origin=IF
BRANCH
if: WHEN type=kotlin.Boolean origin=OROR
BRANCH
if: CALL 'public final fun not (): kotlin.Boolean [operator] declared in kotlin.Boolean' type=kotlin.Boolean origin=null
$this: CALL 'public final fun EQEQ (arg0: kotlin.Any?, arg1: kotlin.Any?): kotlin.Boolean declared in kotlin.internal.ir' type=kotlin.Boolean origin=null
arg0: GET_VAR '$changed: kotlin.Int declared in home.HelloWorld' type=kotlin.Int origin=null
arg1: CONST Int type=kotlin.Int value=0
then: CONST Boolean type=kotlin.Boolean value=true
BRANCH
if: CONST Boolean type=kotlin.Boolean value=true
then: CALL 'public final fun not (): kotlin.Boolean [operator] declared in kotlin.Boolean' type=kotlin.Boolean origin=null
$this: CALL 'public abstract fun <get-skipping> (): kotlin.Boolean declared in androidx.compose.runtime.Composer' type=kotlin.Boolean origin=null
$this: GET_VAR '$composer: androidx.compose.runtime.Composer? [assignable] declared in home.HelloWorld' type=androidx.compose.runtime.Composer? origin=null
then: BLOCK type=kotlin.Unit origin=null
WHEN type=kotlin.Unit origin=IF
BRANCH
if: CALL 'public final fun isTraceInProgress (): kotlin.Boolean declared in androidx.compose.runtime' type=kotlin.Boolean origin=null
then: CALL 'public final fun traceEventStart (key: kotlin.Int, dirty1: kotlin.Int, dirty2: kotlin.Int, info: kotlin.String): kotlin.Unit declared in androidx.compose.runtime' type=kotlin.Unit origin=null
key: CONST Int type=kotlin.Int value=-4391552
dirty1: GET_VAR '$changed: kotlin.Int declared in home.HelloWorld' type=kotlin.Int origin=null
dirty2: CONST Int type=kotlin.Int value=-1
info: CONST String type=kotlin.String value="home.HelloWorld (main.kt:104)"
CALL 'public final fun BasicText (text: kotlin.String, modifier: androidx.compose.ui.Modifier?, style: androidx.compose.ui.text.TextStyle?, onTextLayout: kotlin.Function1<androidx.compose.ui.text.TextLayoutResult, kotlin.Unit>?, overflow: androidx.compose.ui.text.style.TextOverflow, softWrap: kotlin.Boolean, maxLines: kotlin.Int, minLines: kotlin.Int, color: androidx.compose.ui.graphics.ColorProducer?, $composer: androidx.compose.runtime.Composer?, $changed: kotlin.Int, $default: kotlin.Int): kotlin.Unit declared in androidx.compose.foundation.text' type=kotlin.Unit origin=null
text: CONST String type=kotlin.String value="Hello!"
modifier: COMPOSITE type=kotlin.Nothing? origin=DEFAULT_VALUE
CONST Null type=kotlin.Nothing? value=null
style: COMPOSITE type=kotlin.Nothing? origin=DEFAULT_VALUE
CONST Null type=kotlin.Nothing? value=null
onTextLayout: COMPOSITE type=kotlin.Nothing? origin=DEFAULT_VALUE
CONST Null type=kotlin.Nothing? value=null
overflow: COMPOSITE type=androidx.compose.ui.text.style.TextOverflow origin=DEFAULT_VALUE
CALL 'public final fun <unsafe-coerce> <T, R> (v: T of kotlin.jvm.internal.<unsafe-coerce>): R of kotlin.jvm.internal.<unsafe-coerce> declared in kotlin.jvm.internal' type=androidx.compose.ui.text.style.TextOverflow origin=null
<T>: kotlin.Int
<R>: androidx.compose.ui.text.style.TextOverflow
v: CONST Int type=kotlin.Int value=0
softWrap: COMPOSITE type=kotlin.Boolean origin=DEFAULT_VALUE
CONST Boolean type=kotlin.Boolean value=false
maxLines: COMPOSITE type=kotlin.Int origin=DEFAULT_VALUE
CONST Int type=kotlin.Int value=0
minLines: COMPOSITE type=kotlin.Int origin=DEFAULT_VALUE
CONST Int type=kotlin.Int value=0
color: COMPOSITE type=kotlin.Nothing? origin=DEFAULT_VALUE
CONST Null type=kotlin.Nothing? value=null
$composer: GET_VAR '$composer: androidx.compose.runtime.Composer? [assignable] declared in home.HelloWorld' type=androidx.compose.runtime.Composer? origin=null
$changed: CONST Int type=kotlin.Int value=6
$default: CONST Int type=kotlin.Int value=510
WHEN type=kotlin.Unit origin=IF
BRANCH
if: CALL 'public final fun isTraceInProgress (): kotlin.Boolean declared in androidx.compose.runtime' type=kotlin.Boolean origin=null
then: CALL 'public final fun traceEventEnd (): kotlin.Unit declared in androidx.compose.runtime' type=kotlin.Unit origin=null
BRANCH
if: CONST Boolean type=kotlin.Boolean value=true
then: CALL 'public abstract fun skipToGroupEnd (): kotlin.Unit declared in androidx.compose.runtime.Composer' type=kotlin.Unit origin=null
$this: GET_VAR '$composer: androidx.compose.runtime.Composer? [assignable] declared in home.HelloWorld' type=androidx.compose.runtime.Composer? origin=null
BLOCK type=kotlin.Unit origin=null
BLOCK type=kotlin.Unit origin=SAFE_CALL
VAR IR_TEMPORARY_VARIABLE name:tmp_0 type:androidx.compose.runtime.ScopeUpdateScope? [val]
CALL 'public abstract fun endRestartGroup (): androidx.compose.runtime.ScopeUpdateScope? declared in androidx.compose.runtime.Composer' type=androidx.compose.runtime.ScopeUpdateScope? origin=null
$this: GET_VAR '$composer: androidx.compose.runtime.Composer? [assignable] declared in home.HelloWorld' type=androidx.compose.runtime.Composer? origin=null
WHEN type=kotlin.Unit origin=IF
BRANCH
if: CALL 'public final fun EQEQ (arg0: kotlin.Any?, arg1: kotlin.Any?): kotlin.Boolean declared in kotlin.internal.ir' type=kotlin.Boolean origin=null
arg0: GET_VAR 'val tmp_0: androidx.compose.runtime.ScopeUpdateScope? [val] declared in home.HelloWorld' type=androidx.compose.runtime.ScopeUpdateScope? origin=null
arg1: CONST Null type=kotlin.Any? value=null
then: CONST Null type=kotlin.Any? value=null
BRANCH
if: CONST Boolean type=kotlin.Boolean value=true
then: CALL 'public abstract fun updateScope (block: kotlin.Function2<androidx.compose.runtime.Composer, kotlin.Int, kotlin.Unit>): kotlin.Unit declared in androidx.compose.runtime.ScopeUpdateScope' type=kotlin.Unit origin=null
$this: GET_VAR 'val tmp_0: androidx.compose.runtime.ScopeUpdateScope? [val] declared in home.HelloWorld' type=androidx.compose.runtime.ScopeUpdateScope? origin=null
block: FUN_EXPR type=kotlin.Function2<androidx.compose.runtime.Composer?, kotlin.Int, kotlin.Unit> origin=LAMBDA
FUN LOCAL_FUNCTION_FOR_LAMBDA name:<anonymous> visibility:local modality:FINAL <> ($composer:androidx.compose.runtime.Composer?, $force:kotlin.Int) returnType:kotlin.Unit
VALUE_PARAMETER name:$composer index:0 type:androidx.compose.runtime.Composer?
VALUE_PARAMETER name:$force index:1 type:kotlin.Int
BLOCK_BODY
RETURN type=kotlin.Nothing from='local final fun <anonymous> ($composer: androidx.compose.runtime.Composer?, $force: kotlin.Int): kotlin.Unit declared in home.HelloWorld'
CALL 'public final fun HelloWorld ($composer: androidx.compose.runtime.Composer?, $changed: kotlin.Int): kotlin.Unit declared in home' type=kotlin.Unit origin=null
$composer: GET_VAR '$composer: androidx.compose.runtime.Composer? declared in home.HelloWorld.<anonymous>' type=androidx.compose.runtime.Composer? origin=null
$changed: CALL 'internal final fun updateChangedFlags (flags: kotlin.Int): kotlin.Int declared in androidx.compose.runtime' type=kotlin.Int origin=null
flags: CALL 'public final fun or (other: kotlin.Int): kotlin.Int [infix] declared in kotlin.Int' type=kotlin.Int origin=null
$this: GET_VAR '$changed: kotlin.Int declared in home.HelloWorld' type=kotlin.Int origin=null
other: CONST Int type=kotlin.Int value=1

Чтобы вывести диагностические результаты компилятора, информации теперь достаточно. Здесь приходятся кстати средства проверки. В плагине компилятора Compose к «корректным определениям синтаксиса», как определено элементами FIR, добавляются ComposableCallChecker и ComposableDeclarationChecker.

В ComposableCallChecker задаются правила относительно того, каким функциям допустимо вызывать другие функции Composable. Для проверки, вписывается ли конкретный вызов в определенные в Compose ограничения контрактов, здесь анализируется дополненный FIR для типов лямбда-выражений. Этим расширением для проверки вызовов подтверждается, что типы Composable вызываются только из других типов Composable. На этапе разрешения определяются поток управления, уточнение типа, выведение типов и т. д.

Например, разрешением указывается, что́ из этого компилируется в удобном для восприятия формате:

import androidx.compose.material.Text
import androidx.compose.runtime.Composable// Компилируется
@Composable fun HelloWorld() { Text("Hello!") }// Компилируется
val lambdaInvo1 = @Composable { HelloWorld() }// Компилируется
val lambdaInvo2: @Composable (()->Unit) = { HelloWorld() }

Теперь из-за плагина компилятора компилятор Kotlin задается вопросом: «Обозначен ли вызов лямбды как Composable?» Если нет, значит определенный плагином компилятора Compose контракт нарушен и, чтобы отобразить диагностические данные, на этапе разрешения вычисляются сообщения об ошибках. 

В сообщениях об ошибках показывается, что вызовы @Composable могут выполняться только из контекста функции Composable

Для определения корректных правил Compose относительно высокоуровневых объявлений, членов класса и локальных объявлений внутри  —  функций, значений и т. д.  —  в ComposableDeclarationChecker применяется аналогичный подход с рассмотрением PSI и его свойств. Например, в Compose желательно убедиться, что функции main случайно не помечены как тип Composable:

import androidx.compose.runtime.Composable@Composable
fun main(args: Array<String>) { // Не компилируется
print(args)
}

Когда этим RawFIR пройден этап разрешения, FIR преобразуется в промежуточное представление бэкенда, которое становится входными данными для бэкенда.

После того как завершен анализ для создания полного FIR, генерируется FIR и компилируется в формат данных IR для бэкенда или плагинов IDE

Диагностика после отправки разрешения в IDE

После этапа разрешения FIR отправляется в плагин IDE  —  для немедленного получения обратной связи в IDE  —  и/или на бэкенд компилятора Kotlin. Плагины IDE не рассматриваем, так как они отличаются от плагинов компилятора.

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

Скриншот Android Studio с аналогом плагина IDE для той же функции main. Ключевое слово main подчеркнуто красной волнистой линией, и появляется модальное окно с сообщением: «Функции main Composable в настоящее время не поддерживаются»

Стоит отметить, что в список диагностических предупреждений/ошибок добавляется ComposeDiagnosticSupressor. Так минуются языковые ограничения, которые в противном случае чреваты сбоем компиляции в Kotlin. Вот, например, возможность аннотировать @Composable для вызова функциями высшего порядка другой Composable:

@Composable
fun HelloWorld(content: @Composable (()->Unit)) { // Компилируется
content
}

Если дело пойдет, напишем о запуске тестов FIR- компилятора.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Amanda Hinchman: Reverse-Engineering the Compose Compiler Plugin: Intercepting the Frontend

Предыдущая статьяШаблон «Стратегия» на Go
Следующая статьяPalette: оживляем гибридные узлы Amazon EKS