Правила MEL для detekt
Отдельный ruleset mel ловит нарушения архитектуры MEL прямо в сборке:
машина без корутин, контракты-данные, чистый reduce, async только через команды и подписки.
Что это
Набор живёт в модуле :tools:detekt-rules и подключается как detekt-плагин. Это
26 правил с идентификатором ruleset mel. Все правила — PSI-only: они
читают синтаксис исходника и не требуют type resolution. За счёт этого они работают одинаково и в
CLI/Gradle, и под IDE-плагином detekt (который TR не поддерживает).
Те же 26 проверок есть в IDE-плагине MEL как инспекции редактора
(уровень Warning, включены по умолчанию). PSI-хелперы (MelPsi.kt) у них общие с набором,
но параметры зашиты дефолтами — detekt.yml на инспекции не действует. Quick-fix есть
только у MelSubscribeOnError и MelDataContractLeaf.
- Контракт (стейт / факты / интент / эффект) узнаётся по маркеру — простому имени
MelState/MelFacts/MelIntent/MelEffect, достижимому по иерархии в пределах одного файла. - Машина и секция узнаются по прямому супертипу
MelMachine/Section. - reduce-клетка узнаётся по лямбде вызовов
on/onAnyвнутри машины.
Подключение
Нужно четыре вещи: репозиторий артефактов MEL, версии в каталоге, зависимость detektPlugins
на набор правил и секция mel в конфиге detekt. Ниже — по образцу боевого потребителя
(Groovy DSL). Модуль при этом может быть любым — Android-библиотекой, приложением или kotlin-jvm.
1. Репозиторий артефактов
Группа ru.sberbank_sbbol.mel закрепляется за репозиторием артефактов MEL через
exclusiveContent в settings.gradle: артефакты MEL Gradle ищет только там, а за
остальными зависимостями туда не ходит.
// settings.gradle
dependencyResolutionManagement {
repositories {
// … остальные репозитории проекта
exclusiveContent {
forRepository {
maven {
url "<репозиторий артефактов MEL>"
// креды — по месту
}
}
filter { includeGroup "ru.sberbank_sbbol.mel" }
}
}
}
ru.sberbank_sbbol.mel:detekt:0.2.0 (JAR + sources) в
репозиторий артефактов MEL — git-трекаемый maven-репозиторий mel-maven/mel-repo в
репозитории MEL, который раздаётся как raw. Адрес и креды — по месту.
2. Версии в каталоге
Набор собран против detekt-api 1.23.4 — на этой же версии detekt и потребитель.
Версия набора — общая ссылка mel с остальными артефактами MEL:
# gradle/libs.versions.toml
[versions]
detekt = "1.23.4"
mel = "0.2.0"
[libraries]
mel-detekt = { module = "ru.sberbank_sbbol.mel:detekt", version.ref = "mel" }
3. Плагин detekt и набор правил
Gradle-плагин detekt лежит в classpath корневого build.gradle, а ко всем модулям его
применяет общий скрипт (apply from: 'devops/detekt/detekt.gradle'). Набор mel
добавляется в конфигурацию detektPlugins:
// devops/detekt/detekt.gradle
subprojects {
apply plugin: "io.gitlab.arturbosch.detekt"
detekt {
config.setFrom("${rootProject.projectDir}/devops/detekt/detekt-config.yml")
// … baseline, source и прочие проектные настройки
}
dependencies {
detektPlugins libs.mel.detekt // набор правил mel
}
}
4. Конфиг — секция mel
В JAR набора нет дефолтного конфига, а detekt 1.23 считает правило без ключа active
выключенным (сам ruleset mel по умолчанию включён). Поэтому каждое из 26 правил
перечисляют явно с ключом active; параметры можно не писать — их дефолты зашиты в код правил:
# devops/detekt/detekt-config.yml
mel:
active: true
MelNoCoroutinesInMachine:
active: true
MelSubscribeOnError:
active: true
MelMigrationRatchet:
active: false # legacy-MVI в проекте ещё много — см. «Тонкая настройка»
# … так же остальные 23 правила
Образец секции mel: со всеми параметрами и их дефолтами — config/detekt/detekt.yml
в репозитории MEL. Параметры (списки маркеров, имён вызовов, allowlist) меняют только когда
расширяют контракт или переименовывают базовые типы.
Запуск и отчёт
Правила — часть обычной задачи detekt:
# весь проект
./gradlew detekt
# один модуль
./gradlew :<модуль>:detekt
В консоли нарушение печатается строкой файл:строка:колонка: текст [ИмяПравила].
Сборку валит не severity, а порог build.maxIssues с учётом weights и
baseline проекта. В detekt 1.23 дефолт maxIssues: 0, то есть сборку заваливает любое
нарушение — и Defect, и Style.
Как читать правило
У каждого правила есть id (имя класса), severity и долг (debt). В таблицах ниже:
- Defect — нарушение инварианта, ломающего рантайм/тесты (мутабельный стейт, эффект в reduce, корутина в машине). Чинить обязательно.
- Style — отклонение от модели, которое стоит исправить, но
рантайм оно не ломает сразу (нет
onError, контракт неsealed, новый наследник legacy-MVI).
Колонка «срабатывает на» — то, что правило ищет в коде; «зачем» — какой инвариант оно бережёт.
Проверки 26 правил · 5 групп
Группировка повторяет порядок регистрации в MelRuleSetProvider.
Базовая восьмёрка фундамент модели
| Правило | Срабатывает на | Зачем |
|---|---|---|
MelNoCoroutinesInMachineDefect |
launch/async/runBlocking или поле
CoroutineScope/GlobalScope в теле машины или секции |
Машина не запускает корутины сама — асинхронщина только через команды и подписки |
MelSubscribeOnErrorStyle |
subscribe в машине/секции без аргумента onError. Молчит, если
onError не нужен: источник горячий, ошибку гасит .catch, поток из
flowOf/emptyFlow, app-lifetime шина (.asFlow(),
listen*/observe*), либо key в allowedKeysWithoutError |
Падающий источник должен маппить ошибку в интент, а не умирать молча |
MelEffectsAreDataDefect |
Поле или параметр функционального типа (лямбда-коллбэк) внутри эффекта | Эффект — это данные; коллбэк протаскивает поведение мимо контракта |
MelNoMutableEffectContextDefect |
var-свойства или MutableState/MutableStateFlow/MutableSharedFlow
в наследнике 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 (интент не
проверяется; обычный object — только при flagPlainObjects) |
Identity-равенство ломает дедуп StateFlow и сравнение эффектов в тестах |
MelNoLambdaInContractDefect |
Поле/параметр функционального типа в стейте, фактах или интенте | Лямбды ломают equality и протаскивают эффект в reduce |
MelNoPlatformTypesInContractDefect |
Платформенные инструменты (Context, Modifier, SnackbarHostState…) в интенте или эффекте |
Интент уходит в чистый reduce, эффект — это данные; инструментам место в UiHandles |
MelNoBehaviorInContractStyle |
Член-функция с телом («толстая модель») в типе контракта; не трогаются
override/operator и функции в object/companion object |
Решения принимает reduce клетки, а не методы контракта |
MelSealedContractRootStyle |
Корень, напрямую реализующий маркер, объявлен открытым (interface,
abstract или open), а не sealed |
Иначе листы расходятся по файлам, ломая исчерпываемость when и разрешение маркеров в пределах файла |
Тело и поля машины stateless + DSL-граф
| Правило | Срабатывает на | Зачем |
|---|---|---|
MelStatelessMachineDefect |
Мутабельные поля экземпляра или компаньона машины/секции (var, MutableStateFlow, Atomic*, mutableListOf…) |
Машина stateless — всё состояние в MelState |
MelNoSendInReduceDefect |
Вызов send(intent) без получателя где угодно внутри машины/секции, включая тела
command (channel.send(…) не трогается) |
Ре-диспетчеризация минует переход клетки, гонится с петлёй и невидима перехватчикам — оформи цепочку командой |
MelNoRawFrameworkConstructorDefect |
Сырой конструктор каркаса (Transition, Command, Graph, Cell…) вместо DSL;
поле graph, собранное не билдером graph { } |
DSL держит инварианты и виден статическому walker'у; сырой конструктор их обходит |
MelCommandOnErrorMustReturnIntentDefect |
onError команды/подписки перебрасывает ошибку вместо маппинга в интент
(последний стейтмент лямбды — throw) |
Переброс гасит 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…) или Thread.sleep в reduce |
reduce синхронен — асинхронщину оформляй командой/подпиской |
Async и эффект-слой команды, подписки, эффекты
| Правило | Срабатывает на | Зачем |
|---|---|---|
MelBlockingInCommandOrSubscriptionDefect |
Блокирующий вызов (blockingGet, blockingFirst…, Thread.sleep) в теле command/subscribe |
Тело async-блока должно приостанавливаться, а не блокировать общий loop-диспетчер |
MelHotFlowSubscriptionSourceStyle |
Горячий/неконечный поток как source подписки (MutableStateFlow, stateIn, shareIn…) |
Подписка не завершится, onError мёртв — нужен холодный завершающийся-или-отменяемый Flow |
MelNoManualEffectCollectionDefect |
Ручной collect/onEach/launchIn/collectAsState… по model.effects;
в файле, где упомянут MelModel/MelScreen, — по любому получателю |
Канал одного потребителя — второй коллектор крадёт навигацию/снэкбары из effects {} |
MelNoRouterCloseEffectStyle |
Прямой router.close() в эффект-слое (наследник MelEffectContext или блок effects { }) |
Закрытие — это данные контракта (MelCloseEffect/MelExhausted), а не сырой вызов роутера |
Тонкая настройка
У каждого параметра есть дефолт в коде правила; в detekt.yml пишут только то, что
отличается. buildUponDefaultConfig на секцию mel не влияет: дефолтный
конфиг detekt её не содержит.
Параметры правил, которые правят чаще всего
- Маркеры контракта (
contractMarkers) — простые имена базовых типов (MelState,MelFacts,MelIntent,MelEffect). Меняют при переименовании базовых интерфейсов контракта. - Базы машины (
machineBases: ['MelMachine', 'Section']) — якорь для всех «внутри машины»-правил. - Allowlist у
MelMigrationRatchet— FQN экранов, которым временно разрешено наследовать legacy-MVI (пилоты переноса). Уменьшается по мере миграции. Дефолт в коде — два демо-экранаru.sberbank_sbbol.mel.sample.migration.*, так что в проекте с legacy-MVI список заполняют своими FQN либо выключают правило (боевой потребитель держитMelMigrationRatchet: active: false). У IDE-инспекции allowlist пуст. - Списки запрещённых инструментов / вызовов (
bannedInstrumentTypes,bannedCalls,blockingCallees…) — расширяют под новые API.
flagNow (часы вроде Instant.now() в reduce),
enableGet/enableRunBlocking, checkMutatingCalls,
flagInteractorVerbs, flagPlainObjects, flagAmbiguousGetExecute,
flagDataObjectsAndEnums. Включают точечно, когда хотят строже.
Границы и оговорки
- PSI без type resolution. Контракт определяется по маркеру/суффиксу имени, а не по реальному типу. Это даёт работу под IDE-плагином, но значит: класс с «контрактным» именем без маркера в файле правило не увидит, а маркер должен быть достижим в пределах того же файла.
- Якорь машины — прямой супертип. Правила «внутри машины» смотрят на непосредственный
: MelMachine/: Section. Глубокие цепочки наследования не отслеживаются — это намеренно, чтобы не ловить legacy SimpleMviwhen-isвне машины. - Известные пропуски (из KDoc правил). Не ловятся: лист контракта в другом файле, чем
маркированный корень;
throwзаif/whenили в хелпере (MelCommandOnErrorMustReturnIntent);typealiasна конструктор каркаса (MelNoRawFrameworkConstructor); мутабельная фабрика под алиасом (MelStatelessMachine);now()без приёмника (MelNoNonDeterminismInReduce); горячий поток за вызовом метода (MelHotFlowSubscriptionSource);router.close()вне эффект-контекста иeffects { }; каст контракта вне машины/секции. - Источник истины — код и спека. Если поведение правила разошлось с этой страницей —
прав код в
:tools:detekt-rulesи спека.
Набор: :tools:detekt-rules ·
ruleset mel · 26 правил · detekt 1.23.4 · артефакт ru.sberbank_sbbol.mel:detekt:0.2.0.