diff --git a/CHANGELOG.md b/CHANGELOG.md index c56dda2..bbfdeca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 52fd453..e8bcdff 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" diff --git a/plugin-build/plugin-foundation/src/main/kotlin/ru/kode/android/app/quality/plugin/foundation/DetektWiring.kt b/plugin-build/plugin-foundation/src/main/kotlin/ru/kode/android/app/quality/plugin/foundation/DetektWiring.kt index a865864..b4b530d 100644 --- a/plugin-build/plugin-foundation/src/main/kotlin/ru/kode/android/app/quality/plugin/foundation/DetektWiring.kt +++ b/plugin-build/plugin-foundation/src/main/kotlin/ru/kode/android/app/quality/plugin/foundation/DetektWiring.kt @@ -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( @@ -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) diff --git a/plugin-test/foundation/src/test/kotlin/ru/kode/android/app/quality/plugin/foundation/KotlinMultiplatformConfigurationTest.kt b/plugin-test/foundation/src/test/kotlin/ru/kode/android/app/quality/plugin/foundation/KotlinMultiplatformConfigurationTest.kt index 8697da7..2320232 100644 --- a/plugin-test/foundation/src/test/kotlin/ru/kode/android/app/quality/plugin/foundation/KotlinMultiplatformConfigurationTest.kt +++ b/plugin-test/foundation/src/test/kotlin/ru/kode/android/app/quality/plugin/foundation/KotlinMultiplatformConfigurationTest.kt @@ -2,6 +2,7 @@ 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 @@ -9,8 +10,10 @@ 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 /** @@ -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}", + ) + } } diff --git a/plugin-test/utils/src/main/java/ru/kode/android/app/quality/plugin/test/utils/QualityProjectBuilders.kt b/plugin-test/utils/src/main/java/ru/kode/android/app/quality/plugin/test/utils/QualityProjectBuilders.kt index 5d641eb..8f5fac4 100644 --- a/plugin-test/utils/src/main/java/ru/kode/android/app/quality/plugin/test/utils/QualityProjectBuilders.kt +++ b/plugin-test/utils/src/main/java/ru/kode/android/app/quality/plugin/test/utils/QualityProjectBuilders.kt @@ -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 @@ -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)} @@ -492,6 +508,8 @@ private fun moduleBuildFileContent( $buildTypesBlock } + + $androidTargetBlock """.trimIndent().removeBlankLines() }