Versions
- SonarScanner for Gradle: 7.3.1.8318
- SonarQube: Community Build 26.6.0.123539 (reproduced locally)
- Android Gradle Plugin: 9.2.1
- Gradle: 9.5.1
- Kotlin: 2.3.0
- Module type: Kotlin Multiplatform (
org.jetbrains.kotlin.multiplatform) with an androidTarget() producing an AAR via the Android Library plugin (com.android.library) + org.jetbrains.compose. Source sets: commonMain, androidMain, androidUnitTest (plus jvmMain/iosMain/wasmJsMain).
Summary
For KMP modules that have an Android target, the scanner registers each Android-specific source set into sonar.sources/sonar.tests twice, once via the Kotlin source set and once via the Android variant’s source provider. The duplicate directory aborts analysis with:
File <module>/src/androidUnitTest/kotlin/.../SomeTest.kt can't be indexed twice.
Please check that inclusion/exclusion patterns produce disjoint sets for main and test files
The scanner’s own per-module output shows the duplicate directly:
Indexing files of module 'neem'
Base dir: .../design-system/neem
Source paths: src/androidMain/kotlin, src/commonMain/kotlin, src/iosMain/...
Test paths: src/androidUnitTest/kotlin, src/androidUnitTest/kotlin <- same dir listed twice
androidMain is likewise duplicated in Source paths, producing the same error for main source files (e.g. androidMain/.../PlatformNativeToast.android.kt), not just tests.
Steps to reproduce
- A multi-module Gradle build with at least one KMP module applying
org.jetbrains.kotlin.multiplatform with androidTarget() + com.android.library (AGP 9.2.1), containing files under both src/androidMain/kotlin and src/androidUnitTest/kotlin.
- Apply
org.sonarqube 7.3.1.8318 on the root project.
- Run
./gradlew assembleDebug then ./gradlew sonar.
:sonar fails at the indexing phase with “can’t be indexed twice” for an androidUnitTest (or androidMain) file.
This appears to be scanner-side, not user misconfiguration — no sonar.sources/sonar.tests is set manually; the duplicate entries are produced entirely by the plugin’s own source-set computation.
Workarounds tried
Re-setting sonar.sources/sonar.tests per module to a de-duplicated explicit list — the value is appended, not replaced, so the directory still appears twice (the Android variant provider re-adds it).
-Dsonar.gradle.scanAll=true — no effect; the duplicate lives in the Gradle-computed source sets, which scanAll don’t dedupe. Analysis fails identically.
Only working workaround: per-module sonar.exclusions + sonar.test.exclusions of **/src/androidMain/**,**/src/androidUnitTest/**,**/src/test/**. This drops androidMain production code from analysis — a real coverage loss (~5k lines of Android UI code in one module, in our case).
Expected behavior
The scanner should register each KMP Android source-set directory once in sonar.sources/sonar.tests (de-duplicate the Kotlin-source-set vs Android-variant-provider contributions). So, KMP Android modules are analysed without excluding androidMain.
Hello @Harshit_Pandey,
Thanks for reaching out and reporting this issue.
I’ve been able to reproduce your error and created SCANGRADLE-429 to track this issue in our backlog.
Best,
Sebastian Zumbrunn
I’ve added this temporary workaround to my project while this is fixed:
afterEvaluate {
val resolverTask = tasks.withType<SonarResolverTask>().firstOrNull() ?: return@afterEvaluate
val adjustSonarTask = tasks.register("adjustSonar") {
description = "Filters duplicate KMP Kotlin dirs from the SonarQube resolver file"
dependsOn(resolverTask)
// Declare the resolver file as both input and output — the task reads it,
// filters it, and writes back the cleaned version. This enables Gradle's
// up-to-date checks: the task is skipped when the resolver output hasn't changed.
// The input is marked optional because the resolver file doesn't exist at
// configuration time — it's produced by sonarResolver at execution time.
val resolverFile = resolverTask.outputFile
inputs.file(resolverFile).optional(true)
outputs.file(resolverFile).optional(true)
onlyIf { resolverFile.exists() }
doLast {
if (!resolverFile.exists()) {
logger.info("adjustSonar \[${project.path}\]: resolver file not found, skipping")
return@doLast
}
try {
val props = ResolutionSerializer.read(resolverFile)
if (!props.isPresent) return@doLast
val pp = props.get()
val originalSourcesCount = pp.androidSources?.size ?: 0
val originalTestsCount = pp.androidTests?.size ?: 0
// Remove androidMain/kotlin and androidUnitTest/kotlin — these are
// already registered by SonarPropertyComputer.configureForKotlin()
val filteredSources = pp.androidSources
?.filterNot { it.contains("androidMain${File.separator}kotlin") }
?: emptyList()
val filteredTests = pp.androidTests
?.filterNot { it.contains("androidUnitTest${File.separator}kotlin") || it.contains("androidInstrumentedTest${File.separator}kotlin") }
?: emptyList()
val sourcesRemoved = originalSourcesCount - filteredSources.size
val testsRemoved = originalTestsCount - filteredTests.size
if (sourcesRemoved == 0 && testsRemoved == 0) {
logger.info("adjustSonar \[${pp.projectName}\]: no duplicate KMP kotlin dirs found, skipping")
return@doLast
}
val newProps = ProjectProperties.Builder(pp.projectName, pp.isRootProject)
.compileClasspath(pp.compileClasspath ?: emptyList())
.testCompileClasspath(pp.testCompileClasspath ?: emptyList())
.mainLibraries(pp.mainLibraries ?: emptyList())
.testLibraries(pp.testLibraries ?: emptyList())
.androidSources(filteredSources)
.androidTests(filteredTests)
.build()
ResolutionSerializer.write(resolverFile, newProps)
logger.lifecycle(
"adjustSonar \[${pp.projectName}\]: removed $sourcesRemoved androidMain/kotlin source(s) and $testsRemoved androidUnitTest/kotlin test(s) from resolver file"
)
} catch (e: Exception) {
logger.warn("adjustSonar \[${project.path}\]: failed to process resolver file: ${e.message}", e)
}
}
}
// Ensure resolver files are cleaned up before the root SonarTask reads them.
rootProject.tasks.matching { it.name == "sonar" }.configureEach {
dependsOn(adjustSonarTask)
}
}
}
Any update on when this fix will be merged? The MR has been approved and stable for quite a while now.
Hey @Daniel_Dello,
We’re currently planning to merge and release it next week.
Best,
Sebastian Zumbrunn
Hey again,
The fix has been released as part of Scanner for Gradle 7.4.0.8496
Cheers,
Sebastian Zumbrunn
Thank you! Just tried the new plugin and this issue of duplicate registration seems to be gone, but now I seen to be facing a different problem with my JVM only modules:
Probably related to this thread: SonarScanner for Gradle 7.4.0.8496 analysis failing - #2 by stephane-barbaray_ec