Same request here, with a data point that I think reframes it from convenience to correctness: the sources are not merely analysed as main code by mistake — they are dropped silently.
Because the scanner derives sonar.tests from the test source set only, files in any other test suite belong to neither sonar.sources nor sonar.tests. They are never indexed, no rule ever runs on them, and nothing in the log says so. You only notice if you go looking.
Versions: SonarScanner for Gradle 7.5.0.8588, Gradle 9.7.1, SonarQube Server 2026.4.1.
Impact in one real multi-module build (24 Gradle projects, integrationTest registered via testing { suites { register<JvmTestSuite>("integrationTest") } }, plus java-test-fixtures):
|
test directories passed |
modules including integrationTest/testFixtures |
sonar.java.test.binaries |
| plugin default |
17 |
0 |
17 |
| with workaround |
28 |
9 |
28 |
That is 103 Java files — 52 integration tests and 51 test fixtures — absent from analysis. One module consists only of integration tests and test fixtures, so it was invisible almost in its entirety.
Anyone can check their own build without touching SonarQube, which is how the numbers above were produced:
./gradlew sonar -Dsonar.scanner.internal.dumpToFile=/tmp/props.txt
grep 'sonar\.tests' /tmp/props.txt
Suggested detection rule, rather than “all source sets except main”: the JvmTestSuites in the
testing.suites container (first-class since Gradle 7.3, and what integrationTest already is) plus the
testFixtures source set contributed by java-test-fixtures. That keeps jmh, generated or other custom
source sets out, and needs no new configuration from users — the build already declares its intent.
The workaround we settled on (Kotlin DSL, per project, in a convention plugin):
sonar {
properties {
val testSourceSets = the<SourceSetContainer>().filter { it.name != "main" }
// Skip missing directories, the analysis fails on them.
property("sonar.tests", testSourceSets.flatMap { it.java.srcDirs }.filter { it.isDirectory })
property("sonar.java.test.binaries",
testSourceSets.flatMap { it.output.classesDirs.files }.filter { it.isDirectory })
}
}
// The class paths the analyzers resolve types against.
// The Sonar plugin derives them from the "main" and "test" source sets only,
// so without this every dependency of the integration tests and the test fixtures is unresolved,
// and Sonar downgrades all rules that need type information.
// The runtime class path adds the transitive "implementation" dependencies:
// Sonar needs the full closure to resolve types a dependency exposes,
// while the compile class path deliberately hides them.
// Resolve via the resolver task, because the Sonar task itself may not resolve other projects' configurations.
val mainClassPath = files(provider {
the<SourceSetContainer>().filter { it.name == "main" }
.flatMap { listOf(it.compileClasspath, it.runtimeClasspath) }
})
val testClassPath = files(provider {
the<SourceSetContainer>().filter { it.name != "main" }
.flatMap { listOf(it.compileClasspath, it.runtimeClasspath) }
})
tasks.named<SonarResolverTask>(SonarResolverTask.TASK_NAME) {
setCompileClasspath(provider { mainClassPath })
setTestCompileClasspath(provider { testClassPath })
}
Two details that cost us time and may help others: a non-existent directory in sonar.tests fails the analysis outright (File system element for parameter … does not exist), hence the isDirectory filter; and main and test sets must stay disjoint or indexing aborts with “can’t be indexed twice”. Unlike the original post we did not need sonar.java.test.libraries, since our integrationTest configurations extend the test ones, so the classpath is already right.
Is this on the roadmap, or tracked in a ticket we could follow?