--- title: Линтеры и формат description: ktlint и detekt — автоформатирование, статический анализ, конфигурация и интеграция в CI tags: - kotlin - tooling - quality --- Два инструмента с разными задачами: **ktlint** приводит код к официальному стилю и умеет исправлять сам, **detekt** ищет проблемы смысла - сложные функции, мёртвый код, подозрительные конструкции. | | ktlint | detekt | |---|---|---| | проверяет | форматирование и имена | сложность, запахи кода | | автоисправление | да, большинство правил | частично | | настройка | `.editorconfig`, минимум правил | `detekt.yml`, ~200 правил | | нужен ли compile classpath | нет | нет (есть опциональный type resolution) | | скорость | быстро | медленнее | ```kotlin // build.gradle.kts plugins { kotlin("jvm") version "2.4.0" id("org.jlleitschuh.gradle.ktlint") version "12.1.2" id("io.gitlab.arturbosch.detekt") version "1.23.8" } repositories { mavenCentral() } detekt { buildUponDefaultConfig = true allRules = false } ``` ## ktlint Реализует официальный Kotlin coding conventions. Настроек почти нет - это и есть идея: не спорить о стиле. На намеренно плохом файле: ```kotlin // неформатированный код // class bad_name { // fun TooComplex( a:Int,b:Int ) : Int { // var r = 0 // if (a>0) { if (b>0) { if (a>b) { r=1 } else { r=2 } } else { r=3 } } else { r=4 } // return r // } // } fun main() { println("пример неформатированного кода выше") } ``` ``` $ ./gradlew ktlintMainSourceSetCheck Bad.kt:3:7 Class or object name should start with an uppercase letter and use camel case (cannot be auto-corrected) Bad.kt:4:9 Function name should start with a lowercase letter (except factory methods) and use camel case (cannot be auto-corrected) Bad.kt:4:20 Unexpected spacing after "(" Bad.kt:4:23 Missing spacing after ":" Bad.kt:4:27 Missing spacing after "," Bad.kt:6:14 Missing spacing around ">" Bad.kt:6:18 A single line if-statement should be kept simple. The 'THEN' may not be wrapped in a block. (cannot be auto-corrected) Bad.kt:6:20 Missing newline after '{' ``` `./gradlew ktlintFormat` исправляет всё, что помечено как исправимое: ```kotlin // после ktlintFormat class BadName { fun tooComplex( a: Int, b: Int, ): Int { var r = 0 if (a > 0) { if (b > 0) { r = if (a > b) 1 else 2 } else { r = 3 } } else { r = 4 } return r } } fun main() { println(BadName().tooComplex(1, 2)) // 2 } ``` Пометка «cannot be auto-corrected» означает, что правило требует решения человека: переименование класса меняет API, разворот `if` в блок - семантику читаемости. ## Настройка ktlint Через `.editorconfig` в корне проекта: ``` root = true [*.{kt,kts}] indent_size = 4 indent_style = space max_line_length = 120 insert_final_newline = true ij_kotlin_allow_trailing_comma = true ij_kotlin_allow_trailing_comma_on_call_site = true ktlint_standard_no-wildcard-imports = disabled ktlint_standard_function-naming = disabled ktlint_code_style = ktlint_official ``` | Параметр | Значение | |---|---| | `ktlint_code_style` | `ktlint_official`, `intellij_idea`, `android_studio` | | `max_line_length` | длина строки (`off` отключает) | | `ktlint_standard_ = disabled` | отключить конкретное правило | | `ij_kotlin_allow_trailing_comma` | разрешить висячую запятую | Важно, что `.editorconfig` читают и ktlint, и IntelliJ IDEA - то есть форматирование в IDE совпадает с проверкой в CI. Это главный аргумент за `.editorconfig` вместо настроек плагина. Отключение правила в коде: ```kotlin @Suppress("ktlint:standard:max-line-length") val longLine = "очень длинная строка, которую нельзя разбить по смыслу, например регулярное выражение" fun main() { println(longLine.length) // 85 } ``` ## detekt Около 200 правил, разбитых на наборы. Запуск на том же плохом файле: ``` $ ./gradlew detekt Bad.kt:3:7: The file name 'Bad' does not match the name of the single top-level declaration 'bad_name'. [MatchingDeclarationName] Bad.kt:1:1: The package declaration does not match the actual file location. [InvalidPackageDeclaration] Bad.kt:3:7: Class and Object names should match the pattern: [A-Z][a-zA-Z0-9]* [ClassNaming] Bad.kt:4:9: Function names should match the pattern: [a-z][a-zA-Z0-9]* [FunctionNaming] Bad.kt:6:72: This expression contains a magic number. [MagicNumber] Bad.kt:9:24: Private property `x` is unused. [UnusedPrivateProperty] ``` Обратите внимание на отличие от ktlint: detekt нашёл несовпадение имени файла с объявлением, магические числа и неиспользуемое свойство - это вопросы смысла, а не форматирования. | Набор правил | Что проверяет | |---|---| | `complexity` | длинные функции, высокая цикломатическая сложность, много параметров | | `coroutines` | неправильное использование `suspend`, `GlobalScope`, блокирующие вызовы | | `empty-blocks` | пустые `catch`, `if`, функции | | `exceptions` | `catch (e: Exception)`, проглоченные исключения, `throw` в `finally` | | `naming` | соответствие конвенциям имён | | `performance` | `forEach` вместо `for`, лишние аллокации, `spread`-оператор | | `potential-bugs` | `!!`, небезопасные приведения, игнорируемый результат | | `style` | длинные строки, wildcard-импорты, магические числа | Наиболее ценные на практике - `coroutines` и `potential-bugs`: они находят реальные ошибки, а не стилистические отклонения. ## Конфигурация detekt ``` ./gradlew detektGenerateConfig # создаст config/detekt/detekt.yml ``` ```yaml # config/detekt/detekt.yml build: maxIssues: 0 excludeCorrectable: false complexity: LongMethod: active: true threshold: 60 LongParameterList: active: true functionThreshold: 6 ignoreDefaultParameters: true CyclomaticComplexMethod: active: true threshold: 15 style: MagicNumber: active: true ignoreNumbers: ['-1', '0', '1', '2'] ignoreHashCodeFunction: true excludes: ['**/test/**'] MaxLineLength: maxLineLength: 120 WildcardImport: active: true potential-bugs: UnsafeCallOnNullableType: active: true coroutines: GlobalCoroutineUsage: active: true InjectDispatcher: active: true ``` ```kotlin // build.gradle.kts detekt { buildUponDefaultConfig = true // своя конфигурация дополняет дефолтную config.setFrom("$projectDir/config/detekt/detekt.yml") baseline = file("$projectDir/config/detekt/baseline.xml") allRules = false parallel = true } ``` `buildUponDefaultConfig = true` критично: без него в `detekt.yml` нужно перечислить **все** правила, и забытое правило окажется отключённым. ## Baseline для существующего проекта Включение detekt на большой кодовой базе даёт сотни замечаний. Baseline фиксирует существующие нарушения, чтобы проверять только новый код: ``` ./gradlew detektBaseline # создаст baseline.xml ``` ```xml MagicNumber:Bad.kt$6:72 UnusedPrivateProperty:Bad.kt$private val x ``` Новые нарушения ломают сборку, старые игнорируются. Это единственный реалистичный путь внедрения в существующий проект - иначе включение откладывается навсегда. ## Подавление правил ```kotlin @Suppress("MagicNumber") fun httpTimeout() = 30_000 class Legacy { @Suppress("TooGenericExceptionCaught", "SwallowedException") fun risky() { try { error("сбой") } catch (e: Exception) { // осознанно проглочено: вызывающий код не должен знать о сбое } } } fun main() { println(httpTimeout()) // 30000 Legacy().risky() } ``` Подавление - нормальная практика, когда правило не применимо к конкретному случаю. Плохая практика - подавлять правило целиком в `detekt.yml`, если оно нарушается в двух местах из тысячи. ## Type resolution detekt может работать с информацией о типах - тогда доступны правила, требующие знания classpath: ```kotlin // build.gradle.kts tasks.withType().configureEach { jvmTarget = "21" } tasks.register("detektWithTypes") { description = "detekt с разрешением типов" setSource(files("src/main/kotlin")) classpath.setFrom( configurations.getByName("detekt"), sourceSets["main"].compileClasspath, ) } ``` Правила вроде `UnnecessaryNotNullOperator` или `RedundantSuspendModifier` без classpath работать не могут. Цена - заметно медленнее. ## Интеграция в CI ```yaml # .github/workflows/check.yml name: check on: [push, pull_request] jobs: quality: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v4 with: distribution: temurin java-version: 21 - uses: gradle/actions/setup-gradle@v4 - run: ./gradlew ktlintCheck detekt --continue - uses: actions/upload-artifact@v4 if: always() with: name: reports path: | build/reports/detekt/ build/reports/ktlint/ ``` `--continue` важен: без него сборка остановится на первой проваленной проверке, и вы не увидите остальные замечания. ## Git-хук ```bash # .git/hooks/pre-commit #!/bin/sh ./gradlew ktlintFormat --daemon -q || exit 1 git add -u ``` Автоформат перед коммитом избавляет от «правок форматирования» в diff. `detekt` в хук обычно не ставят - он слишком медленный для каждого коммита. Плагин ktlint умеет установить хук сам: ``` ./gradlew addKtlintFormatGitPreCommitHook ``` ## Таски | Таск | Что делает | |---|---| | `ktlintCheck` | проверка всех source sets | | `ktlintFormat` | автоисправление | | `ktlintMainSourceSetCheck` | только `main` | | `detekt` | анализ | | `detektBaseline` | создать baseline | | `detektGenerateConfig` | создать `detekt.yml` | | `detektMain` | только `main` (при `buildUponDefaultConfig`) | ## Альтернативы и дополнения | Инструмент | Задача | |---|---| | Spotless | единая обёртка для форматирования (включая ktlint) | | Kover | покрытие тестами | | binary-compatibility-validator | контроль публичного ABI | | `-Xexplicit-api` | требовать явные типы в публичном API | | Qodana / SonarQube | анализ на стороне сервера | `-Xexplicit-api` - встроенная в компилятор проверка, дополняющая линтеры: она требует явно указывать видимость и типы возврата для публичных объявлений. Подробнее: [Компилятор K2](Компилятор%20K2.md) ## Разумный минимум 1. `.editorconfig` с `ktlint_official` - согласованный стиль в IDE и CI. 2. `ktlintFormat` в pre-commit хуке - форматирование не попадает в diff. 3. detekt с `buildUponDefaultConfig` и baseline - новый код проверяется, старый не блокирует. 4. `ktlintCheck detekt --continue` в CI с публикацией отчётов. 5. `-Xexplicit-api=strict` для библиотек. Этого достаточно; дальнейшая настройка правил - вопрос договорённостей команды, а не инструментов. > [!info] Отличия от Java > | Java | Kotlin | > |---|---| > | Checkstyle | ktlint | > | SpotBugs, PMD, ErrorProne | detekt | > | google-java-format, Spotless | ktlint (или Spotless + ktlint) | > | стиль - вопрос договорённости | официальный coding conventions | > | конфигурация XML | `.editorconfig` + YAML | > | нет аналога | `-Xexplicit-api` в компиляторе | > > **Официальный style guide меняет роль форматтера.** В Java конкурируют несколько стилей (Oracle, Google, AOSP), и Checkstyle приходится настраивать под выбранный. Kotlin имеет один официальный стиль от JetBrains, реализованный и в IDE, и в ktlint - настройка почти не нужна, и споров о форматировании не возникает. Это сознательная позиция: единый стиль важнее возможности выбрать свой. > > **`.editorconfig` как единый источник правды - важнее, чем кажется.** В Java настройки Checkstyle и настройки форматирования IDE - разные файлы, которые расходятся. Файл `.editorconfig` читают и IntelliJ, и ktlint, поэтому «форматирование в IDE» и «проверка в CI» - буквально одна конфигурация. Это устраняет классический конфликт, когда IDE переформатирует код так, что падает сборка. > > **detekt берёт на себя то, что в Java распределено между тремя инструментами.** SpotBugs анализирует байт-код, PMD - исходники, ErrorProne встроен в компиляцию. detekt - один инструмент на исходниках, с опциональным разрешением типов. Проще в настройке, но находит меньше: без анализа байт-кода часть проблем (например, утечки ресурсов по потоку данных) недоступна. > > **Правила для корутин - специфика, которой нет аналога.** Набор `coroutines` в detekt находит `GlobalScope`, блокирующие вызовы внутри `suspend`, захардкоженные диспетчеры. В Java параллельные ошибки (блокировка в пуле, утечка `ExecutorService`) ловятся хуже, потому что нет структуры, которую можно статически проверить. Структурированная конкурентность Kotlin даёт линтеру опору. Подробнее: [Структурированная конкурентность](../Корутины/Структурированная%20конкурентность.md) > > Сравнить: [javac](../../Программирование%20на%20Java/JDK/javac.md) > [!warning] Подводные камни > **`buildUponDefaultConfig = false` (по умолчанию) отключает все неперечисленные правила.** Свой `detekt.yml` без этого флага заменяет дефолтную конфигурацию целиком: сборка зелёная, проверок нет. Это самая частая ошибка настройки detekt. > > **detekt падает на слишком новом JDK.** Версия 1.23.8 на JDK 26 завершается с `GradleException: 26.0.1` - встроенный парсер не знает такой версии байт-кода. Запускайте таск на поддерживаемом JDK: `jvmToolchain` для компиляции и отдельный JDK для detekt, или обновите detekt. > > **ktlint и detekt могут конфликтовать по правилам.** Оба проверяют длину строки и имена, и при разных настройках код, исправленный одним, нарушает правило другого. Задавайте `max_line_length` в `.editorconfig`, а в detekt - `MaxLineLength.maxLineLength` с тем же значением. > > **`ktlintFormat` может изменить семантику в редких случаях.** Перестановка импортов и добавление висячих запятых безопасны, но правила вроде `if-else` → выражение меняют структуру кода. После массового автоформата запускайте тесты. > > **Baseline легко превращается в свалку.** Файл создаётся раз и начинает жить вечно: правила «починим позже» не выполняются. Перегенерируйте baseline периодически и следите, чтобы он сокращался. > > **Правила с type resolution требуют classpath и замедляют анализ.** Без `classpath.setFrom(...)` правила вроде `UnnecessaryNotNullOperator` молча не работают. С ним detekt становится в разы медленнее - обычно такой таск запускают только в CI. > > **`MagicNumber` шумит в тестах.** Тесты полны литералов, и правило даёт десятки замечаний. Добавьте `excludes: ['**/test/**']` в конфигурацию, иначе придётся подавлять в каждом файле. > > **Висячие запятые требуют согласованности.** `ij_kotlin_allow_trailing_comma = true` в `.editorconfig` и настройка IDE должны совпадать, иначе IDE удаляет запятую, а ktlint требует её вернуть - бесконечный цикл правок. > > **Версия плагина ktlint и версия самого ktlint - разные числа.** `org.jlleitschuh.gradle.ktlint` версии 12.x по умолчанию использует ktlint 1.x; при необходимости версия задаётся через `ktlint { version.set("1.5.0") }`. Несогласованность приводит к разным результатам локально и в CI. > > **Проверка в CI без `--continue` скрывает часть проблем.** Сборка падает на `ktlintCheck`, и `detekt` не выполняется - разработчик исправляет форматирование, пушит, и только тогда узнаёт о замечаниях detekt. Два цикла вместо одного. > > **Git-хук с detekt делает коммиты медленными.** Полный анализ среднего проекта - десятки секунд. В хук ставьте только `ktlintFormat`, detekt оставьте для CI. > > **`@Suppress` с неверным именем правила молча не работает.** Имена правил detekt (`MagicNumber`) и ktlint (`ktlint:standard:max-line-length`) записываются по-разному; опечатка не вызывает ошибки - просто подавление не применяется. Далее: [Тестирование](../Тестирование/Тестирование.md)