ru.kode.android.app-quality.foundation is a Gradle plugin for Android/Kotlin repositories that centralizes static analysis and formatting checks.
It configures Detekt for eligible modules, runs ktlint through CLI, and provides aggregate verification tasks for local development and CI.
- Registers aggregate quality tasks:
pipelineCheckandprePushCheck - Registers
ktlintCheckandktlintFormattasks at the root project - Configures Detekt in subprojects and merges platform-specific Detekt configs
- Sets Git hooks path via
gitHooksSetup - Supports configurable logging and Detekt JVM target
- Provides bundled default config files when project-level files are missing
- Java 17
- Gradle 9.x (this repository uses wrapper
9.4.0) - Version catalog named
libs - If the plugin is applied directly to an Android module project: Android Gradle Plugin
7.4.0+andcom.android.application
The plugin looks up these aliases in the libs catalog. None are required — any alias
missing (or the catalog itself missing) falls back to the plugin's bundled default version:
[versions]
detekt = "1.23.8"
ktlintCli = "1.8.0"
detektComposeRules = "1.4.0"
[libraries]
ktlint-cli = { module = "com.pinterest.ktlint:ktlint-cli", version.ref = "ktlintCli" }
detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" }
detekt-compose-rules = { module = "ru.kode:detekt-rules-compose", version.ref = "detektComposeRules" }detekt-compose-rules is required only for modules using Compose Detekt rules.
Catalog lookups are lazy: a missing alias fails the first task that needs it (with an explanatory message), not the plugin apply, so unrelated tasks keep working.
In settings.gradle.kts:
pluginManagement {
repositories {
gradlePluginPortal()
mavenCentral()
google()
}
}In root build.gradle.kts:
plugins {
id("ru.kode.android.app-quality.foundation") version "2.0.0"
}Use the latest published version for your project.
Publish plugin artifacts locally:
./gradlew --project-dir plugin-build publishToMavenLocalThen ensure your consumer project has mavenLocal() in pluginManagement.repositories, and apply:
plugins {
id("ru.kode.android.app-quality.foundation") version "<local-version>"
}Run the main quality pipeline:
./gradlew pipelineCheckPre-push formatting + static analysis:
./gradlew prePushCheckgitHooksSetup: runsgit config core.hooksPath <path>(default<root>/.githooks); skipped automatically when the root project is not a git repository, or whengitHooksEnabledis set tofalsektlintCheck: runs ktlint checks for Kotlin sources (up-to-date aware: skipped when sources and config did not change)ktlintFormat: runs ktlint auto-format for Kotlin sourcespipelineCheck: depends ongitHooksSetup,ktlintCheck,detektCheck(if eligible modules exist), andandroidLintCheck(ifandroidLint.enabledistrue)prePushCheck: depends ongitHooksSetup,ktlintFormat,detektCheck(if eligible modules exist), andandroidLintCheck(ifandroidLint.enabledistrue)androidLintCheck: runs Android Gradle Plugin lint checks; skipped unlessandroidLint.enabledistrueprintRequiredGradleJvmargs: prints the current Gradle JVM input argumentsgenerateDefaultDetektKotlinConfig/...AndroidConfig/...ComposeConfig/...AndroidRulesJar/generateDefaultKtlintEditorconfig: materialize bundled default configs and resources into<root>/build/app-quality/; run automatically only when a default is actually used. The rules jar (generateDefaultDetektAndroidRulesJar) contains the bundled KODE Android detekt ruleset and is placed under<root>/build/app-quality/detekt/rules/
Extension name:
appQualityFoundation { ... }Example:
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
appQualityFoundation {
verboseLogging.set(false)
jvmTarget.set(JvmTarget.JVM_17)
gitHooks.set(rootProject.layout.projectDirectory.file(".githooks"))
gitHooksEnabled.set(true) // set false to opt out of git hooks setup entirely
ktlint {
projectConfig.set(rootProject.layout.projectDirectory.file(".editorconfig"))
sources {
include.set(listOf("**/src/*/kotlin/**/*.kts"))
exclude.set(listOf("**/build-logic/**"))
// Also available as vararg sugar: include("**/src/*/kotlin/**/*.kts"); exclude("**/build-logic/**")
}
}
detekt {
ignoredBuildTypes.set(listOf("release", "internal", "external", "demo"))
sources {
include.set(listOf("src/custom/kotlin"))
exclude.set(listOf("tmpGenerated"))
}
typeResolution.set(false)
baseline.set(layout.projectDirectory.file("detekt-baseline.xml"))
xmlReportEnabled.set(true)
sarifReportEnabled.set(false)
kotlin {
projectConfig.set(layout.projectDirectory.file("detekt-kotlin-config.yml"))
rules {
from(rootProject.layout.projectDirectory.file("libs/detekt-rules-1.4.0.jar"))
}
}
android {
projectConfig.set(layout.projectDirectory.file("detekt-android-config.yml"))
}
compose {
projectConfig.set(layout.projectDirectory.file("detekt-compose-config.yml"))
}
}
androidLint {
enabled.set(true)
}
}| Property | Default |
|---|---|
verboseLogging |
false |
jvmTarget |
JVM_17 |
gitHooks |
<root>/.githooks |
gitHooksEnabled |
true |
ktlint.sources.include |
["**/src/*/java/**/*.kt", "**/src/*/kotlin/**/*.kt"] (while useDefaults is true) |
ktlint.sources.exclude |
["**/build/**", "**/generated/**", "**/templates/**", "**/src/test/**", "**/src/androidTest/**", "**/src/commonTest/**", "templates/**", "**/schema/**/*.kt"] (while useDefaults is true) |
detekt.ignoredBuildTypes |
["release", "internal", "external", "demo"] |
detekt.sources.include |
per-platform Kotlin/Java source dirs (while useDefaults is true) |
detekt.sources.exclude |
[] |
detekt.typeResolution |
false |
detekt.baseline |
unset (no baseline) |
detekt.xmlReportEnabled |
false |
detekt.sarifReportEnabled |
false |
androidLint.enabled |
false |
ktlint.cli |
libs.ktlint-cli, falling back to the plugin's own baked-in com.pinterest.ktlint:ktlint-cli coordinate if no matching catalog alias exists (while useDefaults is true) |
detekt.kotlin.rules |
libs.detekt-formatting, falling back to the plugin's own baked-in io.gitlab.arturbosch.detekt:detekt-formatting coordinate if no matching catalog alias exists (while useDefaults is true) |
detekt.android.rules |
the plugin's bundled KODE Android rules jar (not published anywhere externally — see MIGRATION.md) (while useDefaults is true) |
detekt.compose.rules |
libs.detekt-compose-rules, falling back to the plugin's own baked-in ru.kode:detekt-rules-compose coordinate if no matching catalog alias exists (while useDefaults is true) |
Every external dependency of the plugin lives in a uniform slot (ktlint.cli,
detekt.<platform>.rules) configurable from ANY source through one from(...) API —
version-catalog accessors, string coordinates (e.g. your own published rule sets), or jar
files. Additions always stack ON TOP of the slot's default; disable the default with
useDefaults.set(false).
For ktlint.cli/detekt.kotlin.rules/detekt.compose.rules, a matching alias in your own
libs catalog (if present) always wins; the plugin's baked-in coordinate is only a fallback,
so the plugin works with zero catalog setup too. detekt.android.rules has no catalog-alias
option at all — its default is bundled directly in the plugin (the jar isn't published to any
Maven repo). See MIGRATION.md for upgrade notes.
appQualityFoundation {
ktlint.cli {
from(deps.ktlint.cli) // typed accessor from any catalog
// from("com.pinterest.ktlint:ktlint-cli:1.8.0") // or coordinates
// from(fileTree("tools/ktlint") { include("*.jar") }) // or checked-in jars
useDefaults.set(false) // drop the `libs` catalog default
}
detekt.kotlin.rules {
from(files("libs/detekt-rules-1.4.0.jar")) // stacks on detekt-formatting
}
detekt.compose.rules {
from("ru.kode:detekt-rules-compose:1.4.0") // published custom rules
useDefaults.set(false) // replace the default entirely
}
}A configured-but-missing file in any slot fails the build with an explanatory message naming the slot.
All configuration blocks (ktlint { }, detekt { }, detekt.kotlin { }, cli { },
rules { }) have both Action and Groovy Closure overloads, so the same block syntax
works identically in build.gradle.kts and Groovy build.gradle scripts.
Detekt configs are resolved and merged per module: each module independently walks the priority chain for every platform layer that applies to it (see Module Coverage below):
- Extension override — set once at the root, forces that file for ALL modules
- Module-local file — e.g.
<module>/detekt-kotlin-config.yml, lets a module customize its own rules; other modules are unaffected - Bundled default — used by any module without an override or a local file
Only the storage location of the bundled defaults is root-level: since their content is
identical for every module, they are generated once under <root>/build/app-quality/ by
dedicated tasks instead of being copied into every module. Nothing is written at
configuration time, so the configuration cache stays reusable and creating a module file
later is picked up correctly.
- Ktlint (runs once at the root over all modules, so its whole chain is root-level):
- Extension override:
ktlint.projectConfig - Project file lookup:
<root>/.editorconfig - Bundled default (generated):
<root>/build/app-quality/ktlint/.editorconfig
- Extension override:
- Detekt Kotlin (per module):
- Extension override:
detekt.kotlin.projectConfig - Module file lookup:
<module>/detekt-kotlin-config.yml - Bundled default (generated):
<root>/build/app-quality/detekt/kotlin-config.yml
- Extension override:
- Detekt Android (per module):
- Extension override:
detekt.android.projectConfig - Module file lookup:
<module>/detekt-android-config.yml - Bundled default (generated):
<root>/build/app-quality/detekt/android-config.yml
- Extension override:
- Detekt Compose (per module):
- Extension override:
detekt.compose.projectConfig - Module file lookup:
<module>/detekt-compose-config.yml - Bundled default (generated):
<root>/build/app-quality/detekt/compose-config.yml
- Extension override:
detekt.kotlin.rules, detekt.compose.rules, and ktlint.cli fall back to a baked-in
coordinate default when no matching libs catalog alias exists (see Defaults above).
detekt.android.rules has its own bundled-jar default, since it isn't published anywhere
externally. A configured-but-missing file in any slot fails the build with an explanatory
message naming the slot (see "Configuring dependencies" above).
Detekt is applied only to subprojects that use a matching plugin (plain Java modules are left untouched). Config layers merged per module:
- Kotlin config —
org.jetbrains.kotlin.jvm,org.jetbrains.kotlin.multiplatform,org.jetbrains.kotlin.android,com.android.application,com.android.library - Android config (additionally) —
org.jetbrains.kotlin.android,com.android.application,com.android.library - Compose config (additionally) —
org.jetbrains.compose,org.jetbrains.kotlin.plugin.compose
- Full checks:
./gradlew preMerge - Plugin checks only:
./gradlew --project-dir plugin-build preMerge - Test suite:
./gradlew --project-dir plugin-test test - Example app quality run:
./gradlew --project-dir example-project pipelineCheck
Set environment variables:
GRADLE_PUBLISH_KEYGRADLE_PUBLISH_SECRET
Then publish:
./gradlew --project-dir plugin-build setupPluginUploadFromEnvironment publishPluginsThis project is licensed under the MIT License. See LICENSE.