From 22b59c0da762e0881c23b863f3ed6bc646eddfd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Lo=CC=81pez?= Date: Sun, 26 Jul 2026 16:28:11 -0400 Subject: [PATCH] fix: classify Gradle/KMP test source sets as test zone for Kotlin The Kotlin plugin declared no zone_rules, so it fell back to the common rules, which look for a '/test/' or '/tests/' path segment. Kotlin projects are laid out by Gradle source set instead, and Kotlin Multiplatform names every test source set 'Test': shared/src/commonTest/kotlin/... sync/src/jvmTest/kotlin/... composeApp/src/androidUnitTest/kotlin/... None contain '/test/', so every test file was zoned production. On a KMP project that put all 241 files in the production zone, with 64 test files reported as orphaned (a test file has no importers by definition) and hardcoded test credentials raised as production security findings. Adds desloppify/languages/kotlin/_zones.py covering KMP and plain Gradle/Android test source sets, marking Gradle build scripts as config, and wires it into the plugin via zone_rules. --- desloppify/languages/kotlin/__init__.py | 3 + desloppify/languages/kotlin/_zones.py | 44 ++++++++++++ .../tests/lang/common/test_kotlin_zones.py | 67 +++++++++++++++++++ 3 files changed, 114 insertions(+) create mode 100644 desloppify/languages/kotlin/_zones.py create mode 100644 desloppify/tests/lang/common/test_kotlin_zones.py diff --git a/desloppify/languages/kotlin/__init__.py b/desloppify/languages/kotlin/__init__.py index cd9e40cd5..3c850a18a 100644 --- a/desloppify/languages/kotlin/__init__.py +++ b/desloppify/languages/kotlin/__init__.py @@ -2,6 +2,7 @@ from desloppify.languages._framework.generic_support.core import generic_lang from desloppify.languages._framework.treesitter import KOTLIN_SPEC +from desloppify.languages.kotlin._zones import KOTLIN_ZONE_RULES generic_lang( name="kotlin", @@ -20,9 +21,11 @@ depth="shallow", detect_markers=["build.gradle.kts", "build.gradle"], treesitter_spec=KOTLIN_SPEC, + zone_rules=KOTLIN_ZONE_RULES, ) __all__ = [ "generic_lang", "KOTLIN_SPEC", + "KOTLIN_ZONE_RULES", ] diff --git a/desloppify/languages/kotlin/_zones.py b/desloppify/languages/kotlin/_zones.py new file mode 100644 index 000000000..ca5559cb3 --- /dev/null +++ b/desloppify/languages/kotlin/_zones.py @@ -0,0 +1,44 @@ +"""Zone/path classification rules for Kotlin. + +Kotlin projects are laid out by Gradle source sets, not by a top-level ``test/`` +directory. Kotlin Multiplatform in particular names every test source set +``Test`` — ``src/commonTest/kotlin``, ``src/jvmTest/kotlin``, +``src/iosTest/kotlin``, ``src/androidUnitTest/kotlin`` — none of which contain the +``/test/`` path segment the common rules look for. Without these rules every test +file is scored as production code. +""" + +from __future__ import annotations + +from desloppify.engine.policy.zones import COMMON_ZONE_RULES, Zone, ZoneRule + +KOTLIN_ZONE_RULES = [ + # Gradle/KMP test source sets: src/test/, src/androidTest/, src/commonTest/, + # src/jvmTest/, src/iosTest/, src/androidUnitTest/, src/nativeTest/, ... + # The pattern has no leading "." / trailing "_" so it matches as a path substring. + ZoneRule( + Zone.TEST, + [ + "/src/test/", + "/src/androidTest/", + "/src/testFixtures/", + "Test/kotlin/", + "Test/java/", + "Test/resources/", + ], + ), + ZoneRule(Zone.GENERATED, ["/generated/", "/build/generated/"]), + ZoneRule( + Zone.CONFIG, + [ + "build.gradle.kts", + "settings.gradle.kts", + "build.gradle", + "settings.gradle", + "gradle.properties", + "gradle/libs.versions.toml", + ], + ), +] + COMMON_ZONE_RULES + +__all__ = ["KOTLIN_ZONE_RULES"] diff --git a/desloppify/tests/lang/common/test_kotlin_zones.py b/desloppify/tests/lang/common/test_kotlin_zones.py new file mode 100644 index 000000000..d0be7d747 --- /dev/null +++ b/desloppify/tests/lang/common/test_kotlin_zones.py @@ -0,0 +1,67 @@ +"""Regression tests for Kotlin/Gradle zone classification. + +Kotlin projects are organised by Gradle source sets rather than a top-level +``test/`` directory, so the common rules classify every test file as production. +""" + +from __future__ import annotations + +import pytest + +from desloppify.engine.policy.zones import Zone, classify_file +from desloppify.languages.kotlin._zones import KOTLIN_ZONE_RULES + + +def _zone(rel_path: str) -> Zone: + return classify_file(rel_path, KOTLIN_ZONE_RULES) + + +@pytest.mark.parametrize( + "rel_path", + [ + # Kotlin Multiplatform source sets + "shared/src/commonTest/kotlin/com/example/FormEngineTest.kt", + "sync/src/jvmTest/kotlin/com/example/SubmissionStoreTest.kt", + "sync/src/iosTest/kotlin/com/example/DeviceCryptoTest.kt", + "composeApp/src/androidUnitTest/kotlin/com/example/RendererTest.kt", + "shared/src/nativeTest/kotlin/com/example/ParserTest.kt", + # Plain Gradle JVM / Android layouts + "app/src/test/java/com/example/HelperTest.kt", + "app/src/androidTest/java/com/example/LoginFlowTest.kt", + "lib/src/testFixtures/kotlin/com/example/Fixtures.kt", + ], +) +def test_gradle_test_source_sets_are_test_zone(rel_path): + assert _zone(rel_path) == Zone.TEST + + +@pytest.mark.parametrize( + "rel_path", + [ + "shared/src/commonMain/kotlin/com/example/FormEngine.kt", + "sync/src/jvmMain/kotlin/com/example/SubmissionStore.kt", + "composeApp/src/androidMain/kotlin/com/example/Renderer.kt", + "composeApp/src/iosMain/kotlin/com/example/MapSeam.ios.kt", + ], +) +def test_main_source_sets_stay_production(rel_path): + assert _zone(rel_path) == Zone.PRODUCTION + + +@pytest.mark.parametrize( + "rel_path", + [ + "build.gradle.kts", + "settings.gradle.kts", + "androidApp/build.gradle.kts", + "gradle.properties", + "gradle/libs.versions.toml", + ], +) +def test_gradle_build_files_are_config_zone(rel_path): + assert _zone(rel_path) == Zone.CONFIG + + +def test_latest_is_not_mistaken_for_a_test_source_set(): + """A directory merely ending in 'Test' must still need the source-set suffix.""" + assert _zone("shared/src/commonMain/kotlin/com/example/LatestKotlinThing.kt") == Zone.PRODUCTION