Правила MEL для detekt
Отдельный ruleset mel ловит нарушения архитектуры MEL прямо в сборке:
машина без корутин, контракты-данные, чистый reduce, async только через команды и подписки.
Что это
Набор живёт в модуле :tools:detekt-rules и подключается как detekt-плагин. Это
26 правил с идентификатором ruleset mel. Все правила — PSI-only: они
читают синтаксис исходника и не требуют type resolution. За счёт этого они работают одинаково и в
CLI/Gradle, и под IDE-плагином detekt (который TR не поддерживает).
- Контракт (стейт / факты / интент / эффект) узнаётся по маркеру — простому имени
MelState/MelFacts/MelIntent/MelEffect, достижимому по иерархии в пределах одного файла. - Машина и секция узнаются по прямому супертипу
MelMachine/Section. - reduce-клетка узнаётся по лямбде вызовов
on/onAnyвнутри машины.
Подключение
Нужно три вещи: плагин detekt, конфиг проекта и зависимость detektPlugins на набор
правил. Модуль при этом может быть любым — Android-библиотекой, приложением или kotlin-jvm.
1. Плагин и версия
Версия detekt задаётся в каталоге версий и применяется в корневом build.gradle.kts
с apply false:
# gradle/libs.versions.toml
[versions]
detekt = "1.23.4"
[plugins]
detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
2. Модуль-экран
В build.gradle.kts модуля, который содержит MEL-экраны:
plugins {
alias(libs.plugins.detekt)
}
detekt {
buildUponDefaultConfig = true // дефолтные правила + наши отклонения
config.setFrom(rootProject.file("config/detekt/detekt.yml"))
autoCorrect = false
}
dependencies {
detektPlugins(project(":tools:detekt-rules")) // набор правил mel
}
ru.mel:detekt:0.1.0. Вне монорепо вместо
project(":tools:detekt-rules") подключайте координаты:
detektPlugins("ru.mel:detekt:0.1.0")
3. Конфиг — секция mel
Файл config/detekt/detekt.yml уже содержит всю секцию mel: с
active: true и параметрами каждого правила. Включить весь набор:
# config/detekt/detekt.yml
mel:
active: true
MelNoCoroutinesInMachine:
active: true
machineBases: ['MelMachine', 'Section']
# … остальные 25 правил со своими параметрами
В репозитории секция уже заполнена — отдельно прописывать правила не нужно. Параметры (списки маркеров, имён вызовов, allowlist) меняют только когда расширяют контракт или переименовывают базовые типы.
Запуск и отчёт
Правила — часть обычной задачи detekt:
# весь проект
./gradlew detekt
# один модуль
./gradlew :sample:migration:detekt
./gradlew :app:detekt
Нарушение печатается как mel/<ИмяПравила> с позицией и текстом. Severity
Defect заваливает сборку detekt; Style сообщает, но по умолчанию сборку не валит
(зависит от порога maxIssues/baseline проекта).
:tools:detekt-rules остановите демон — ./gradlew --stop —
иначе detekt подхватит старую версию классов правил.
Как читать правило
У каждого правила есть id (имя класса), severity и долг (debt). В таблицах ниже:
- Defect — нарушение инварианта, ломающего рантайм/тесты (мутабельный стейт, эффект в reduce, корутина в машине). Чинить обязательно.
- Style — отклонение от модели, которое стоит исправить, но
рантайм оно не ломает сразу (нет
onError, контракт неsealed, новый наследник legacy-MVI).
Колонка «срабатывает на» — то, что правило ищет в коде; «зачем» — какой инвариант оно бережёт.
Проверки 26 правил · 5 групп
Группировка повторяет порядок регистрации в MelRuleSetProvider.
Базовая восьмёрка фундамент модели
| Правило | Срабатывает на | Зачем |
|---|---|---|
MelNoCoroutinesInMachineDefect |
launch/async/runBlocking или поле
CoroutineScope/GlobalScope в теле машины или секции |
Машина не запускает корутины сама — асинхронщина только через команды и подписки |
MelSubscribeOnErrorStyle |
subscribe без аргумента onError |
Падающий источник должен маппить ошибку в интент, а не умирать молча |
MelEffectsAreDataDefect |
Поле или параметр функционального типа (лямбда-коллбэк) внутри эффекта | Эффект — это данные; коллбэк протаскивает поведение мимо контракта |
MelNoMutableEffectContextDefect |
var-свойства или Mutable*-типы в классе MelEffectContext |
Контекст края экрана — иммутабельный набор инструментов |
MelImmutableFactsDefect |
var или мутабельные коллекции / MutableState в стейте/фактах |
Стейт и факты иммутабельны — переход только через copy() |
MelNoInstrumentsInStateDefect |
Поведенческие инструменты (TextFieldState, Context,
Modifier, NavController…) в стейте/фактах |
Compose/Android-инструментам место в UiHandles, не в стейте |
MelNoTypeChecksDefect |
is/as по контракт-типам (суффиксы State/Intent/Effect) внутри машины |
Связь с контрактом — типизированная регистрация on<>/view<>/subscribe<>, а не смарт-каст |
MelMigrationRatchetStyle |
Новый наследник SimpleMviViewModel/MviModel/MviViewModel вне allowlist |
Целевая модель — MEL; новые экраны не расширяют v3/SimpleMvi |
Контракты-данные стейт / факты / интент / эффект
| Правило | Срабатывает на | Зачем |
|---|---|---|
MelDataContractLeafDefect |
Конкретный лист контракта объявлен не как data |
Identity-равенство ломает дедуп StateFlow и сравнение эффектов в тестах |
MelNoLambdaInContractDefect |
Поле/параметр функционального типа в стейте, фактах или интенте | Лямбды ломают equality и протаскивают эффект в reduce |
MelNoPlatformTypesInContractDefect |
Платформенные инструменты (Context, Modifier, SnackbarHostState…) в интенте или эффекте |
Интент уходит в чистый reduce, эффект — это данные; инструментам место в UiHandles |
MelNoBehaviorInContractStyle |
Член-функция с бизнес-логикой («толстая модель») в типе контракта | Решения принимает reduce клетки, а не методы контракта |
MelSealedContractRootStyle |
Корень контракта объявлен не как sealed |
Иначе листы расходятся по файлам, ломая исчерпываемость when и разрешение маркеров в пределах файла |
Тело и поля машины stateless + DSL-граф
| Правило | Срабатывает на | Зачем |
|---|---|---|
MelStatelessMachineDefect |
Мутабельные поля экземпляра или компаньона машины/секции (MutableStateFlow, Atomic*, mutableListOf…) |
Машина stateless — всё состояние в MelState |
MelNoSendInReduceDefect |
Вызов send(intent) внутри машины/секции |
Ре-диспетчеризация минует переход клетки, гонится с петлёй и невидима перехватчикам — оформи цепочку командой |
MelNoRawFrameworkConstructorDefect |
Сырой конструктор каркаса (Transition, Command, Graph, Cell…) вместо DSL |
DSL держит инварианты и виден статическому walker'у; сырой конструктор их обходит |
MelCommandOnErrorMustReturnIntentDefect |
onError команды/подписки перебрасывает ошибку вместо маппинга в интент |
Переброс гасит job и оставляет вечный спиннер |
Чистота reduce чистый и синхронный шаг
| Правило | Срабатывает на | Зачем |
|---|---|---|
MelNoThrowInReduceDefect |
throw или error/require/check/TODO в теле клетки on/onAny |
reduce завершается переходом — верни стейт/эффект ошибки вместо исключения |
MelNoMutationInReduceDefect |
Присваивание захваченному полю (=, +=…) в reduce |
Единственный выход reduce — переход через copy() над иммутабельным стеком |
MelNoSideEffectsInReduceDefect |
Навигация, логирование, аналитика (navigate, log, track…) в reduce |
Это возвращённые данные-эффекты, а reduce чист |
MelNoNonDeterminismInReduceDefect |
System.currentTimeMillis, UUID.randomUUID, Random и пр. в reduce |
Недетерминизм ломает реплей и golden-тесты — время и случайность приходят фактом на интенте |
MelNoBlockingInReduceDefect |
Блокирующий дренаж будущего (blockingGet, getCompleted…) в reduce |
reduce синхронен — асинхронщину оформляй командой/подпиской |
Async и эффект-слой команды, подписки, эффекты
| Правило | Срабатывает на | Зачем |
|---|---|---|
MelBlockingInCommandOrSubscriptionDefect |
Блокирующий вызов (blockingGet, blockingFirst…) в теле command/subscribe |
Тело async-блока должно приостанавливаться, а не блокировать общий loop-диспетчер |
MelHotFlowSubscriptionSourceStyle |
Горячий/неконечный поток как source подписки (MutableStateFlow, stateIn, shareIn…) |
Подписка не завершится, onError мёртв — нужен холодный завершающийся-или-отменяемый Flow |
MelNoManualEffectCollectionDefect |
Ручной collect/onEach/launchIn по model.effects |
Канал одного потребителя — второй коллектор крадёт навигацию/снэкбары из effects {} |
MelNoRouterCloseEffectStyle |
Прямой router.close() в эффект-слое |
Закрытие — это данные контракта (MelCloseEffect/MelExhausted), а не сырой вызов роутера |
Тонкая настройка
Проект включает buildUponDefaultConfig = true, поэтому detekt.yml
хранит только отклонения от дефолта плюс секцию mel.
Осознанно отключённый style
Эталонный MVP смотрит на правила MEL, а не на общий kotlin-style, поэтому в дефолтном наборе
выключены: FunctionNaming (экраны — @Composable в PascalCase),
PackageNaming, TooManyFunctions, MagicNumber,
UnusedPrivateMember/UnusedParameter (превью «не используются» по
построению), UseCheckOrError. Включать общий style — отдельной задачей.
Параметры правил, которые правят чаще всего
- Маркеры контракта (
contractMarkers) — простые имена базовых типов (MelState,MelFacts,MelIntent,MelEffect). Меняют при переименовании базовых интерфейсов контракта. - Базы машины (
machineBases: ['MelMachine', 'Section']) — якорь для всех «внутри машины»-правил. - Allowlist у
MelMigrationRatchet— FQN экранов, которым временно разрешено наследовать legacy-MVI (пилоты переноса). Уменьшается по мере миграции. - Списки запрещённых инструментов / вызовов (
bannedInstrumentTypes,bannedCalls,blockingCallees…) — расширяют под новые API.
flagNow (часы вроде Instant.now() в reduce),
enableGet/enableRunBlocking, checkMutatingCalls,
flagInteractorVerbs, flagPlainObjects. Включают точечно, когда хотят
строже.
Границы и оговорки
- PSI без type resolution. Контракт определяется по маркеру/суффиксу имени, а не по реальному типу. Это даёт работу под IDE-плагином, но значит: класс с «контрактным» именем без маркера в файле правило не увидит, а маркер должен быть достижим в пределах того же файла.
- Якорь машины — прямой супертип. Правила «внутри машины» смотрят на непосредственный
: MelMachine/: Section. Глубокие цепочки наследования не отслеживаются — это намеренно, чтобы не ловить legacy SimpleMviwhen-isвне машины. - Severity ≠ блокировка. Завалит ли Style-правило сборку — зависит от
maxIssues/baseline проекта, а не от самого правила. - Источник истины — код и спека. Если поведение правила разошлось с этой страницей —
прав код в
:tools:detekt-rulesи спека.
Набор: :tools:detekt-rules ·
ruleset mel · 26 правил · detekt 1.23.4 · публикация ru.mel:detekt:0.1.0.