Detekt-набор

Правила 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 внутри машины.
Зачем отдельный набор Целевая модель проекта — MEL. Правила фиксируют её инварианты автоматически, чтобы экран не «съезжал» обратно к толстым моделям, мутабельному стейту и эффектам в reduce. Каноническое обоснование каждого правила — в спеке.

Подключение

Нужно три вещи: плагин 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
}
Внешний проект — берите опубликованный артефакт Набор опубликован в Nexus как 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 проекта).

Грабля: gradle-демон кэширует плагин После пересборки :tools:detekt-rules остановите демон — ./gradlew --stop — иначе detekt подхватит старую версию классов правил.

Как читать правило

У каждого правила есть id (имя класса), severity и долг (debt). В таблицах ниже:

  • Defect — нарушение инварианта, ломающего рантайм/тесты (мутабельный стейт, эффект в reduce, корутина в машине). Чинить обязательно.
  • Style — отклонение от модели, которое стоит исправить, но рантайм оно не ломает сразу (нет onError, контракт не sealed, новый наследник legacy-MVI).

Колонка «срабатывает на» — то, что правило ищет в коде; «зачем» — какой инвариант оно бережёт.

Проверки 26 правил · 5 групп

Группировка повторяет порядок регистрации в MelRuleSetProvider.

Базовая восьмёрка фундамент модели

ПравилоСрабатывает наЗачем
MelNoCoroutinesInMachine
Defect
launch/async/runBlocking или поле CoroutineScope/GlobalScope в теле машины или секции Машина не запускает корутины сама — асинхронщина только через команды и подписки
MelSubscribeOnError
Style
subscribe без аргумента onError Падающий источник должен маппить ошибку в интент, а не умирать молча
MelEffectsAreData
Defect
Поле или параметр функционального типа (лямбда-коллбэк) внутри эффекта Эффект — это данные; коллбэк протаскивает поведение мимо контракта
MelNoMutableEffectContext
Defect
var-свойства или Mutable*-типы в классе MelEffectContext Контекст края экрана — иммутабельный набор инструментов
MelImmutableFacts
Defect
var или мутабельные коллекции / MutableState в стейте/фактах Стейт и факты иммутабельны — переход только через copy()
MelNoInstrumentsInState
Defect
Поведенческие инструменты (TextFieldState, Context, Modifier, NavController…) в стейте/фактах Compose/Android-инструментам место в UiHandles, не в стейте
MelNoTypeChecks
Defect
is/as по контракт-типам (суффиксы State/Intent/Effect) внутри машины Связь с контрактом — типизированная регистрация on<>/view<>/subscribe<>, а не смарт-каст
MelMigrationRatchet
Style
Новый наследник SimpleMviViewModel/MviModel/MviViewModel вне allowlist Целевая модель — MEL; новые экраны не расширяют v3/SimpleMvi

Контракты-данные стейт / факты / интент / эффект

ПравилоСрабатывает наЗачем
MelDataContractLeaf
Defect
Конкретный лист контракта объявлен не как data Identity-равенство ломает дедуп StateFlow и сравнение эффектов в тестах
MelNoLambdaInContract
Defect
Поле/параметр функционального типа в стейте, фактах или интенте Лямбды ломают equality и протаскивают эффект в reduce
MelNoPlatformTypesInContract
Defect
Платформенные инструменты (Context, Modifier, SnackbarHostState…) в интенте или эффекте Интент уходит в чистый reduce, эффект — это данные; инструментам место в UiHandles
MelNoBehaviorInContract
Style
Член-функция с бизнес-логикой («толстая модель») в типе контракта Решения принимает reduce клетки, а не методы контракта
MelSealedContractRoot
Style
Корень контракта объявлен не как sealed Иначе листы расходятся по файлам, ломая исчерпываемость when и разрешение маркеров в пределах файла

Тело и поля машины stateless + DSL-граф

ПравилоСрабатывает наЗачем
MelStatelessMachine
Defect
Мутабельные поля экземпляра или компаньона машины/секции (MutableStateFlow, Atomic*, mutableListOf…) Машина stateless — всё состояние в MelState
MelNoSendInReduce
Defect
Вызов send(intent) внутри машины/секции Ре-диспетчеризация минует переход клетки, гонится с петлёй и невидима перехватчикам — оформи цепочку командой
MelNoRawFrameworkConstructor
Defect
Сырой конструктор каркаса (Transition, Command, Graph, Cell…) вместо DSL DSL держит инварианты и виден статическому walker'у; сырой конструктор их обходит
MelCommandOnErrorMustReturnIntent
Defect
onError команды/подписки перебрасывает ошибку вместо маппинга в интент Переброс гасит job и оставляет вечный спиннер

Чистота reduce чистый и синхронный шаг

ПравилоСрабатывает наЗачем
MelNoThrowInReduce
Defect
throw или error/require/check/TODO в теле клетки on/onAny reduce завершается переходом — верни стейт/эффект ошибки вместо исключения
MelNoMutationInReduce
Defect
Присваивание захваченному полю (=, +=…) в reduce Единственный выход reduce — переход через copy() над иммутабельным стеком
MelNoSideEffectsInReduce
Defect
Навигация, логирование, аналитика (navigate, log, track…) в reduce Это возвращённые данные-эффекты, а reduce чист
MelNoNonDeterminismInReduce
Defect
System.currentTimeMillis, UUID.randomUUID, Random и пр. в reduce Недетерминизм ломает реплей и golden-тесты — время и случайность приходят фактом на интенте
MelNoBlockingInReduce
Defect
Блокирующий дренаж будущего (blockingGet, getCompleted…) в reduce reduce синхронен — асинхронщину оформляй командой/подпиской

Async и эффект-слой команды, подписки, эффекты

ПравилоСрабатывает наЗачем
MelBlockingInCommandOrSubscription
Defect
Блокирующий вызов (blockingGet, blockingFirst…) в теле command/subscribe Тело async-блока должно приостанавливаться, а не блокировать общий loop-диспетчер
MelHotFlowSubscriptionSource
Style
Горячий/неконечный поток как source подписки (MutableStateFlow, stateIn, shareIn…) Подписка не завершится, onError мёртв — нужен холодный завершающийся-или-отменяемый Flow
MelNoManualEffectCollection
Defect
Ручной collect/onEach/launchIn по model.effects Канал одного потребителя — второй коллектор крадёт навигацию/снэкбары из effects {}
MelNoRouterCloseEffect
Style
Прямой 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 SimpleMvi when-is вне машины.
  • Severity ≠ блокировка. Завалит ли Style-правило сборку — зависит от maxIssues/baseline проекта, а не от самого правила.
  • Источник истины — код и спека. Если поведение правила разошлось с этой страницей — прав код в :tools:detekt-rules и спека.

Набор: :tools:detekt-rules · ruleset mel · 26 правил · detekt 1.23.4 · публикация ru.mel:detekt:0.1.0.