22 KiB
title, description, tags
| title | description | tags | |||
|---|---|---|---|---|---|
| Линтеры и формат | ktlint и detekt — автоформатирование, статический анализ, конфигурация и интеграция в CI |
|
Два инструмента с разными задачами: ktlint приводит код к официальному стилю и умеет исправлять сам, detekt ищет проблемы смысла - сложные функции, мёртвый код, подозрительные конструкции.
| ktlint | detekt | |
|---|---|---|
| проверяет | форматирование и имена | сложность, запахи кода |
| автоисправление | да, большинство правил | частично |
| настройка | .editorconfig, минимум правил |
detekt.yml, ~200 правил |
| нужен ли compile classpath | нет | нет (есть опциональный type resolution) |
| скорость | быстро | медленнее |
// 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. Настроек почти нет - это и есть идея: не спорить о стиле.
На намеренно плохом файле:
// неформатированный код
// 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 исправляет всё, что помечено как исправимое:
// после 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_<rule> = disabled |
отключить конкретное правило |
ij_kotlin_allow_trailing_comma |
разрешить висячую запятую |
Важно, что .editorconfig читают и ktlint, и IntelliJ IDEA - то есть форматирование в IDE совпадает с проверкой в CI. Это главный аргумент за .editorconfig вместо настроек плагина.
Отключение правила в коде:
@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
# 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
// 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 version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>MagicNumber:Bad.kt$6:72</ID>
<ID>UnusedPrivateProperty:Bad.kt$private val x</ID>
</CurrentIssues>
</SmellBaseline>
Новые нарушения ломают сборку, старые игнорируются. Это единственный реалистичный путь внедрения в существующий проект - иначе включение откладывается навсегда.
Подавление правил
@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:
// build.gradle.kts
tasks.withType<io.gitlab.arturbosch.detekt.Detekt>().configureEach {
jvmTarget = "21"
}
tasks.register<io.gitlab.arturbosch.detekt.Detekt>("detektWithTypes") {
description = "detekt с разрешением типов"
setSource(files("src/main/kotlin"))
classpath.setFrom(
configurations.getByName("detekt"),
sourceSets["main"].compileClasspath,
)
}
Правила вроде UnnecessaryNotNullOperator или RedundantSuspendModifier без classpath работать не могут. Цена - заметно медленнее.
Интеграция в CI
# .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-хук
# .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
Разумный минимум
.editorconfigсktlint_official- согласованный стиль в IDE и CI.ktlintFormatв pre-commit хуке - форматирование не попадает в diff.- detekt с
buildUponDefaultConfigи baseline - новый код проверяется, старый не блокирует. ktlintCheck detekt --continueв CI с публикацией отчётов.-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 даёт линтеру опору. Подробнее: Структурированная конкурентностьСравнить: javac
[!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) записываются по-разному; опечатка не вызывает ошибки - просто подавление не применяется.
Далее: Тестирование