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

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

Это происходит из-за плагинов компилятора Jetpack Compose, которыми в IDE отправляются диагностические данные для получения обратной связи от пользователей. Посмотрим, как эта диагностика извлекается компилятором Kotlin, при том что поведение во время выполнения и компиляция кода перехватываются плагинами компилятора.
Изучим компилятор Kotlin, выполнив небольшой реверсинг. Понадобятся две карты: компилятора Kotlin и архитектуры плагинов компилятора.
Внимание: возможна путаница между частями этапов компиляции и Analysis API, который не совсем то же, что конвейер компиляции, хоть и связан с ним. Продолжаю прорабатывать это, особенно с этапом разрешения. Но работа компилятора изучается при его тестировании, хотя концепции Analysis API более продвинуты. Вот документация по Analysis API.
Примечание: Роли, выполняемые на разных этапах для плагина компилятора Compose меняются. Функциональность компилятора со временем тоже.
Компилятор Kotlin
Обсуждения плагинов компилятора осуществляются в контексте самого компилятора. Но делать это в одиночку опасно: чтение статьи желательно продолжить, открыв эти ресурсы.
Начнем с общей архитектуры компилятора Kotlin, а подробнее — с фронтенда компилятора. Здесь имеется две текущие версии: новый фронтенд и старый. Возьмем новый, называемый FIR-фронтендом, то есть промежуточным представлением фронтенда, или компилятором k2. Вот он схематично:

Если упрощенно: многократным сканированием кода компилятором создаются новые форматы данных и продолжается самодополнение с получением более полной картины.
Если еще проще, компилятором многократно выполняются:
- компилирование: данные компилируются в новый формат.
- уменьшение: иногда упрощается и оптимизируется имеющийся формат данных, для выполнения компиляторного анализа данные добавляются к имеющейся структуре.
Имея общее представление и карту компилятора Kotlin, посмотрим теперь, как плагины компилятора встраиваются в компилятор. А затем заглянем внутрь плагина компилятора Compose.
Архитектура плагина компилятора
Простейшее объяснение архитектуры плагинов компилятора изложено в докладе Кевина Моста:

Вкратце резюмируем этот доклад и создадим «мысленную карту» того, как ориентироваться в плагине компилятора:
Плагин
- 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.

Что перехватывается плагином компилятора 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 еще используются:

Рассмотрев содержимое самого плагина компилятора Compose, выясним, что и где перехватывается при прохождении частей этапов этого компилятора.
Плагины компилятора подключаются к любой части компилятора Kotlin. Запустив код через компилятор, которым настраивается плагин Gradle, мы увидим:
- Как код перемещается по компилятору при компилировании и уменьшении данных.
- Как, чтобы изменить поведение при компиляции, плагином компилятора Compose перехватываются и расширяются конкретные этапы компилятора Kotlin.
Вернемся к исходному вопросу. Напишем такой код в IDE и сконфигурируем зависимость Compose в проект:
@Composable fun HelloWorld() { Text("Hello!") }
Мысленно проследим этот код по карте компилятора Kotlin, тестируя FIR-компилятор. Оставим точки отладки в классах перехватываемого плагином компилятора и отследим в стеке вызовов до места, где находятся исходные этапы компилятора.
Этап синтаксического анализа

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

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

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

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 дополняется этой информацией, затем проводится анализ для проверок на предмет верификации, проверки типов и т. д.

В 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 контракт нарушен и, чтобы отобразить диагностические данные, на этапе разрешения вычисляются сообщения об ошибках.

Для определения корректных правил Compose относительно высокоуровневых объявлений, членов класса и локальных объявлений внутри — функций, значений и т. д. — в ComposableDeclarationChecker применяется аналогичный подход с рассмотрением PSI и его свойств. Например, в Compose желательно убедиться, что функции main случайно не помечены как тип Composable:
import androidx.compose.runtime.Composable@Composable
fun main(args: Array<String>) { // Не компилируется
print(args)
}
Когда этим RawFIR пройден этап разрешения, FIR преобразуется в промежуточное представление бэкенда, которое становится входными данными для бэкенда.

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

main. Ключевое слово main подчеркнуто красной волнистой линией, и появляется модальное окно с сообщением: «Функции main Composable в настоящее время не поддерживаются»Стоит отметить, что в список диагностических предупреждений/ошибок добавляется ComposeDiagnosticSupressor. Так минуются языковые ограничения, которые в противном случае чреваты сбоем компиляции в Kotlin. Вот, например, возможность аннотировать @Composable для вызова функциями высшего порядка другой Composable:
@Composable
fun HelloWorld(content: @Composable (()->Unit)) { // Компилируется
content
}
Если дело пойдет, напишем о запуске тестов FIR- компилятора.
Читайте также:
- Реализация бесконечной прокрутки списка новостей в приложении TrendNow. Часть 4
- Вопросы для собеседования по Android: как обрабатывать валидацию ввода в Jetpack Compose?
- 5 функций-расширений в арсенале каждого разработчика Jetpack Compose
Читайте нас в Telegram, VK и Дзен
Перевод статьи Amanda Hinchman: Reverse-Engineering the Compose Compiler Plugin: Intercepting the Frontend





