Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ All notable changes to this project are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.0.2] - 2026-08-20

### Fixed

- Detekt's `**/generated/**`/`**/build/**` exclude patterns no longer get silently overridden on
AGP/KMP Android-target variant tasks (e.g. `detektAndroidDebug`). detekt-gradle-plugin's own
lazy variant-registration callback reassigns `task.source` from the AGP variant's `sourceSets`
after this plugin's own `fileTree(include/exclude)` assignment, and that AGP source set already
treats the KSP output directory as a first-class source root β€” so KSP-generated code was being
linted. Added a lazy, absolute-path-based `task.exclude { ... }` that survives the later
`setSource()` override.

## [2.0.1] - 2026-08-19
* Switch to use Gradle commons library

Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[versions]
appQualityPlugin = "2.0.1"
appQualityPlugin = "2.0.2"
kotlin = "2.4.10"
agp = "9.3.1"
detekt = "1.23.8"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import ru.kode.android.app.quality.plugin.foundation.utils.resolveConfigFile
import ru.kode.android.app.quality.plugin.foundation.utils.wireDependencies
import ru.kode.android.app.quality.plugin.foundation.validate.validateSubprojectAgpVersion
import ru.kode.android.gradle.commons.logger.LoggerService
import java.io.File

internal val DEFAULT_DETEKT_INCLUDE_PATTERNS =
listOf(
Expand Down Expand Up @@ -269,6 +270,17 @@ private fun Project.configureDetektTasks(
}
}

// Defense-in-depth: AGP/KMP Android-target variant tasks (e.g. detektAndroidDebug) have
// their `source` reassigned later by detekt-gradle-plugin's own variant-registration
// callback, from the AGP variant's sourceSets β€” which already treats the KSP output dir
// as a first-class source root, silently overriding the exclude patterns above. A glob
// exclude can't catch this either: for those variants the source root itself already
// sits inside build/generated/..., so a root-relative path never contains that segment
// again. `exclude(Spec)` is lazy and additive, evaluated against whatever `source` ends
// up being at execution time, and matches on the absolute file path instead.
val generatedPathMarker = "${File.separator}build${File.separator}generated${File.separator}"
task.exclude { fileTreeElement -> fileTreeElement.file.path.contains(generatedPathMarker) }

task.reports {
it.xml.required.set(detektConfig.xmlReportEnabled)
it.html.required.set(false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@ package ru.kode.android.app.quality.plugin.foundation

import org.gradle.testkit.runner.TaskOutcome
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import ru.kode.android.app.quality.plugin.test.utils.ModuleSpec
import ru.kode.android.app.quality.plugin.test.utils.ModuleType
import ru.kode.android.app.quality.plugin.test.utils.createQualityProject
import ru.kode.android.app.quality.plugin.test.utils.resolveRequiredAgpJars
import ru.kode.android.app.quality.plugin.test.utils.runTask
import ru.kode.android.app.quality.plugin.test.utils.runTaskWithFail
import ru.kode.android.app.quality.plugin.test.utils.runTasks
import java.io.File

/**
Expand Down Expand Up @@ -67,4 +70,48 @@ class KotlinMultiplatformConfigurationTest {

assertEquals(TaskOutcome.SUCCESS, result.task(":shared:detekt")?.outcome)
}

// Regression guard for the AGP/KMP variant-task override bug: detekt-gradle-plugin's own
// lazy variant-registration callback reassigns `task.source` from the AGP variant's
// sourceSets AFTER this plugin's own fileTree(include/exclude) assignment, which already
// treats build/generated/... as a first-class source root. Only a lazy, absolute-path-based
// `exclude(Spec)` (DetektWiring.kt) survives that later override.
@Test
fun `KSP-generated source under a KMP Android-target variant is excluded from detektAndroidDebug`() {
projectDir.createQualityProject(
modules =
listOf(
ModuleSpec(
name = "shared",
type = ModuleType.AndroidLib,
applyMultiplatformPlugin = true,
detektAndroidConfigContent = Configs.DETEKT_MAX_LINE_60,
kotlinSources = mapOf("src/androidMain/kotlin/ru/kode/test/Main.kt" to Sources.CLEAN_TWO_SPACE),
),
),
)
// Simulate KSP's output dir being registered as a source root by AGP's variant API β€”
// this file violates the module's own max-line-length rule, so if the exclude ever
// stops holding, detektAndroidDebug fails on it.
val generatedFile =
File(
projectDir,
"shared/build/generated/ksp/androidDebug/kotlin/ru/kode/test/Generated.kt",
)
generatedFile.parentFile.mkdirs()
generatedFile.writeText(Sources.LONG_LINE_80)

val result =
projectDir.runTasks(
":shared:detektAndroidDebug",
agpClasspath = resolveRequiredAgpJars(LEGACY_AGP_VERSION),
gradleVersion = LEGACY_GRADLE_VERSION,
)

assertEquals(TaskOutcome.SUCCESS, result.task(":shared:detektAndroidDebug")?.outcome)
assertFalse(
result.output.contains("Generated.kt"),
"expected the KSP-generated file to be excluded from analysis entirely, got: ${result.output}",
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,13 @@ private fun moduleBuildFileContent(
}
.orEmpty()
val kotlinAndroidPlugin =
if (module.applyKotlinAndroidPlugin) pluginId("org.jetbrains.kotlin.android", useKotlinDsl) else ""
if (module.applyMultiplatformPlugin) {
pluginId("org.jetbrains.kotlin.multiplatform", useKotlinDsl)
} else if (module.applyKotlinAndroidPlugin) {
pluginId("org.jetbrains.kotlin.android", useKotlinDsl)
} else {
""
}
val composePlugin =
if (module.applyJetbrainsComposePlugin) {
// Since Compose Multiplatform 1.6.10, org.jetbrains.compose requires the
Expand Down Expand Up @@ -475,6 +481,16 @@ private fun moduleBuildFileContent(
}
val compileSdkLine =
if (useKotlinDsl) "compileSdk = ${module.compileSdk}" else "compileSdk ${module.compileSdk}"
val androidTargetBlock =
if (module.applyMultiplatformPlugin) {
"""
kotlin {
androidTarget()
}
"""
} else {
""
}
"""
plugins {
${pluginId(androidPluginId, useKotlinDsl)}
Expand All @@ -492,6 +508,8 @@ private fun moduleBuildFileContent(

$buildTypesBlock
}

$androidTargetBlock
""".trimIndent().removeBlankLines()
}

Expand Down
Loading