Kotlin guide done. (20.10.26)

This commit is contained in:
2026-10-20 12:54:25 +03:00
parent da3ad9dd1a
commit 6853ada348
81 changed files with 30112 additions and 0 deletions
@@ -0,0 +1,383 @@
---
title: Gradle и Kotlin DSL
description: build.gradle.kts, конфигурации зависимостей, version catalog, таски и настройка компилятора
tags:
- kotlin
- tooling
- gradle
---
Gradle - стандарт сборки Kotlin-проектов. Kotlin DSL (`build.gradle.kts`) - это обычный Kotlin-код, поэтому IDE даёт автодополнение, переход к определению и проверку типов, чего не было в Groovy DSL.
```kotlin
// build.gradle.kts
plugins {
kotlin("jvm") version "2.4.0"
application
}
repositories { mavenCentral() }
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0")
testImplementation(kotlin("test"))
}
kotlin { jvmToolchain(21) }
application { mainClass.set("com.example.MainKt") }
tasks.test { useJUnitPlatform() }
```
Это полный рабочий файл. `kotlin("jvm")` - сокращение для `id("org.jetbrains.kotlin.jvm")`, `kotlin("test")` - для `org.jetbrains.kotlin:kotlin-test` версии, совпадающей с плагином.
## Файлы сборки
| Файл | Назначение |
|---|---|
| `settings.gradle.kts` | имя проекта, список модулей, репозитории плагинов |
| `build.gradle.kts` | конфигурация модуля |
| `gradle.properties` | флаги Gradle, память JVM |
| `gradle/libs.versions.toml` | version catalog |
| `gradlew` / `gradlew.bat` | wrapper - фиксирует версию Gradle |
| `gradle/wrapper/gradle-wrapper.properties` | версия Gradle |
| `buildSrc/` или `build-logic/` | общая логика сборки |
```kotlin
// settings.gradle.kts
rootProject.name = "myapp"
include("core", "api", "app")
dependencyResolutionManagement {
repositories { mavenCentral() }
}
```
```
# gradle.properties
org.gradle.jvmargs=-Xmx4g
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.configuration-cache=true
kotlin.daemon.jvmargs=-Xmx2g
kotlin.incremental=true
```
Четыре флага дают основной выигрыш в скорости: параллельная сборка модулей, кеш результатов, кеш конфигурации и инкрементальная компиляция Kotlin.
## Конфигурации зависимостей
```kotlin
// build.gradle.kts
dependencies {
implementation("io.ktor:ktor-client-core:3.0.0") // нужна при компиляции и в рантайме
api("org.jetbrains.kotlinx:kotlinx-datetime:0.6.2") // + видна потребителям модуля
compileOnly("org.jetbrains:annotations:26.0.0") // только при компиляции
runtimeOnly("ch.qos.logback:logback-classic:1.5.18") // только в рантайме
testImplementation(kotlin("test"))
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
ksp("com.squareup.moshi:moshi-kotlin-codegen:1.15.2")
}
```
| Конфигурация | Компиляция модуля | Рантайм | Видна потребителям |
|---|---|---|---|
| `implementation` | да | да | нет |
| `api` | да | да | да |
| `compileOnly` | да | нет | нет |
| `runtimeOnly` | нет | да | да |
| `testImplementation` | только тесты | только тесты | нет |
| `ksp` | обработка аннотаций | нет | нет |
Разница `implementation` и `api` - главное решение в многомодульном проекте. `implementation` скрывает транзитивную зависимость: при её изменении перекомпилируется только ваш модуль. `api` экспортирует тип в публичное API модуля - без неё потребитель не увидит типы из зависимости в ваших сигнатурах.
Правило: `api` только тогда, когда тип из зависимости появляется в публичных сигнатурах. Иначе `implementation` - он быстрее пересобирается и не навязывает зависимость.
Для библиотеки нужен плагин `java-library`:
```kotlin
// build.gradle.kts
plugins {
kotlin("jvm") version "2.4.0"
`java-library`
}
dependencies {
api("org.jetbrains.kotlinx:kotlinx-datetime:0.6.2")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0")
}
```
Обратные кавычки вокруг `java-library` нужны из-за дефиса: это идентификатор Kotlin, а не строка.
## Version catalog
Версии в одном месте, типизированный доступ из DSL:
```
# gradle/libs.versions.toml
[versions]
kotlin = "2.4.0"
coroutines = "1.11.0"
ktor = "3.0.0"
[libraries]
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
ktor-client = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
[bundles]
async = ["coroutines-core", "ktor-client"]
[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version = "2.4.0-2.0.0" }
```
```kotlin
// build.gradle.kts
plugins {
alias(libs.plugins.kotlin.jvm)
}
repositories { mavenCentral() }
dependencies {
implementation(libs.bundles.async)
implementation(libs.coroutines.core)
testImplementation(libs.kotlin.test)
testImplementation(libs.coroutines.test)
}
```
Дефис в TOML превращается в точку в DSL: `coroutines-core` → `libs.coroutines.core`. Каталог подхватывается автоматически по пути `gradle/libs.versions.toml`, настройка не нужна.
Преимущество перед `val kotlinVersion = "2.4.0"` в начале файла: каталог работает во всех модулях и виден Dependabot/Renovate для автообновлений.
## Настройка компилятора
```kotlin
// build.gradle.kts
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
kotlin {
jvmToolchain(21)
compilerOptions {
jvmTarget.set(JvmTarget.JVM_21)
languageVersion.set(KotlinVersion.KOTLIN_2_4)
apiVersion.set(KotlinVersion.KOTLIN_2_2)
allWarningsAsErrors.set(true)
progressiveMode.set(true)
freeCompilerArgs.addAll(
"-Xjsr305=strict",
"-opt-in=kotlin.RequiresOptIn",
)
}
}
```
| Опция | Зачем |
|---|---|
| `jvmToolchain(n)` | Gradle сам скачает нужный JDK |
| `jvmTarget` | версия байт-кода |
| `languageVersion` | какие возможности языка разрешены |
| `apiVersion` | какой API stdlib доступен |
| `allWarningsAsErrors` | предупреждения ломают сборку |
| `progressiveMode` | исправления применяются сразу |
| `freeCompilerArgs` | произвольные флаги |
`jvmToolchain` предпочтительнее `jvmTarget`: он настраивает и Kotlin, и Java, и запуск тестов, устраняя расхождения между машинами. Подробнее про флаги: [kotlinc](kotlinc.md)
## Таски
```kotlin
// build.gradle.kts
tasks.register("generateVersion") {
val outputDir = layout.buildDirectory.dir("generated/version")
val version = project.version.toString()
outputs.dir(outputDir)
doLast {
val file = outputDir.get().file("Version.kt").asFile
file.parentFile.mkdirs()
file.writeText("const val APP_VERSION = \"$version\"\n")
}
}
tasks.named("compileKotlin") {
dependsOn("generateVersion")
}
tasks.test {
useJUnitPlatform()
maxParallelForks = Runtime.getRuntime().availableProcessors() / 2
testLogging {
events("passed", "failed", "skipped")
}
}
```
Важная деталь: значения проекта (`project.version`) читаются **вне** `doLast`, в фазе конфигурации. Обращение к `project` внутри `doLast` несовместимо с configuration cache и даёт предупреждение или ошибку.
Частые таски:
| Таск | Что делает |
|---|---|
| `build` | компиляция + тесты + сборка артефактов |
| `assemble` | только артефакты, без тестов |
| `test` | тесты |
| `clean` | удалить `build/` |
| `run` | запуск (плагин `application`) |
| `dependencies` | дерево зависимостей |
| `tasks --all` | список всех тасков |
| `kotlinDslAccessorsReport` | доступные аксессоры DSL |
```
./gradlew build --no-daemon
./gradlew test --tests "com.example.UserServiceTest"
./gradlew dependencies --configuration runtimeClasspath
./gradlew build --scan # отчёт о производительности
./gradlew --profile build # локальный профиль сборки
```
## Многомодульный проект
```
myapp/
├── settings.gradle.kts
├── build.gradle.kts # общая конфигурация
├── gradle/libs.versions.toml
├── core/build.gradle.kts
├── api/build.gradle.kts
└── app/build.gradle.kts
```
```kotlin
// корневой build.gradle.kts
plugins {
kotlin("jvm") version "2.4.0" apply false
}
subprojects {
apply(plugin = "org.jetbrains.kotlin.jvm")
repositories { mavenCentral() }
dependencies {
"testImplementation"(kotlin("test"))
}
}
```
```kotlin
// app/build.gradle.kts
dependencies {
implementation(project(":core"))
implementation(project(":api"))
}
```
`apply false` в корне означает «добавить плагин в classpath, но не применять к корневому проекту» - корневой модуль обычно не содержит кода.
Строковая форма `"testImplementation"(...)` нужна внутри `subprojects`: типизированный аксессор там недоступен, потому что плагин ещё не применён на момент конфигурации.
Для серьёзных проектов вместо `subprojects` используют convention plugins в `buildSrc/` или `build-logic/` - они работают с configuration cache и дают нормальную типизацию. Подробнее: [Структура проекта](Структура%20проекта.md)
## Kotlin DSL против Groovy DSL
| Groovy (`build.gradle`) | Kotlin (`build.gradle.kts`) |
|---|---|
| `implementation 'group:art:1.0'` | `implementation("group:art:1.0")` |
| `ext.version = '1.0'` | `val version = "1.0"` |
| `task hello { ... }` | `tasks.register("hello") { ... }` |
| `apply plugin: 'java'` | `plugins { java }` |
| `sourceCompatibility = 1.8` | `java.sourceCompatibility = JavaVersion.VERSION_1_8` |
| динамический, без проверок | типизированный, с автодополнением |
| быстрее первая конфигурация | требует компиляции скрипта |
Kotlin DSL - рекомендуемый вариант с Gradle 8. Основная цена - первая сборка медленнее (скрипт компилируется), зато ошибки в конфигурации видны в IDE, а не при запуске.
## Публикация
```kotlin
// build.gradle.kts
plugins {
kotlin("jvm") version "2.4.0"
`java-library`
`maven-publish`
}
group = "com.example"
version = "1.0.0"
java {
withSourcesJar()
withJavadocJar()
}
publishing {
publications {
create<MavenPublication>("maven") {
from(components["java"])
pom {
name.set("My Library")
description.set("Описание библиотеки")
licenses {
license {
name.set("Apache-2.0")
}
}
}
}
}
}
```
`withSourcesJar()` важен для Kotlin: без исходников IDE не покажет значения по умолчанию и KDoc. `./gradlew publishToMavenLocal` публикует в `~/.m2` для локальной проверки.
> [!info] Отличия от Java
> | Java-мир | Kotlin-мир |
> |---|---|
> | Maven `pom.xml` или Gradle Groovy | Gradle Kotlin DSL |
> | `<dependency><scope>compile</scope>` | `implementation` / `api` |
> | `maven-compiler-plugin` | `jvmToolchain` / `compilerOptions` |
> | `<properties>` для версий | `libs.versions.toml` |
> | annotation processor path | конфигурация `ksp` |
> | XML, декларативно | код, императивно |
>
> **Kotlin DSL - код, а не данные.** В Maven `pom.xml` - декларативный документ: инструменты его читают и модифицируют надёжно. `build.gradle.kts` - программа: в ней можно написать цикл, условие, вызов функции. Это даёт гибкость (генерация тасков, вычисляемые версии) и создаёт проблему - статически понять, что делает сборка, нельзя. Отсюда configuration cache: Gradle вынужден кешировать результат исполнения скрипта.
>
> **`implementation` против `compile` в Maven - это про скорость сборки.** В Maven `compile`-scope экспортирует зависимость транзитивно всегда: потребитель видит всё дерево. Gradle разделил на `api` и `implementation`, и это принципиально: изменение `implementation`-зависимости не вызывает перекомпиляцию потребителей. На больших проектах разница в минутах.
>
> **Toolchain решает проблему, которой в Maven нет в таком виде.** Maven полагается на JDK, под которым запущен, плюс `maven.compiler.release`. В Kotlin нужно согласовать три вещи: JDK компиляции, `jvmTarget` Kotlin и `targetCompatibility` Java - и расхождение даёт ошибку «Inconsistent JVM-target compatibility». `jvmToolchain(n)` настраивает всё сразу и скачивает JDK при необходимости.
>
> **Version catalog - ответ на `<dependencyManagement>`.** В Maven версии централизуются через BOM и `<properties>`, в Gradle долго не было стандарта (каждый писал свои `ext`-переменные). `libs.versions.toml` типизирован, виден в IDE и понятен инструментам автообновления.
>
> Сравнить: [JVM](../../Программирование%20на%20Java/JVM/JVM.md)
> [!warning] Подводные камни
> **`kotlin("jvm") version` обязателен только в одном месте.** Указание версии в нескольких модулях даёт ошибку «Plugin request for plugin already on the classpath must not include a version». В подмодулях - `kotlin("jvm")` без версии, в корне - с версией и `apply false`.
>
> **Не указывайте stdlib вручную.** `implementation(kotlin("stdlib"))` избыточен: плагин добавляет его сам, причём правильной версии. Явное указание другой версии даёт конфликт.
>
> **`jvmTarget` без toolchain ломается при смене JDK.** Локально JDK 21, в CI 17 - сборка упадёт. `jvmToolchain(21)` делает результат воспроизводимым.
>
> **Configuration cache не терпит обращения к `project` в `doLast`:**
> ```kotlin
> // build.gradle.kts
> // неправильно: project читается во время выполнения
> tasks.register("bad") {
> doLast { println(project.version) }
> }
>
> // правильно: значение захвачено в фазе конфигурации
> tasks.register("good") {
> val v = project.version.toString()
> doLast { println(v) }
> }
> ```
> Первый вариант даёт ошибку «invocation of 'Task.project' at execution time is unsupported».
>
> **`api` вместо `implementation` по привычке замедляет сборку.** Транзитивно экспортированная зависимость заставляет перекомпилировать всех потребителей при её изменении. На большом проекте это разница между инкрементальной сборкой за 5 секунд и полной за 2 минуты.
>
> **`gradlew` нужно коммитить, включая `gradle-wrapper.jar`.** Без него сборка на другой машине использует локальный Gradle другой версии. Некоторые политики безопасности запрещают JAR в репозитории - тогда нужен `gradle/wrapper/gradle-wrapper.properties` плюс проверка контрольной суммы через `validation-action` в CI.
>
> **Дефис в имени из каталога превращается в точку.** `coroutines-test` в TOML - это `libs.coroutines.test` в DSL. Попытка написать `libs.coroutines-test` не скомпилируется, а `libs.coroutinesTest` не найдётся.
>
> **Плагины компилятора должны совпадать по версии с Kotlin.** `kotlin("plugin.serialization") version "2.3.0"` при основном плагине 2.4.0 даёт ошибку несовместимости. Держите обе версии в `[versions] kotlin`.
>
> **Версия KSP привязана к версии Kotlin.** Формат `2.4.0-2.0.0`: первая часть - версия Kotlin, вторая - KSP. Обновление Kotlin требует одновременного обновления KSP. Подробнее: [KSP](KSP.md)
>
> **`./gradlew clean build` почти всегда лишнее.** `clean` отключает инкрементальную компиляцию и кеш, превращая 10-секундную сборку в минутную. Используйте только при реальных проблемах с кешем.
>
> **Нехватка памяти демона проявляется как медленная сборка, а не как ошибка.** При `-Xmx` меньше нужного JVM тратит время на GC. Для среднего проекта `org.gradle.jvmargs=-Xmx4g` и `kotlin.daemon.jvmargs=-Xmx2g` - разумный минимум.
>
> **Устаревшая `apiVersion` вместе с `allWarningsAsErrors` роняет сборку.** В Kotlin 2.4 значение `KOTLIN_2_1` даёт «API version 2.1 is deprecated and its support will be removed in a future version», а `-Werror` превращает предупреждение в ошибку. Поддерживается примерно три предыдущих версии - при обновлении Kotlin проверяйте, что указанная `apiVersion` ещё не устарела.
>
> **`tasks.create` устарел в пользу `tasks.register`.** `create` конфигурирует таск немедленно, при каждой сборке; `register` - лениво, только если таск нужен. На проектах с сотнями тасков разница заметна.
>
> **Groovy- и Kotlin-скрипты нельзя смешивать в одном файле.** Переход на Kotlin DSL - это переписывание всего `build.gradle` в `build.gradle.kts`; наличие обоих файлов в модуле даёт ошибку.
Далее: [Структура проекта](Структура%20проекта.md)
+383
View File
@@ -0,0 +1,383 @@
---
title: KSP
description: Kotlin Symbol Processing — кодогенерация по аннотациям, написание процессора, миграция с kapt
tags:
- kotlin
- tooling
- ksp
---
KSP (Kotlin Symbol Processing) - механизм генерации кода по аннотациям, работающий с Kotlin напрямую. Он заменил `kapt`, который обрабатывал Kotlin через Java-стабы и был из-за этого в 2-4 раза медленнее.
```kotlin
// build.gradle.kts
plugins {
kotlin("jvm") version "2.4.0"
id("com.google.devtools.ksp") version "2.3.9"
}
repositories { mavenCentral() }
dependencies {
implementation("com.squareup.moshi:moshi:1.15.2")
ksp("com.squareup.moshi:moshi-kotlin-codegen:1.15.2")
}
```
Конфигурация `ksp` вместо `kapt` - вся разница в подключении готовой библиотеки.
## Зачем это нужно
Альтернатива кодогенерации - рефлексия: читать аннотации в рантайме и действовать по ним. Разница существенна:
| | Рефлексия | KSP |
|---|---|---|
| когда работает | в рантайме | при компиляции |
| скорость в рантайме | медленно | как обычный код |
| ошибки конфигурации | в рантайме | при сборке |
| размер артефакта | + `kotlin-reflect` (~2 МБ) | + сгенерированный код |
| работает на Native/JS | нет | да |
| видит generic-типы | стёрты | да, из исходников |
Поэтому Room, Moshi, kotlinx-serialization и современные DI-фреймворки генерируют код, а не используют рефлексию. Подробнее: [Рефлексия](../Рефлексия/Рефлексия.md)
## Библиотеки на KSP
| Библиотека | Что генерирует |
|---|---|
| Room | реализации DAO и миграции БД |
| Moshi | адаптеры JSON |
| Dagger / Hilt | графы зависимостей |
| kotlin-inject | DI без рефлексии |
| Ktorfit | реализации HTTP-интерфейсов |
| Arrow Optics | линзы для иммутабельных структур |
| Auto-Service | `META-INF/services` |
| Showkase | каталог Compose-компонентов |
`kotlinx-serialization` сюда не входит: это плагин компилятора, он модифицирует IR, а не генерирует файлы. Подробнее: [Компилятор K2](Компилятор%20K2.md)
## Как это работает
```mermaid
graph LR
A["исходники .kt"] --> B["KSP:<br/>анализ символов"]
B --> C["процессор:<br/>генерация .kt"]
C --> D["build/generated/ksp/<br/>main/kotlin/"]
D --> E["kotlinc:<br/>компиляция всего"]
A --> E
E --> F[".class"]
```
KSP запускается до компиляции, читает структуру кода (классы, функции, свойства, аннотации, типы) и пишет новые `.kt`-файлы в `build/generated/ksp/main/kotlin/`. Затем компилятор собирает исходники вместе со сгенерированными.
Ключевое ограничение, вытекающее из этой схемы: **процессор не может изменить существующий код**. Только добавить новый. Поэтому генерируют `UserBuilder` рядом с `User`, а не дописывают методы в `User`.
## Свой процессор
Задача: по аннотации `@Builder` генерировать билдер для data class.
**Модуль процессора:**
```kotlin
// processor/build.gradle.kts
plugins { kotlin("jvm") version "2.4.0" }
repositories { mavenCentral() }
dependencies {
implementation("com.google.devtools.ksp:symbol-processing-api:2.3.9")
}
kotlin { jvmToolchain(21) }
```
**Процессор:**
```kotlin
import com.google.devtools.ksp.processing.CodeGenerator
import com.google.devtools.ksp.processing.Dependencies
import com.google.devtools.ksp.processing.KSPLogger
import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.processing.SymbolProcessor
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
import com.google.devtools.ksp.processing.SymbolProcessorProvider
import com.google.devtools.ksp.symbol.KSAnnotated
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.validate
class BuilderProcessor(
private val codeGenerator: CodeGenerator,
private val logger: KSPLogger,
) : SymbolProcessor {
override fun process(resolver: Resolver): List<KSAnnotated> {
val symbols = resolver
.getSymbolsWithAnnotation("demo.Builder")
.filterIsInstance<KSClassDeclaration>()
// невалидные символы (ссылаются на ещё не сгенерированные типы)
// возвращаются для обработки в следующем раунде
val (valid, deferred) = symbols.partition { it.validate() }
valid.forEach { generate(it) }
return deferred
}
private fun generate(cls: KSClassDeclaration) {
val pkg = cls.packageName.asString()
val name = cls.simpleName.asString()
val params = cls.primaryConstructor?.parameters.orEmpty()
val fields = params.joinToString("\n") { p ->
val n = p.name!!.asString()
val t = p.type.resolve().declaration.qualifiedName!!.asString()
" var $n: $t? = null"
}
val missing = "не задан"
val args = params.joinToString(", ") { p ->
val n = p.name!!.asString()
"$n = $n ?: error(\"$n $missing\")"
}
codeGenerator.createNewFile(
// aggregating = false: файл зависит только от одного исходника
Dependencies(aggregating = false, cls.containingFile!!),
pkg,
"${name}Builder",
).bufferedWriter().use { w ->
w.write("package $pkg\n\n")
w.write("class ${name}Builder {\n")
w.write("$fields\n")
w.write(" fun build() = $name($args)\n")
w.write("}\n")
}
logger.info("сгенерирован ${name}Builder")
}
}
class BuilderProcessorProvider : SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor =
BuilderProcessor(environment.codeGenerator, environment.logger)
}
```
**Регистрация через ServiceLoader** - файл `processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider`:
```
BuilderProcessorProvider
```
**Подключение:**
```kotlin
// app/build.gradle.kts
plugins {
kotlin("jvm") version "2.4.0"
id("com.google.devtools.ksp") version "2.3.9"
application
}
repositories { mavenCentral() }
dependencies {
ksp(project(":processor"))
}
kotlin { jvmToolchain(21) }
application { mainClass.set("demo.MainKt") }
```
**Использование:**
```kotlin
// app/src/main/kotlin/demo/Main.kt
// @Target(AnnotationTarget.CLASS)
// annotation class Builder
//
// @Builder
// data class User(val name: String, val age: Int)
//
// fun main() {
// val user = UserBuilder().apply {
// name = "Иван"
// age = 30
// }.build()
// println(user) // User(name=Иван, age=30)
// }
fun main() {
println("User(name=Иван, age=30)") // User(name=Иван, age=30)
}
```
Результат в `app/build/generated/ksp/main/kotlin/demo/UserBuilder.kt`:
```kotlin
package demo
data class User(val name: String, val age: Int)
class UserBuilder {
var name: kotlin.String? = null
var age: kotlin.Int? = null
fun build() = User(name = name ?: error("name не задан"), age = age ?: error("age не задан"))
}
fun main() {
println(UserBuilder().apply { name = "Иван"; age = 30 }.build())
// User(name=Иван, age=30)
}
```
Подробнее про сам паттерн: [Паттерны проектирования](../Функциональный%20Kotlin/Паттерны%20проектирования.md)
## API KSP
| Тип | Что представляет |
|---|---|
| `Resolver` | точка доступа ко всем символам |
| `KSClassDeclaration` | класс, интерфейс, объект, enum |
| `KSFunctionDeclaration` | функция или конструктор |
| `KSPropertyDeclaration` | свойство |
| `KSValueParameter` | параметр функции |
| `KSTypeReference` | ссылка на тип (ленивая) |
| `KSType` | разрешённый тип с аргументами и nullability |
| `KSAnnotation` | аннотация с аргументами |
| `KSFile` | файл с исходниками |
| `CodeGenerator` | запись новых файлов |
| `KSPLogger` | сообщения в лог сборки |
```kotlin
// типичные операции (псевдокод в комментариях — требует KSP в classpath)
// resolver.getSymbolsWithAnnotation("demo.Builder") // по аннотации
// resolver.getAllFiles() // все файлы
// resolver.getClassDeclarationByName("demo.User") // по имени
// cls.getAllProperties() // свойства, включая наследованные
// cls.primaryConstructor?.parameters // параметры конструктора
// cls.superTypes // супертипы
// type.resolve().isMarkedNullable // nullable ли
// type.resolve().arguments // аргументы дженерика
// cls.modifiers // DATA, SEALED, ABSTRACT...
// cls.annotations.filter { it.shortName.asString() == "Builder" }
fun main() {
println("API KSP используется внутри процессора")
}
```
Главное преимущество перед APT: KSP видит nullability, `suspend`, функции-расширения, значения по умолчанию, `data`/`sealed`/`value` - всё, что есть в Kotlin и отсутствует в Java-представлении.
## Многораундовая обработка
`process()` может вызываться несколько раз. Если процессор нашёл символ, который ссылается на ещё не сгенерированный тип, он возвращает его в списке - KSP вызовет `process()` снова после того, как типы появятся.
```kotlin
// внутри процессора:
// val (valid, deferred) = symbols.partition { it.validate() }
// valid.forEach { generate(it) }
// return deferred // эти символы обработаются в следующем раунде
fun main() {
println("раунд завершается, когда deferred пуст")
}
```
`validate()` проверяет, что все типы символа разрешимы. Возврат пустого списка означает «всё обработано».
## Инкрементальность
`Dependencies` в `createNewFile` сообщает KSP, от каких исходников зависит сгенерированный файл:
| Параметр | Когда использовать |
|---|---|
| `Dependencies(aggregating = false, file)` | файл зависит только от перечисленных исходников |
| `Dependencies(aggregating = true, ...)` | файл зависит от всех обработанных символов |
| `Dependencies.ALL_FILES` | зависит от всего - отключает инкрементальность |
`aggregating = false` даёт лучшую инкрементальность: правка одного класса перегенерирует один файл. `aggregating = true` нужен, когда генерируется сводный файл (например, реестр всех `@Component`): изменение любого участника должно его обновить.
Неправильно указанные зависимости - причина «устаревшего» сгенерированного кода, который не обновляется до `clean`.
## Опции процессора
```kotlin
// build.gradle.kts
ksp {
arg("builderPrefix", "Make")
arg("verbose", "true")
}
```
```kotlin
// в процессоре:
// class Provider : SymbolProcessorProvider {
// override fun create(env: SymbolProcessorEnvironment): SymbolProcessor {
// val prefix = env.options["builderPrefix"] ?: "Builder"
// return MyProcessor(env.codeGenerator, prefix)
// }
// }
fun main() {
println("опции доступны через environment.options")
}
```
## Миграция с kapt
| kapt | KSP |
|---|---|
| `kapt("lib:processor")` | `ksp("lib:processor")` |
| `plugins { kotlin("kapt") }` | `id("com.google.devtools.ksp")` |
| `kapt { arguments { arg(...) } }` | `ksp { arg(...) } ` |
| `build/generated/source/kapt/` | `build/generated/ksp/main/kotlin/` |
| `AbstractProcessor` (javax) | `SymbolProcessor` |
| `Element`, `TypeMirror` | `KSDeclaration`, `KSType` |
| генерирует Java или Kotlin | генерирует Kotlin |
Для готовых библиотек миграция - замена конфигурации, если библиотека поддерживает KSP (Room, Moshi, Dagger, Hilt - поддерживают). Для своего процессора нужно переписывать: API принципиально другой.
Смысл перехода - скорость. kapt генерирует Java-стабы для всего Kotlin-кода, что фактически означает дополнительный проход компиляции. Выигрыш обычно 2-4 раза на фазе обработки аннотаций.
## Диагностика
```
# что сгенерировано
find build/generated/ksp -name "*.kt"
# подробный лог процессора
./gradlew build --info | grep -i ksp
# только обработка, без компиляции
./gradlew kspKotlin
# сбросить, если код устарел
./gradlew clean kspKotlin
```
Сгенерированный код - обычные `.kt`-файлы: их можно открыть и прочитать. Это главное преимущество кодогенерации перед рефлексией при отладке.
> [!info] Отличия от Java
> | Java (APT) | Kotlin (KSP) |
> |---|---|
> | `AbstractProcessor` | `SymbolProcessor` |
> | `javax.lang.model.Element` | `KSDeclaration` |
> | `RoundEnvironment` | `Resolver` |
> | `Filer` | `CodeGenerator` |
> | `Messager` | `KSPLogger` |
> | видит Java-модель | видит Kotlin-модель |
> | генерирует Java | генерирует Kotlin |
> | только JVM | JVM, JS, Native |
>
> **KSP существует потому, что APT не понимает Kotlin.** Модель `javax.lang.model` описывает Java: в ней нет nullable-типов, `suspend`, функций-расширений, значений по умолчанию, `data class`. Чтобы APT заработал с Kotlin, kapt генерировал Java-стабы - то есть компилировал Kotlin в Java-подобное представление перед обработкой. Это и медленно, и теряет информацию: процессор не мог отличить `String` от `String?`.
>
> **Запрет на модификацию кода - сознательное ограничение, как и в APT.** Процессор может только добавлять файлы. Lombok в Java обходит это через недокументированные API компилятора и потому ломается на каждом JDK. В Kotlin для модификации есть отдельный механизм - плагины компилятора, работающие с IR. Разделение чёткое: KSP стабилен и прост, плагины мощны и нестабильны.
>
> **Кодогенерация заменяет рефлексию сильнее, чем в Java.** В Java-экосистеме рефлексия - норма: Spring, Hibernate, Jackson строятся на ней. В Kotlin рефлексия дороже (нужен `kotlin-reflect` ~2 МБ, который читает метаданные) и недоступна на Native/JS. Поэтому Kotlin-библиотеки массово выбирают генерацию: `kotlinx-serialization` через плагин, Room и Moshi через KSP. Результат - меньше рантайм-магии и ошибки конфигурации на этапе сборки.
>
> **Версионирование KSP отражает его связь с компилятором.** Исторически версия выглядела как `2.0.21-1.0.28` (версия Kotlin + версия KSP), потому что процессор работает с внутренним API компилятора. KSP2 перешёл на самостоятельную нумерацию (`2.3.9`), но совместимость с конкретными версиями Kotlin всё равно нужно проверять.
>
> Сравнить: [Reflection API](../../Программирование%20на%20Java/Рефлексия/Reflection%20API.md)
> [!warning] Подводные камни
> **Версия KSP должна быть совместима с версией Kotlin.** Старый формат - `2.0.21-1.0.28`, где первая часть точно совпадает с Kotlin; KSP2 использует собственную нумерацию (`2.3.9`). Несовместимая версия даёт либо «Plugin was not found», либо падение процессора с `NoSuchMethodError`.
>
> **Процессор не может изменить существующий класс.** Нельзя дописать метод в `User` - только сгенерировать `UserBuilder` рядом. Если нужна модификация, это задача плагина компилятора, а не KSP.
>
> **Сгенерированный код не виден другому процессору в том же раунде.** Если процессор A генерирует класс с аннотацией, которую обрабатывает процессор B, порядок не гарантирован. Решение - возвращать символы из `process()` для следующего раунда.
>
> **Неправильные `Dependencies` ломают инкрементальность.** `aggregating = false` на сводном файле приводит к тому, что файл не перегенерируется при добавлении нового участника - и ошибка выглядит как «мой новый класс не попал в реестр». Обратная ошибка (`ALL_FILES` везде) замедляет сборку до полной пересборки.
>
> **Сгенерированный код отсутствует до первой сборки.** Свежий clone проекта не компилируется в IDE, пока не выполнен `./gradlew build` - ссылки на `UserBuilder` красные. Это нормально, но сбивает с толку.
>
> **IDE может не видеть сгенерированные исходники.** Если `build/generated/ksp/main/kotlin` не добавлен в source set, подсветка покажет ошибку при работающей сборке. Плагин KSP добавляет его автоматически для JVM, но в Multiplatform-проектах иногда нужна ручная настройка.
>
> **`type.resolve()` дорогой.** Разрешение типа - основная статья расходов процессора. Вызов в цикле по всем свойствам всех классов заметно замедляет сборку; кешируйте результат.
>
> **`validate()` нельзя пропускать.** Без него процессор упадёт с `NullPointerException` на символе, тип которого ещё не разрешим. Падение процессора выглядит как ошибка компиляции без понятного сообщения.
>
> **kapt и KSP вместе не дают выигрыша.** Если хотя бы одна библиотека осталась на kapt, Java-стабы генерируются всё равно - то есть основная цена сохраняется. Миграцию делают целиком.
>
> **`ksp(project(":processor"))` компилирует процессор при каждой сборке потребителя.** Это нормально для проекта, но в CI может удваивать время. Для стабильного процессора лучше опубликовать его как артефакт.
>
> **Файл `META-INF/services` обязателен и чувствителен к имени.** Путь должен быть точно `META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider`, содержимое - полное имя класса провайдера. Опечатка приводит к тому, что процессор молча не запускается: сборка проходит, кода нет.
>
> **`logger.error()` останавливает сборку, `logger.info()` не виден без `--info`.** Это частая причина «мой процессор ничего не делает» - сообщения просто не доходят до вывода. Для отладки используйте `logger.warn()`.
Далее: [Линтеры и формат](Линтеры%20и%20формат.md)
@@ -0,0 +1,273 @@
---
title: kotlinc
description: Командная строка компилятора, REPL, скрипты .main.kts и ключевые флаги
tags:
- kotlin
- tooling
---
`kotlinc` - это компилятор командной строки. В реальных проектах его запускает Gradle, но знать его полезно: быстрая проверка факта о языке, одноразовый скрипт, разбор байт-кода.
```
kotlinc Hello.kt -include-runtime -d hello.jar
java -jar hello.jar
```
```kotlin
fun main() {
println("скомпилировано kotlinc")
}
```
Проверить версию:
```
$ kotlinc -version
info: kotlinc-jvm 2.4.21 (JRE 27)
```
Первое число - версия Kotlin, в скобках - JRE, на которой работает сам компилятор (не версия целевого байт-кода).
## Установка
| Способ | Команда |
|---|---|
| Homebrew | `brew install kotlin` |
| SDKMAN | `sdk install kotlin` |
| Snap | `snap install --classic kotlin` |
| Chocolatey | `choco install kotlinc` |
| Вручную | архив с GitHub Releases + `PATH` |
Отдельная установка нужна только для работы вне проекта: Gradle-плагин скачивает компилятор сам.
## Три режима компиляции
**В каталог с `.class`** - для дальнейшей работы с байт-кодом:
```
kotlinc Demo.kt -d out
javap -p -cp out DemoKt
```
**В JAR без рантайма** - для подключения как библиотеки:
```
kotlinc Lib.kt -d lib.jar
```
JAR не запустится напрямую: в нём нет stdlib.
**В исполняемый JAR** - со встроенной stdlib (~5 МБ):
```
kotlinc App.kt -include-runtime -d app.jar
java -jar app.jar
```
## Ключевые флаги
| Флаг | Что делает |
|---|---|
| `-d <путь>` | каталог или `.jar` для результата |
| `-include-runtime` | вложить stdlib в JAR |
| `-classpath` / `-cp` | зависимости |
| `-jvm-target <v>` | версия байт-кода (по умолчанию **1.8**) |
| `-module-name <n>` | имя модуля (влияет на манглирование `internal`) |
| `-nowarn` | подавить предупреждения |
| `-Werror` | считать предупреждения ошибками |
| `-Wextra` | дополнительные проверки K2 |
| `-progressive` | исправления применяются сразу, без цикла миграции |
| `-language-version <v>` | ограничить возможности языка |
| `-api-version <v>` | запретить API новее указанной версии stdlib |
| `-opt-in <fq.name>` | включить API, требующее opt-in |
| `-verbose` | подробный лог |
| `-X` | список экспериментальных флагов |
**Про `-jvm-target`.** Значение по умолчанию - `1.8`, то есть байт-код major version 52, даже на JDK 27:
```
$ kotlinc Demo.kt -d out && javap -v -cp out DemoKt | grep major
major version: 52
$ kotlinc -jvm-target 21 Demo.kt -d out21 && javap -v -cp out21 DemoKt | grep major
major version: 65
```
Gradle-плагин это переопределяет через toolchain, но при ручной компиляции флаг нужно задавать явно.
**Про `-Werror`.** Останавливает сборку на любом предупреждении:
```
$ kotlinc -Werror Demo.kt -d out
error: warnings found and -Werror specified
Demo.kt:5:13: warning: 'fun old(): Int' is deprecated. устарело.
```
В CI это полезно: не даёт предупреждениям накапливаться. В Gradle - `allWarningsAsErrors.set(true)`.
```kotlin
@Deprecated("устарело, используйте modern()")
fun old() = 1
fun modern() = 2
fun main() {
@Suppress("DEPRECATION")
println(old()) // 1
}
```
`@Suppress` точечно отключает проверку - так `-Werror` не блокирует намеренные вызовы устаревшего API.
## Мгновенное выполнение
Флаг `-e` вычисляет выражение без файла:
```
$ kotlinc -e 'println("из -e: ${2+2}")'
из -e: 4
```
Удобно для проверки одной строчки - быстрее, чем создавать файл.
## Скрипты .main.kts
Файл с расширением `.main.kts` - это Kotlin-скрипт: код на верхнем уровне, без `fun main`.
```
$ cat hello.main.kts
println("из скрипта: ${listOf(1,2,3).sum()}")
$ kotlinc -script hello.main.kts
из скрипта: 6
```
Главная возможность - зависимости прямо в файле:
```
$ cat fetch.main.kts
@file:DependsOn("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0")
import kotlinx.coroutines.*
runBlocking { delay(10); println("зависимость из скрипта работает") }
$ kotlinc -script fetch.main.kts
зависимость из скрипта работает
```
`@file:DependsOn` скачивает артефакт из Maven Central при первом запуске и кеширует. Это закрывает нишу «однофайловая утилита с библиотеками» - там, где раньше нужен был отдельный проект.
Доступные файловые аннотации скриптов:
| Аннотация | Что делает |
|---|---|
| `@file:DependsOn("group:artifact:version")` | зависимость из Maven |
| `@file:Repository("https://...")` | дополнительный репозиторий |
| `@file:CompilerOptions("-jvm-target", "21")` | флаги компилятора |
| `@file:Import("other.main.kts")` | подключить другой скрипт |
Аргументы командной строки доступны как `args`:
```kotlin
// count.main.kts — запуск: kotlinc -script count.main.kts файл.txt
// val path = args.firstOrNull() ?: error("укажите файл")
// println(java.io.File(path).readLines().size)
fun main() {
// в скрипте args доступен напрямую, здесь — имитация
val args = arrayOf("демо.txt")
println("аргументов: ${args.size}") // аргументов: 1
}
```
Shebang делает скрипт исполняемым:
```
#!/usr/bin/env kotlin
println("запущено как ./script.main.kts")
```
## REPL
Запуск `kotlinc` без аргументов открывает интерактивную оболочку:
```
$ kotlinc
Welcome to Kotlin version 2.4.21
>>> val x = 42
>>> x * 2
res1: kotlin.Int = 84
>>> :quit
```
REPL полезен для проверки поведения stdlib. Ограничения: нет истории между сессиями, нет зависимостей, многострочные конструкции вводить неудобно. Для исследований лучше Kotlin Notebook в IDEA или `.main.kts`.
## Смешанная компиляция вручную
Если нужно скомпилировать Kotlin и Java вместе без Gradle, порядок важен:
```
# 1. Kotlin видит Java-исходники
kotlinc -cp . Lib.kt JLib.java -d out
# 2. javac видит скомпилированные Kotlin-классы
javac -cp out -d out Main.java
# 3. запуск с stdlib в classpath
java -cp out:$KOTLIN_HOME/lib/kotlin-stdlib.jar Main
```
Kotlin-компилятор принимает `.java`-файлы в аргументах - он их не компилирует, но читает для разрешения ссылок. Без stdlib в classpath запуск падает:
```
Exception in thread "main" java.lang.NoClassDefFoundError: kotlin/jvm/internal/Intrinsics
```
Подробнее: [Структура проекта](Структура%20проекта.md)
## Демон компилятора
Запуск JVM и инициализация компилятора занимают 1-2 секунды, поэтому `kotlinc` на каждый файл медленный. Gradle держит демон компилятора, который переиспользуется между сборками - отсюда разница между первой сборкой (десятки секунд) и последующими (секунды).
Настройка демона в `gradle.properties`:
```
kotlin.daemon.jvmargs=-Xmx2g
org.gradle.jvmargs=-Xmx4g
kotlin.incremental=true
```
Недостаточная память демона - частая причина `OutOfMemoryError` или неожиданно медленных сборок на больших проектах.
## Полезные комбинации
```
# что реально попало в байт-код
kotlinc Demo.kt -d out && javap -p -cp out DemoKt
# аннотации nullability и метаданные
kotlinc Demo.kt -d out && javap -v -p -cp out DemoKt | grep -E "Nullable|NotNull|Metadata"
# проверить, что код чистый
kotlinc -Werror -Wextra Demo.kt -d /dev/null
# совместимость со старым языком
kotlinc -language-version 2.0 -api-version 2.0 Demo.kt -d out
# список экспериментальных флагов
kotlinc -X
```
Подробнее про чтение байт-кода: [Kotlin в байт-коде](../Взаимодействие%20с%20Java/Kotlin%20в%20байт-коде.md)
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `javac Demo.java` | `kotlinc Demo.kt` |
> | `java Demo` | `java -jar app.jar` или `kotlin DemoKt` |
> | `--release 21` | `-jvm-target 21` |
> | `-Xlint:all -Werror` | `-Wextra -Werror` |
> | JEP 330: `java Demo.java` | `kotlinc -script` или `.main.kts` |
> | `jshell` | `kotlinc` без аргументов |
> | нет аналога | `@file:DependsOn` в скриптах |
>
> **Главное практическое различие - stdlib не входит в платформу.** `javac` компилирует в байт-код, который запустится на любой JVM: вся библиотека уже есть в JDK. Kotlin-код зависит от `kotlin-stdlib.jar`, поэтому нужен либо `-include-runtime`, либо stdlib в classpath. Это цена за то, что stdlib развивается независимо от JDK и одинакова на всех платформах (JVM, JS, Native).
>
> **`-jvm-target 1.8` по умолчанию - решение о совместимости, а не недоработка.** Kotlin много лет был основным языком Android, где долго держался уровень Java 8. Дефолт остался консервативным, и его нужно задавать осознанно: современный серверный проект почти всегда хочет 17 или 21.
>
> **Скрипты в Kotlin сильнее, чем однофайловый запуск в Java.** JEP 330 позволяет `java Demo.java`, но только с тем, что есть в JDK: зависимостей нет. `@file:DependsOn` в `.main.kts` скачивает артефакты из Maven, то есть однофайловый скрипт может использовать корутины, сериализацию, HTTP-клиент. Это полноценная замена Python/Bash для задач автоматизации.
>
> Сравнить: [JVM](../../Программирование%20на%20Java/JVM/JVM.md)
> [!warning] Подводные камни
> **`-jvm-target` по умолчанию 1.8.** Байт-код окажется совместимым с Java 8, и современные API JDK будут недоступны, а JIT не применит оптимизации новых версий. На это легко не обратить внимания - предупреждения нет.
>
> **JAR без `-include-runtime` не запускается:**
> ```
> $ kotlinc App.kt -d app.jar && java -jar app.jar
> Exception in thread "main" java.lang.NoClassDefFoundError: kotlin/jvm/internal/Intrinsics
> ```
> Либо добавьте флаг, либо укажите stdlib в `-cp`.
>
> **`-include-runtime` даёт JAR на ~5 МБ.** Для библиотеки это недопустимо: stdlib продублируется в каждой зависимости. Флаг только для конечных приложений.
>
> **`-e` и `-script` требуют разных форм записи.** `-e` принимает выражение в кавычках, `-script` - путь к `.kts`-файлу. Передать `.kts` в `-e` или выражение в `-script` не получится.
>
> **Файл скрипта должен называться `*.main.kts`, не `*.kts`.** Расширение `.kts` зарезервировано для Gradle-скриптов с другим шаблоном: `@file:DependsOn` в нём не сработает.
>
> **Первый запуск `@file:DependsOn` медленный.** Скачивание артефактов занимает секунды или десятки секунд. Дальше работает кеш `~/.m2`, но в CI без сохранённого кеша задержка повторится на каждом прогоне.
>
> **`-Werror` падает на предупреждениях из зависимостей.** Вызов устаревшего API библиотеки даст warning и остановит сборку. Нужен точечный `@Suppress("DEPRECATION")`, а не отключение флага.
>
> **REPL не видит зависимостей.** `kotlinc` без аргументов даёт только stdlib: подключить библиотеку нельзя (есть `-cp`, но он ненадёжен в интерактивном режиме). Для исследований с библиотеками используйте Kotlin Notebook или `.main.kts`.
>
> **Kotlin-компилятор не компилирует `.java`-файлы.** Передача `.java` в аргументах нужна только для разрешения ссылок; сами классы потом компилирует `javac`. Пропустить второй шаг - получить `ClassNotFoundException` на Java-классе.
>
> **`-module-name` влияет на видимость `internal`.** `internal` компилируется в публичное имя с суффиксом модуля (`helper$main`). Компиляция двух частей проекта с разными `-module-name` сделает их `internal`-члены взаимно недоступными, хотя в одном Gradle-модуле всё работало.
>
> **`-d /dev/null` для проверки работает не везде.** На Linux и macOS это способ скомпилировать без записи результата, но на некоторых версиях компилятор пытается создать каталог. Надёжнее писать во временный каталог и удалять.
Далее: [Gradle и Kotlin DSL](Gradle%20и%20Kotlin%20DSL.md)
@@ -0,0 +1,205 @@
---
title: Инструментарий
description: Обзор инструментов Kotlin — компилятор, сборка, кодогенерация, статический анализ — и как они складываются в проект
tags:
- kotlin
- tooling
---
Язык - это половина дела. Вторая половина - инструменты: чем компилировать, чем собирать, как генерировать код и как держать стиль. В Kotlin эти роли распределены между четырьмя слоями.
```mermaid
graph TD
A["исходники .kt"] --> B["компилятор K2<br/>kotlinc"]
B --> C[".class + kotlin_module"]
C --> D["jar / aar"]
E["Gradle + Kotlin DSL"] -->|"управляет"| B
E -->|"управляет"| F["KSP: кодогенерация"]
F -->|"генерирует .kt"| A
E -->|"управляет"| G["ktlint / detekt"]
G -->|"проверяет"| A
```
## Четыре слоя
| Слой | Инструмент | Зачем |
|---|---|---|
| компиляция | `kotlinc` / компилятор K2 | исходники → байт-код |
| сборка | Gradle + Kotlin DSL | зависимости, таски, многомодульность |
| кодогенерация | KSP | аннотации → сгенерированный код |
| качество | ktlint, detekt | формат и статический анализ |
```kotlin
fun main() {
// этот файл можно скомпилировать тремя способами:
// 1. kotlinc Main.kt -include-runtime -d app.jar
// 2. gradle build (с build.gradle.kts)
// 3. kotlinc -script Main.main.kts (как скрипт)
println("любой способ даёт один байт-код")
}
```
## Какой инструмент когда
**`kotlinc` напрямую** - для одного файла, эксперимента, проверки факта о языке. Не масштабируется: нет управления зависимостями.
**Gradle** - стандарт для всего остального. Kotlin DSL (`build.gradle.kts`) - это Kotlin-код, поэтому работает автодополнение и проверка типов. Подробнее: [Gradle и Kotlin DSL](Gradle%20и%20Kotlin%20DSL.md)
**Maven** поддерживается плагином `kotlin-maven-plugin`, но экосистема Kotlin ориентирована на Gradle: Multiplatform, Compose и большинство плагинов существуют только для него.
**KSP** - когда нужно генерировать код по аннотациям (Room, Moshi, Dagger, kotlin-inject). Заменил `kapt`, который работал через Java-стабы и был в разы медленнее. Подробнее: [KSP](KSP.md)
## Минимальный проект
```kotlin
// build.gradle.kts
plugins {
kotlin("jvm") version "2.4.0"
application
}
repositories { mavenCentral() }
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0")
testImplementation(kotlin("test"))
}
kotlin {
jvmToolchain(21)
}
application {
mainClass.set("com.example.MainKt")
}
tasks.test {
useJUnitPlatform()
}
```
Четыре блока покрывают 90% нужд: плагины, репозитории, зависимости, версия JVM. Остальное - настройка под конкретную задачу.
## Структура каталогов
```
project/
├── build.gradle.kts # сборка модуля
├── settings.gradle.kts # список модулей
├── gradle.properties # флаги Gradle и JVM
├── gradle/
│ ├── libs.versions.toml # version catalog
│ └── wrapper/ # gradle-wrapper.jar + properties
├── gradlew # запуск без установленного Gradle
└── src/
├── main/kotlin/
├── main/resources/
├── test/kotlin/
└── test/resources/
```
Подробнее: [Структура проекта](Структура%20проекта.md)
## Что делает компилятор K2
K2 - фронтенд компилятора, стабильный с Kotlin 2.0 и единственный в 2.x. Он быстрее старого примерно вдвое, точнее выводит типы и даёт более умные приведения. Важно, что K2 - это не только компиляция: один фронтенд обслуживает IDE, компилятор и инструменты анализа, поэтому подсветка в IDE и ошибки сборки наконец совпадают. Подробнее: [Компилятор K2](Компилятор%20K2.md)
## Версии: три разных числа
| Что | Где задаётся | Что значит |
|---|---|---|
| версия Kotlin | `kotlin("jvm") version "2.4.0"` | версия компилятора и stdlib |
| `languageVersion` | `compilerOptions` | какие возможности языка разрешены |
| `jvmTarget` / toolchain | `jvmToolchain(21)` | версия байт-кода и JDK |
```kotlin
// build.gradle.kts
plugins { kotlin("jvm") version "2.4.0" }
kotlin {
jvmToolchain(21) // JDK для компиляции и запуска
compilerOptions {
// можно ограничить язык более старой версией
// languageVersion.set(KotlinVersion.KOTLIN_2_1)
allWarningsAsErrors.set(false)
}
}
```
Частая ошибка - считать их одним. Можно компилировать Kotlin 2.4 компилятором, ограничив язык версией 2.1 (для совместимости с библиотекой), и выдавать байт-код для JVM 8.
## Качество кода
**ktlint** - форматирование по официальному style guide, почти без настроек. **detekt** - статический анализ: находит сложные функции, мёртвый код, подозрительные паттерны. Они дополняют друг друга: первый про форму, второй про смысл. Подробнее: [Линтеры и формат](Линтеры%20и%20формат.md)
## Карта раздела
| Заметка | О чём |
|---|---|
| [kotlinc](kotlinc.md) | CLI, скрипты, REPL, ключевые флаги |
| [Gradle и Kotlin DSL](Gradle%20и%20Kotlin%20DSL.md) | конфигурации, version catalog, таски |
| [Структура проекта](Структура%20проекта.md) | каталоги, модули, смешанные исходники |
| [Компилятор K2](Компилятор%20K2.md) | фазы, что изменилось, флаги |
| [KSP](KSP.md) | кодогенерация, процессоры, миграция с kapt |
| [Линтеры и формат](Линтеры%20и%20формат.md) | ktlint, detekt, CI |
## Что ещё есть в экосистеме
| Инструмент | Задача |
|---|---|
| Dokka | документация из KDoc (аналог Javadoc) |
| binary-compatibility-validator | контроль публичного ABI библиотеки |
| kotlinx-benchmark | микробенчмарки на JMH |
| Kover | покрытие тестами |
| kotlin-multiplatform | общий код для JVM/JS/Native |
| Compose Multiplatform | UI поверх Multiplatform |
| kotlinx-serialization | сериализация через плагин компилятора |
| Amper | экспериментальная альтернатива Gradle от JetBrains |
```kotlin
// build.gradle.kts с типичным набором для библиотеки
plugins {
kotlin("jvm") version "2.4.0"
kotlin("plugin.serialization") version "2.4.0"
id("org.jetbrains.dokka") version "2.0.0"
id("org.jetbrains.kotlinx.binary-compatibility-validator") version "0.17.0"
id("io.gitlab.arturbosch.detekt") version "1.23.8"
}
repositories { mavenCentral() }
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
testImplementation(kotlin("test"))
}
```
Обратите внимание: `kotlinx-serialization` - это плагин компилятора, а не библиотека рефлексии. Сериализаторы генерируются при компиляции, поэтому работают без `kotlin-reflect` и на Kotlin/Native.
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `javac` | `kotlinc` (K2) |
> | Maven или Gradle (Groovy) | Gradle + Kotlin DSL |
> | annotation processing (APT) | KSP |
> | Checkstyle, SpotBugs, PMD | ktlint, detekt |
> | Javadoc | Dokka (KDoc) |
> | Lombok | возможности языка (`data class`, свойства) |
> | один таргет - JVM | JVM, JS, Native, Wasm |
>
> **Gradle в Kotlin-проектах не альтернатива, а стандарт.** В Java-мире Maven остаётся распространённым выбором, и выбор между ними - вопрос предпочтений. В Kotlin инфраструктура (Multiplatform, Compose, KSP, большинство плагинов) строится вокруг Gradle, и Maven-плагин поддерживается по минимуму. Это сужает выбор, но концентрирует усилия.
>
> **KSP вместо APT - следствие того, что аннотационная обработка Java не видит Kotlin.** APT работает с Java-AST; Kotlin-код для него требует генерации стабов, то есть фактически компиляции дважды. KSP читает структуру Kotlin напрямую, поэтому работает в 2-4 раза быстрее и понимает nullable-типы, `suspend` и расширения. Цена - процессоры нужно писать заново: API другой.
>
> **Lombok не нужен.** Половина его функций (`@Data`, `@Getter`, `@Builder`, `@Value`) заменена возможностями языка: свойства, `data class`, значения по умолчанию в конструкторе. Это убирает целый класс проблем - Lombok работал через недокументированные API компилятора и ломался на каждом новом JDK.
>
> **Одна система сборки обслуживает несколько платформ.** Java-проект собирается для JVM; выход за её пределы (GWT, TeaVM, GraalVM) - отдельная история. В Kotlin JS, Native и Wasm - полноправные таргеты в том же `build.gradle.kts`, с общим исходным кодом. Цена - сложность конфигурации Multiplatform-проекта заметно выше.
>
> Сравнить: [JVM](../../Программирование%20на%20Java/JVM/JVM.md)
> [!warning] Подводные камни
> **Версия плагина Kotlin и версия stdlib должны совпадать.** Плагин подтягивает stdlib своей версии автоматически; явное указание `implementation(kotlin("stdlib", "2.3.0"))` при плагине 2.4.0 даёт конфликт и предупреждение. Не указывайте stdlib вручную вообще.
>
> **`jvmTarget` без toolchain ломается на другом JDK.** Если локально JDK 21, а в CI - 17, сборка с `jvmTarget = "21"` упадёт. `jvmToolchain(21)` решает: Gradle сам скачает нужный JDK.
>
> **`jvmTarget` Kotlin и `targetCompatibility` Java должны совпадать.** В смешанном проекте разные значения дают ошибку «Inconsistent JVM-target compatibility detected». `jvmToolchain` настраивает оба.
>
> **Значение по умолчанию `jvmTarget` - 1.8.** Компиляция напрямую через `kotlinc` без флага даёт байт-код версии 52 (Java 8), даже на JDK 27. Gradle-плагин это переопределяет, но при ручной компиляции нужно помнить.
>
> **kapt и KSP в одном проекте замедляют сборку.** Если часть библиотек ещё на kapt, оба процессора работают, и стабы генерируются всё равно. Миграцию стоит делать целиком.
>
> **Gradle без `gradlew` - источник расхождений.** Версия Gradle, установленная локально, почти наверняка отличается от CI. Wrapper (`gradlew`) фиксирует версию в репозитории - коммитить его обязательно.
>
> **`kotlin.incremental` может давать ложные сборки.** При смене версии компилятора или странных ошибках «unresolved reference» на корректном коде помогает `./gradlew clean` или удаление `build/` и `~/.gradle/caches/build-cache-1`.
>
> **Плагины компилятора фиксируют версию Kotlin.** `kotlin("plugin.serialization") version "2.4.0"` должен совпадать с версией основного плагина - иначе ошибка о несовместимости плагина компилятора. Используйте version catalog, чтобы версия была в одном месте.
Далее: [kotlinc](kotlinc.md)
@@ -0,0 +1,386 @@
---
title: Компилятор K2
description: Фазы компиляции, что изменилось по сравнению с K1, умные приведения, экспериментальные флаги
tags:
- kotlin
- tooling
- compiler
---
K2 - фронтенд компилятора Kotlin, стабильный с версии 2.0 и единственный в линии 2.x. Он быстрее старого примерно вдвое, точнее выводит типы и, что важнее всего, обслуживает одновременно компилятор и IDE - поэтому подсветка ошибок в редакторе совпадает с результатом сборки.
```kotlin
interface Base { fun f(): String }
class A : Base { override fun f() = "A" }
class B : Base { override fun f() = "B" }
fun describe(x: Any) {
if (x is A || x is B) {
// K2 выводит общий супертип Base для объединения типов
println(x.f())
}
}
fun main() {
describe(A()) // A
describe(B()) // B
}
```
В K1 этот код не компилировался: после `is A || is B` переменная оставалась `Any`, и `x.f()` требовал явного приведения.
## Фазы компиляции
Посмотреть реальные фазы можно флагами `-verbose -Xreport-perf`:
```
$ kotlinc -verbose -Xreport-perf Demo.kt -d out
PERF: INIT 271 ms
PERF: ANALYZE 530 ms 11.311 loc/s
PERF: TRANSLATION to IR 144 ms 41.453 loc/s
PERF: KLIB WRITING 0 ms
PERF: IR LOWERING 122 ms 48.799 loc/s
PERF: BACKEND 75 ms 79.456 loc/s
```
```mermaid
graph LR
A[".kt"] --> B["INIT<br/>загрузка classpath"]
B --> C["ANALYZE<br/>фронтенд K2: FIR"]
C --> D["TRANSLATION<br/>FIR → IR"]
D --> E["IR LOWERING<br/>развёртка inline,<br/>suspend, лямбды"]
E --> F["BACKEND<br/>IR → байт-код"]
F --> G[".class + Metadata"]
```
| Фаза | Что происходит |
|---|---|
| INIT | чтение classpath, инициализация компилятора |
| ANALYZE | разбор, разрешение имён, вывод типов, проверки - фронтенд K2 строит FIR |
| TRANSLATION to IR | FIR превращается в промежуточное представление, общее для всех бэкендов |
| IR LOWERING | развёртка `inline`, CPS для `suspend`, генерация лямбд, `data class`-методов |
| BACKEND | IR → байт-код JVM (или JS, Native, Wasm) |
Ключевое архитектурное решение - разделение на FIR (фронтенд) и IR (общий бэкенд). Фронтенд один для всех платформ, бэкенды разные. Отсюда и возможность Multiplatform: анализ типов выполняется один раз, генерация - под каждую цель.
## Что изменилось в умных приведениях
K2 переписал анализ потока данных, и это самое заметное для повседневной работы улучшение.
**Приведение через переменную с результатом проверки:**
```kotlin
fun viaVariable(x: Any?) {
val isString = x is String
if (isString) {
// K2 приводит тип по val с результатом проверки
println(x.length)
}
}
fun main() {
viaVariable("шесть!") // 6
}
```
**Приведение проникает в лямбду:**
```kotlin
fun intoLambda(x: Any) {
if (x is String) {
run { println(x.length) } // приведение видно внутри лямбды
}
}
fun main() {
intoLambda("семь!!!") // 7
}
```
**Nullable-функция вызывается после проверки:**
```kotlin
fun callable(f: (() -> Unit)?) {
if (f != null) {
f() // без ?.invoke()
}
}
fun main() {
callable { println("лямбда вызвана") } // лямбда вызвана
}
```
**Исчерпывающий `when` по данным потока:**
```kotlin
sealed interface State
data object Loading : State
data class Done(val value: String) : State
fun render(s: State) = when (s) {
Loading -> "загрузка"
is Done -> "готово: ${s.value}"
}
fun main() {
println(render(Loading)) // загрузка
println(render(Done("данные"))) // готово: данные
}
```
`else` не нужен - компилятор знает все варианты `sealed`. Подробнее: [Умные приведения](../Синтаксис%20и%20типы/Null-безопасность/Умные%20приведения.md)
Чего K2 **не** делает - приведения для изменяемого свойства класса:
```
$ kotlinc Demo.kt -d out
error: smart cast to 'String' is impossible, because 'value' is a mutable property
that could be mutated concurrently.
```
Это принципиальное ограничение, а не недоработка: между проверкой и использованием другой поток может записать `null`. Решение - локальная копия:
```kotlin
class Holder { var value: String? = "есть" }
fun main() {
val h = Holder()
val snapshot = h.value
if (snapshot != null) {
println(snapshot.length) // 4
}
}
```
## Полезные флаги анализа
### -Wextra
Дополнительные проверки K2: неиспользуемые значения, подозрительные конструкции.
```
kotlinc -Wextra Demo.kt -d out
```
### -Xreturn-value-checker
Предупреждает о проигнорированном результате функции - полезно для функций, чей результат нельзя терять:
```
$ kotlinc -Xreturn-value-checker=full Demo.kt -d out
warning: unused return value of 'compute'.
compute()
^^^^^^^
```
Режимы: `check` (только помеченные `@MustUseReturnValue`), `full` (все), `disable`. Особенно важно для `Result`-возвращающих функций: потерянный `Result` означает молча проглоченную ошибку. Подробнее: [Result и runCatching](../Исключения/Result%20и%20runCatching.md)
### -Xexplicit-api
Для библиотек: требует явно указывать видимость и типы в публичном API.
```
$ kotlinc -Xexplicit-api=strict Lib.kt -d out
error: visibility must be specified in explicit API mode.
class Public {
^^^^^^^^^^^^
error: visibility must be specified in explicit API mode.
fun noExplicitType() = 42
^^^^^^^^^^^^^^^^^^
```
```kotlin
// так проходит explicit API mode
public class Service {
public fun compute(): Int = 42
private fun internalHelper(): Int = 1
}
fun main() {
println(Service().compute()) // 42
}
```
В Gradle - `kotlin { explicitApi() }`. Это защита от случайного расширения публичного API: без флага `fun compute() = 42` публична и имеет выведенный тип, который может измениться при правке реализации.
### Производительность
| Флаг | Что делает |
|---|---|
| `-Xbackend-threads=N` | кодогенерация в N потоков |
| `-Xreport-perf` | статистика по фазам (с `-verbose`) |
| `-Xlambdas={class\|indy}` | схема компиляции лямбд |
| `-Xstring-concat={indy\|indy-with-constants\|inline}` | схема конкатенации строк |
`-Xlambdas=indy` (по умолчанию для JVM-таргета 8+) компилирует лямбды через `invokedynamic`, как в Java. `class` создаёт отдельные классы - нужно для совместимости с инструментами, которые ожидают классы лямбд.
## Экспериментальные возможности языка
K2 - точка входа для новых конструкций. Полный список - `kotlinc -X`.
| Флаг | Возможность |
|---|---|
| `-Xcontext-parameters` | context parameters (в 2.4 работают без флага) |
| `-Xwhen-guards` | `when` с охранными условиями |
| `-Xnon-local-break-continue` | `break`/`continue` из inline-лямбды |
| `-Xmulti-dollar-interpolation` | `$$` для литералов с `$` |
| `-Xname-based-destructuring` | деструктуризация по именам |
| `-Xnested-type-aliases` | `typealias` внутри класса |
| `-Xexplicit-backing-fields` | отдельный тип backing field и свойства |
| `-Xcontext-sensitive-resolution` | разрешение имён по ожидаемому типу |
| `-Xdata-flow-based-exhaustiveness` | исчерпывающий `when` по анализу потока |
| `-Xannotation-target-all` | `@all:` для аннотаций на свойствах |
```kotlin
// context parameters — в 2.4 уже без флага
interface Logger { fun log(message: String) }
context(logger: Logger)
fun process(data: String) {
logger.log("обработка: $data")
}
fun main() {
val logger = object : Logger {
override fun log(message: String) = println(message)
}
with(logger) {
process("запись") // обработка: запись
}
}
```
Подробнее: [Receivers и context parameters](../Функциональный%20Kotlin/Receivers%20и%20context%20parameters.md)
## Три версии, которые путают
```kotlin
// build.gradle.kts
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
kotlin {
jvmToolchain(21)
compilerOptions {
jvmTarget.set(JvmTarget.JVM_21) // версия байт-кода
languageVersion.set(KotlinVersion.KOTLIN_2_4) // возможности языка
apiVersion.set(KotlinVersion.KOTLIN_2_2) // доступный API stdlib
}
}
```
| Опция | Отвечает за | Типичная ошибка |
|---|---|---|
| `jvmTarget` | формат `.class` | оставить дефолтный 1.8 |
| `languageVersion` | синтаксис и семантика | ожидать новые возможности при старом значении |
| `apiVersion` | какие функции stdlib видны | указать устаревшую версию |
`apiVersion` ниже `languageVersion` - легальная комбинация: можно писать на новом языке, но не использовать новый API stdlib (чтобы библиотека работала со старым рантаймом).
Важно: устаревшая `apiVersion` сама даёт предупреждение. В Kotlin 2.4 значение 2.1 выдаёт «API version 2.1 is deprecated», и с `allWarningsAsErrors` это ломает сборку. Подробнее: [Gradle и Kotlin DSL](Gradle%20и%20Kotlin%20DSL.md)
## Progressive mode
```kotlin
// build.gradle.kts
kotlin {
compilerOptions {
progressiveMode.set(true)
}
}
```
Обычно исправления некорректного поведения проходят цикл миграции: предупреждение в одной версии, ошибка через две. Progressive mode применяет их сразу. Код, написанный в этом режиме, совместим с обычным, но не наоборот. Разумный выбор для приложений с частым обновлением Kotlin; для библиотек - осторожнее.
## Плагины компилятора
K2 поддерживает плагины, работающие на уровне FIR/IR - они видят типы и могут генерировать код:
| Плагин | Что делает |
|---|---|
| `kotlinx-serialization` | генерирует сериализаторы при компиляции |
| `all-open` | делает классы `open` (для Spring, Mockito) |
| `no-arg` | добавляет конструктор без аргументов (JPA) |
| `kotlin-spring` | предустановленный `all-open` для Spring-аннотаций |
| `kotlin-jpa` | предустановленный `no-arg` для JPA |
| `kotlin-lombok` | понимание Lombok-геттеров в смешанном коде |
| `power-assert` | подробные сообщения в `assert` |
| `compose` | оптимизация composable-функций |
```kotlin
// build.gradle.kts
plugins {
kotlin("jvm") version "2.4.0"
kotlin("plugin.serialization") version "2.4.0"
kotlin("plugin.spring") version "2.4.0"
kotlin("plugin.jpa") version "2.4.0"
}
```
Отличие плагина компилятора от KSP: плагин модифицирует IR напрямую (может менять существующий код), KSP только генерирует новые файлы. Плагины мощнее, но их API нестабилен между версиями Kotlin - поэтому сторонних плагинов мало. Подробнее: [KSP](KSP.md)
## Инкрементальная компиляция
```
# gradle.properties
kotlin.incremental=true
kotlin.incremental.useClasspathSnapshot=true
kotlin.daemon.jvmargs=-Xmx2g
```
Компилятор отслеживает, какие файлы зависят от изменённых, и пересобирает только их. Снимок classpath позволяет не пересобирать модуль, если в зависимости изменилась только реализация, а не ABI.
Что ломает инкрементальность:
| Причина | Следствие |
|---|---|
| изменение `inline`-функции | пересборка всех её вызовов |
| изменение публичного API | пересборка зависимых модулей |
| `api` вместо `implementation` | пересборка потребителей |
| правка в `buildSrc` | пересборка всего проекта |
| `clean` | полная пересборка |
## Диагностика проблем
```
# подробный лог компиляции
./gradlew compileKotlin --info
# время по фазам
kotlinc -verbose -Xreport-perf Demo.kt -d out
# что попало в байт-код
kotlinc Demo.kt -d out && javap -p -cp out DemoKt
# проверить без записи результата
./gradlew compileKotlin --rerun-tasks
# сбросить кеши при странных ошибках
./gradlew clean && rm -rf .gradle .kotlin
```
Если IDE показывает ошибку, а сборка проходит (или наоборот) - это признак расхождения версий компилятора в IDE и Gradle. В K2 такое встречается заметно реже, чем в K1, но при разных версиях плагина IDE и проекта возможно.
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `javac` - один фронтенд и бэкенд | FIR (фронтенд) + IR (бэкенды) |
> | компилятор только для JVM | JVM, JS, Native, Wasm |
> | нет анализа потока для приведений | умные приведения |
> | `instanceof` с pattern matching (16+) | `is` + автоматическое приведение |
> | аннотационные процессоры (APT) | плагины компилятора + KSP |
> | `-Xlint` | `-Wextra`, `-Xreturn-value-checker` |
> | IDE имеет свой анализатор | K2 общий для IDE и сборки |
>
> **Общий фронтенд для IDE и компилятора - главное инфраструктурное отличие.** В Java IntelliJ использует собственную реализацию анализа, отдельную от `javac`, и расхождения возможны, но редки - язык прост. Kotlin с его выводом типов, умными приведениями и перегрузкой разрешения сложнее, и в эпоху K1 расхождения между IDE и сборкой были постоянной болью. K2 устранил причину: это один и тот же код.
>
> **Умные приведения заменяют то, что в Java решается pattern matching.** Java 16 добавил `if (o instanceof String s)` - явное связывание переменной. Kotlin идёт дальше: переменная не вводится, а тип существующей уточняется по всему блоку, включая проверки через `&&`, `||`, переменные с результатом и лямбды. Цена - ограничение на изменяемые свойства: Kotlin отказывается приводить `var`-свойство класса, потому что не может гарантировать неизменность между проверкой и использованием. Java такой гарантии и не пытается дать, потому что связывает значение, а не уточняет тип.
>
> **Плагины компилятора - то, чего в Java осознанно нет.** APT в Java может только генерировать новые файлы: модификация существующего кода запрещена (Lombok делает это через недокументированные API, и потому ломается на каждом JDK). Kotlin разрешает плагинам менять IR, что даёт `kotlinx-serialization` без рефлексии и Compose с его оптимизациями. Цена - API плагинов нестабилен, и сторонний плагин нужно обновлять под каждую версию Kotlin.
>
> **Многоплатформенность определила архитектуру.** `javac` компилирует в байт-код JVM - это его единственная задача. Kotlin выводит типы один раз во фронтенде, а затем генерирует код под JVM, JS, Native или Wasm из общего IR. Отсюда и сложность компилятора, и то, что stdlib не часть платформы, и возможность писать общий код для сервера и браузера.
>
> Сравнить: [javac](../../Программирование%20на%20Java/JDK/javac.md), [JIT](../../Программирование%20на%20Java/JVM/JIT.md)
> [!warning] Подводные камни
> **Умное приведение не работает для `var`-свойства класса:**
> ```kotlin
> class Holder { var value: String? = "есть" }
>
> fun main() {
> val h = Holder()
> // if (h.value != null) println(h.value.length) // ошибка компиляции
> val snapshot = h.value
> println(if (snapshot != null) snapshot.length else 0) // 4
> }
> ```
> Сообщение: «smart cast is impossible, because 'value' is a mutable property that could be mutated concurrently». Это не обойти флагом - нужна локальная копия.
>
> **`-Xexplicit-api` обнаруживает проблему поздно.** Включение на существующей библиотеке даёт сотни ошибок сразу. Начинайте с `-Xexplicit-api=warning` и правьте постепенно, иначе включение откладывается навсегда.
>
> **Экспериментальные флаги `-X` не гарантируют совместимость.** Возможность может измениться или исчезнуть в следующей версии. Код, написанный с `-Xname-based-destructuring`, перестанет компилироваться при смене семантики флага. Для production-кода используйте только стабильные возможности.
>
> **`progressiveMode` - односторонний переход.** Код, написанный в progressive, компилируется и в обычном режиме, но обратное неверно: включение режима может сломать существующий код. Для библиотек, собираемых пользователями, это риск.
>
> **Устаревшая `apiVersion` вместе с `-Werror` роняет сборку.** В Kotlin 2.4 `apiVersion = 2.1` даёт предупреждение об устаревании, которое `allWarningsAsErrors` превращает в ошибку. Поддерживается около трёх предыдущих версий.
>
> **Изменение `inline`-функции обнуляет инкрементальность её вызовов.** Тело встроено во все точки вызова, поэтому пересобирается всё, что её использует - включая другие модули. `inline` в публичном API библиотеки фиксирует реализацию в ABI.
>
> **Нехватка памяти демона маскируется под медленную сборку.** Без `kotlin.daemon.jvmargs=-Xmx2g` компилятор на среднем проекте начинает тратить время на GC. Ошибки нет - просто сборка идёт в разы дольше.
>
> **`-Xbackend-threads` не всегда ускоряет.** На малых модулях накладные расходы на синхронизацию превышают выигрыш. Эффект заметен на модулях с тысячами файлов; измеряйте перед включением.
>
> **Расхождение IDE и сборки означает разные версии компилятора.** Плагин Kotlin в IDE и версия в `build.gradle.kts` должны соответствовать. Если IDE подсвечивает корректный код как ошибку, проверьте «Settings → Languages → Kotlin → Compiler» и перезапустите «Invalidate Caches».
>
> **`-Xreport-perf` без `-verbose` ничего не печатает.** Статистика идёт в лог, который включается отдельным флагом - без него команда выполняется молча, и кажется, что флаг не работает.
>
> **Плагин компилятора фиксирует версию Kotlin.** Обновление Kotlin требует обновления всех плагинов компилятора одновременно, иначе ошибка «plugin is not compatible». Это ограничивает скорость обновления на проектах с Compose или нестандартными плагинами.
Далее: [KSP](KSP.md)
@@ -0,0 +1,385 @@
---
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_<rule> = 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
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>MagicNumber:Bad.kt$6:72</ID>
<ID>UnusedPrivateProperty:Bad.kt$private val x</ID>
</CurrentIssues>
</SmellBaseline>
```
Новые нарушения ломают сборку, старые игнорируются. Это единственный реалистичный путь внедрения в существующий проект - иначе включение откладывается навсегда.
## Подавление правил
```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<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
```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)
@@ -0,0 +1,379 @@
---
title: Структура проекта
description: Каталоги исходников, пакеты, многомодульность, смешанный Kotlin+Java код и convention plugins
tags:
- kotlin
- tooling
- gradle
---
Структура Kotlin-проекта наследует соглашения Maven, но с двумя отличиями: каталог называется `kotlin`, а требование «путь пакета = путь каталога» необязательно.
```
myapp/
├── settings.gradle.kts
├── build.gradle.kts
├── gradle.properties
├── gradle/
│ ├── libs.versions.toml
│ └── wrapper/
├── gradlew
└── src/
├── main/
│ ├── kotlin/com/example/
│ └── resources/
└── test/
├── kotlin/com/example/
└── resources/
```
```kotlin
// src/main/kotlin/com/example/Main.kt
package com.example
fun main() {
println("стандартная структура")
}
```
Главный класс для `application` - `com.example.MainKt`: имя файла с суффиксом `Kt`. Подробнее: [Kotlin в байт-коде](../Взаимодействие%20с%20Java/Kotlin%20в%20байт-коде.md)
## Source sets
| Source set | Каталоги | Зависимости |
|---|---|---|
| `main` | `src/main/kotlin`, `src/main/java`, `src/main/resources` | `implementation`, `api` |
| `test` | `src/test/kotlin`, `src/test/java`, `src/test/resources` | `testImplementation` + всё из `main` |
Gradle-плагин Kotlin добавляет `src/main/java` в Kotlin source set, поэтому `.kt`-файл, лежащий в `src/main/java`, скомпилируется:
```
$ ls build/classes/kotlin/main/demo/
ExtraKt.class # из src/main/java/demo/Extra.kt
KotlinImpl.class # из src/main/kotlin/demo/KotlinImpl.kt
```
Работает, но вводит в заблуждение - держите Kotlin в `src/main/kotlin`.
Добавить свой source set:
```kotlin
// build.gradle.kts
sourceSets {
val integrationTest by creating {
kotlin.srcDir("src/integrationTest/kotlin")
resources.srcDir("src/integrationTest/resources")
compileClasspath += sourceSets["main"].output
runtimeClasspath += output + compileClasspath
}
}
val integrationTest by tasks.registering(Test::class) {
testClassesDirs = sourceSets["integrationTest"].output.classesDirs
classpath = sourceSets["integrationTest"].runtimeClasspath
useJUnitPlatform()
}
```
Для этой задачи есть и готовое решение - плагин `jvm-test-suite`:
```kotlin
// build.gradle.kts
testing {
suites {
val test by getting(JvmTestSuite::class) {
useKotlinTest()
}
register<JvmTestSuite>("integrationTest") {
useKotlinTest()
dependencies {
implementation(project())
}
}
}
}
```
## Пакеты и каталоги
В Kotlin объявленный пакет не обязан совпадать с путём каталога:
```kotlin
// файл src/main/kotlin/Utils.kt — лежит в корне
package com.example.utils
fun helper() = "работает из любого каталога"
fun main() {
println(helper()) // работает из любого каталога
}
```
Компилятор примет это без ошибки. Тем не менее совпадение - сильное соглашение: иначе навигация по проекту ломается, а IDE предупреждает. Исключение, которое делают осознанно: мелкие файлы с функциями верхнего уровня можно держать плоско.
Правило, которое действительно обязательно: один файл может содержать сколько угодно классов верхнего уровня, и их имена не связаны с именем файла. Это заметно отличается от Java. Подробнее: [Пакеты и импорты](../Синтаксис%20и%20типы/Базовый%20синтаксис/Пакеты%20и%20импорты.md)
## Как раскладывать файлы
```
com/example/
├── Main.kt # точка входа
├── domain/
│ ├── User.kt # data class User + связанные типы
│ ├── Order.kt
│ └── OrderStatus.kt # enum или sealed
├── repository/
│ ├── UserRepository.kt # interface
│ └── UserRepositoryImpl.kt
├── service/
│ └── OrderService.kt
├── api/
│ ├── Routes.kt # функции верхнего уровня
│ └── Dto.kt # несколько data class в одном файле
└── util/
├── Strings.kt # функции-расширения
└── Dates.kt
```
Две практики, специфичные для Kotlin:
**Мелкие связанные типы - в один файл.** `sealed interface Result` вместе со всеми наследниками, DTO одного эндпоинта, `enum` рядом с использующим классом. В Java каждый публичный класс требовал отдельного файла; здесь это выбор, а не обязанность.
**Расширения группируются по типу-получателю, а не по назначению.** `Strings.kt` со всеми расширениями `String`, `Collections.kt` - для коллекций. Это заменяет утилитные классы вроде `StringUtils`. Подробнее: [Функции-расширения](../Функциональный%20Kotlin/Функции-расширения.md)
## Смешанный Kotlin + Java
Один модуль может содержать оба языка, причём зависимости допускаются в обе стороны одновременно:
```
src/main/
├── kotlin/demo/KotlinImpl.kt
└── java/demo/
├── JavaBase.java
└── UsesKotlin.java
```
```java
// src/main/java/demo/JavaBase.java
package demo;
public abstract class JavaBase {
public abstract String name();
public String describe() { return "Java-база: " + name(); }
}
```
```kotlin
// src/main/kotlin/demo/KotlinImpl.kt
// Kotlin наследуется от Java-класса
// class KotlinImpl : JavaBase() {
// override fun name() = "из Kotlin"
// }
fun main() {
println("Java-база: из Kotlin") // Java-база: из Kotlin
}
```
```java
// src/main/java/demo/UsesKotlin.java
package demo;
public class UsesKotlin {
// Java использует Kotlin-класс, который наследуется от Java-класса
public static String call() { return new KotlinImpl().describe(); }
}
```
Это работает, хотя выглядит как циклическая зависимость. Порядок компиляции:
```mermaid
graph LR
A["Java-исходники"] -->|"читаются как стабы"| B["kotlinc:<br/>компиляция Kotlin"]
B --> C["Kotlin .class"]
C -->|"в classpath"| D["javac:<br/>компиляция Java"]
D --> E["Java .class"]
```
`kotlinc` получает `.java`-файлы в аргументах и разбирает их для разрешения ссылок, не компилируя. Затем `javac` компилирует Java, уже видя готовые Kotlin-классы. Поэтому Java может наследоваться от Kotlin-класса, который наследуется от Java-класса. Подробнее: [Взаимодействие с Java](../Взаимодействие%20с%20Java/Взаимодействие%20с%20Java.md)
Ограничение: эта схема работает в пределах одного модуля. Между модулями циклов быть не может.
## Многомодульный проект
```
myapp/
├── settings.gradle.kts
├── build.gradle.kts
├── gradle/libs.versions.toml
├── build-logic/ # convention plugins
│ ├── settings.gradle.kts
│ └── src/main/kotlin/
│ └── kotlin-conventions.gradle.kts
├── domain/ # чистая логика, без зависимостей
├── data/ # БД, HTTP
├── service/ # бизнес-логика
└── app/ # точка входа, DI
```
```kotlin
// settings.gradle.kts
rootProject.name = "myapp"
include("domain", "data", "service", "app")
```
```kotlin
// корневой build.gradle.kts
plugins {
kotlin("jvm") version "2.4.0" apply false
}
subprojects {
apply(plugin = "org.jetbrains.kotlin.jvm")
repositories { mavenCentral() }
dependencies {
"testImplementation"(kotlin("test"))
}
}
```
```kotlin
// app/build.gradle.kts
dependencies {
implementation(project(":service"))
implementation(project(":data"))
}
```
Направление зависимостей важно: `app` → `service` → `data` → `domain`, и `domain` не зависит ни от чего. Gradle обнаружит цикл между модулями и откажется собирать.
## Convention plugins
`subprojects { }` в корневом файле работает, но несовместим с configuration cache и не даёт типизации. Правильное решение - convention plugin:
```
build-logic/
├── settings.gradle.kts
├── build.gradle.kts
└── src/main/kotlin/kotlin-conventions.gradle.kts
```
```kotlin
// build-logic/build.gradle.kts
plugins {
`kotlin-dsl`
}
repositories {
mavenCentral()
gradlePluginPortal()
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.4.0")
}
```
```kotlin
// build-logic/src/main/kotlin/kotlin-conventions.gradle.kts
plugins {
kotlin("jvm")
}
repositories { mavenCentral() }
kotlin {
jvmToolchain(21)
compilerOptions {
allWarningsAsErrors.set(true)
}
}
dependencies {
"testImplementation"(kotlin("test"))
}
tasks.withType<Test> {
useJUnitPlatform()
}
```
```kotlin
// settings.gradle.kts
pluginManagement {
includeBuild("build-logic")
}
rootProject.name = "myapp"
include("domain", "data", "service", "app")
```
```kotlin
// domain/build.gradle.kts — одна строка вместо дублирования
plugins {
id("kotlin-conventions")
}
```
Имя плагина - это имя файла без `.gradle.kts`. Такой подход даёт типизированную конфигурацию, работает с configuration cache и не требует ничего знать о других модулях.
Исторически ту же роль играл каталог `buildSrc/`. Он проще (не нужен `includeBuild`), но у него есть недостаток: любое изменение в `buildSrc` инвалидирует кеш всего проекта. Для больших проектов предпочтителен `build-logic` как included build.
## Ресурсы
```kotlin
fun main() {
// чтение из src/main/resources
val stream = object {}.javaClass.getResourceAsStream("/config.properties")
println("ресурс найден: ${stream != null}") // ресурс найден: false
// идиоматичнее — через класс или top-level объект
val text = ClassLoader.getSystemResourceAsStream("missing.txt")
?.bufferedReader()?.readText() ?: "ресурс отсутствует"
println(text) // ресурс отсутствует
}
```
Путь с `/` - от корня classpath, без `/` - относительно пакета класса. Файлы из `src/main/resources` попадают в корень JAR. Для функций верхнего уровня ссылки на класс нет, поэтому используют `object {}.javaClass` или `ClassLoader`.
## Файлы, которые нужны в репозитории
| Путь | Коммитить |
|---|---|
| `gradlew`, `gradlew.bat` | да |
| `gradle/wrapper/gradle-wrapper.jar` | да |
| `gradle/wrapper/gradle-wrapper.properties` | да |
| `gradle/libs.versions.toml` | да |
| `gradle.properties` | да |
| `.gradle/`, `build/` | нет |
| `local.properties` | нет |
| `.idea/` | частично (`codeStyles`, `inspectionProfiles`) |
| `*.iml` | нет |
```
# .gitignore
build/
.gradle/
local.properties
*.iml
.idea/*
!.idea/codeStyles/
!.idea/inspectionProfiles/
.kotlin/
```
Каталог `.kotlin/` появился в Kotlin 2.x - это служебные данные компилятора, их не коммитят.
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `src/main/java` | `src/main/kotlin` |
> | один публичный класс на файл | сколько угодно |
> | имя файла = имя публичного класса | не связаны |
> | пакет обязан совпадать с каталогом | не обязан |
> | утилитный класс `StringUtils` | файл `Strings.kt` с расширениями |
> | `Main.class` | `MainKt.class` |
>
> **Освобождение от «один класс на файл» меняет раскладку кода.** В Java структура каталогов повторяла структуру типов: 20 классов - 20 файлов. В Kotlin файл - единица логической группировки, а не технического ограничения. `sealed interface` со всеми вариантами в одном файле читается целиком, а не по кускам. Побочный эффект: навигация «по файлам» работает хуже, и важнее осмысленные имена файлов.
>
> **Несовпадение пакета и каталога разрешено, но соглашение всё равно действует.** Kotlin не требует соответствия, потому что для компилятора оно не нужно: пакет указан в файле. Однако IDE, инструменты анализа и люди ожидают совпадения, поэтому отклоняются от него редко и только для плоских файлов с функциями.
>
> **Смешанная компиляция в одном модуле - то, чего нет в однодходовом `javac`.** Двухфазная схема (kotlinc читает Java-стабы, затем javac видит Kotlin-классы) позволяет взаимные ссылки между языками внутри модуля. Это ключевая возможность для миграции: можно конвертировать по одному файлу, не трогая остальное. Ограничение - только в пределах модуля.
>
> **Convention plugins - ответ Gradle на отсутствие наследования `pom.xml`.** В Maven общая конфигурация выносится в parent POM, и это декларативно. В Gradle аналога не было: писали `subprojects { }` или `allprojects { }`, что работает, но несовместимо с configuration cache и лишено типизации. Convention plugin - это обычный Gradle-плагин, лежащий в проекте: типизированный, кешируемый, переиспользуемый.
>
> Сравнить: [javac](../../Программирование%20на%20Java/JDK/javac.md), [jar](../../Программирование%20на%20Java/JDK/jar.md)
> [!warning] Подводные камни
> **Главный класс для `application` - не имя файла.** Для `src/main/kotlin/com/example/Main.kt` это `com.example.MainKt`, а не `com.example.Main`. Ошибка проявляется как `ClassNotFoundException` при `./gradlew run`. Если в файле стоит `@file:JvmName("App")`, то класс - `com.example.App`.
>
> **Kotlin-файлы в `src/main/java` компилируются молча.** Плагин добавляет этот каталог в Kotlin source set, так что ошибки не будет - но структура станет путаной, а инструменты, ожидающие Java в `java/`, могут сбиться.
>
> **`buildSrc` инвалидирует кеш всего проекта.** Любая правка в `buildSrc` заставляет Gradle пересобрать все модули. На больших проектах это минуты на каждое изменение конфигурации - используйте `build-logic` как included build.
>
> **`subprojects { }` несовместим с configuration cache.** Он обращается к другим проектам в фазе конфигурации, что кеш запрещает. При включении `org.gradle.configuration-cache=true` появятся ошибки «invocation of 'Project.subprojects'». Решение - convention plugins.
>
> **Циклы между модулями запрещены.** Если `data` ссылается на `service`, а `service` на `data`, Gradle откажется собирать с ошибкой «Circular dependency between tasks». Внутри одного модуля цикл Kotlin↔Java допустим, между модулями - нет. Общие типы выносят в третий модуль.
>
> **Ресурс не найден из функции верхнего уровня.** `javaClass` недоступен вне класса; `this::class.java` в top-level функции не скомпилируется. Используйте `object {}.javaClass.getResourceAsStream(...)` или `ClassLoader.getSystemResourceAsStream(...)`.
>
> **Путь к ресурсу чувствителен к ведущему слэшу.** `getResourceAsStream("config.txt")` ищет относительно пакета класса, `getResourceAsStream("/config.txt")` - от корня classpath. Первый вариант неожиданно возвращает `null`, если класс лежит в пакете.
>
> **`gradle-wrapper.jar` обязателен в репозитории.** Без него `./gradlew` не работает, и сборка пойдёт локальным Gradle другой версии. Если политика запрещает бинарники, добавьте в CI шаг `gradle/actions/wrapper-validation`.
>
> **Переименование файла меняет имя класса.** `Utils.kt` даёт `UtilsKt`, после переименования в `Helpers.kt` - `HelpersKt`. Для Java-клиентов и рефлексии это ломающее изменение; фиксируйте имя через `@file:JvmName`.
>
> **Тесты не видят `internal` из другого модуля.** `internal` в Kotlin - это видимость в пределах модуля компиляции, и `src/test` - отдельный source set. Gradle-плагин специально связывает `test` с `main`, поэтому тесты `internal`-члены видят; но тесты **другого** модуля - нет.
>
> **Файл без `package` попадает в корневой пакет.** Такой класс не импортируется из других пакетов нормальным способом и недоступен из Java-кода в пакете. Всегда объявляйте пакет, кроме скриптов.
Далее: [Компилятор K2](Компилятор%20K2.md)