Sonar multimodule with coverage logging File '<filename>' not found in project sources

Versions

  • SonarQube Server Enterprise Edition v2026.2.1 (121354)
  • Plugin version: 7.2.3.7755
  • jacoco packaged with gradle 9.4.1

Relevant config:

tasks.register("codeCoverageReport", JacocoReport) {
    subprojects { subproject ->
        subproject.plugins.withType(JacocoPlugin).configureEach {
            subproject.tasks.matching({ t -> t.extensions.findByType(JacocoTaskExtension) }).configureEach { testTask ->
                if (testTask.extensions.getByType(JacocoTaskExtension).isEnabled()) {
                    sourceSets subproject.sourceSets.main
                    executionData(testTask)
                } else {
                    logger.warn('Jacoco extension is disabled for test task \'{}\' in project \'{}\'. this test task will be excluded from jacoco report.',testTask.getName(),subproject.getName())
                }
            }

            // To automatically run `test` every time `./gradlew codeCoverageReport` is called,
            // you may want to set up a task dependency between them as shown below.
            // Note that this requires the `test` tasks to be resolved eagerly (see `forEach`) which
            // may have a negative effect on the configuration time of your build.
            subproject.tasks.matching({ t -> t.extensions.findByType(JacocoTaskExtension) }).forEach {
                rootProject.tasks.codeCoverageReport.dependsOn(it)
            }
        }
    }

    reports {
        xml.required = true
    }
}

And each module has org.sonarqube, jvm-test-suite and jacoco.

What is going wrong:
In our gitlab runners we are running the sonar gradle task. The logging for this tasks is too much for the runner and it cuts of the logging for the rest of the task. The log line that keeps repeating is: “File ‘’ not found in project sources”

What have i tried:
Setting sonar.coverage.jacoco.aggregateXmlReportPaths in each module
Setting sonar.coverage.jacoco.aggregateXmlReportPaths at top level only
Setting sonar.coverage.jacoco.xmlReportPaths in each module
Using the jacoco-report-aggregation plugin alongside these other solutions
I have also found a tmp solution by just piping the output into grep (e.g. | grep -v "File '[^']' not found in project sources"), but this is not really a good solution.

None of these solutions gave the desired result. I do believe it has something to do with the exclusions we use. We are excluding our generated file annotated with Generated. The excluded files seem to be logged as not found in project sources. So my guess is: jacoco generates a report on all files → sonar excludes some files but still finds these files in the coverage → log line output. OR: each module has the full (aggregated) report as their coverage → when sonar scans these modules it reads the coverage, but the sources do not have the files outside the module.

Hi,

Welcome to the community!

Based on these error messages, I guess (and it is a guess, without more context) the paths analysis is seeing in your coverage reports don’t match the paths it’s seeing during analysis.

The last paragraph of the docs section on adding coverage in a multi-module Maven project is relevant:

Please note that the import of aggregate report files depends on the value of sonar.sources analysis parameter pointing to the exact location where your Java sources. For example, if your project (or subproject) contains both Java and Kotlin sources, respectively under src/main/java and src/main/kotlin , then sonar.sources should include both folders expliclity (e.g. sonar.sources=src/main/java,src/main/kotlin ) rather than their shared common root (e.g. sonar.sources=src/main ).

 
Ann

I had a similar issue, because I have configured SonarQube to import my JaCoCo XML, so my sonar properties look like this:

sonar.sources=src/main/java,build.gradle.kts,settings.gradle.kts

sonar.coverage.jacoco.xmlReportPaths=build/reports/jacoco/codeCoverageReport/codeCoverageReport.xml

And then I saw this error:

Sensor JaCoCo XML Report Importer [jacoco]
Importing 1 report(s). Turn your logs in debug mode in order to see the exhaustive list.
File ‘MyGeneratedClass.java’ not found in project sources

The issue is thatMyGeneratedClass.java is included by the JaCoCo XML, and it is located somewhere inside build/generated/sources/annotationProcessor/java/main/, which is not part of sonar.sources , this is how the Gradle plugin works (and that is okay, as generated files should not be analyzed).

The solution is therefore to exclude the generated file from coverage, so this is actually a JaCoCo reporting problem, not a Sonar problem. For this I don’t know any better solution than Baeldung’s well-known article (assuming you use the JaCoCo report aggregation plugin):

reportTask.configure {
	classDirectories.setFrom(
		sourceSets.main.get().output.classesDirs.map { dir ->
			fileTree(dir).exclude(
                // You can of course be more specific if needed
                // to prevent unwanted matches.
				"**/MyGeneratedClass.class"
			)
		}
	)
}

I wrote this in here just in case someone finds this topic like I did.

It’s not just logging these entries for generated classes. It’s generating one warning for each module, for each class that is defined in another module. For us that’s 21751 lines of warnings!

It also happens for the example multi-module maven project in sonar-scanning-examples/sonar-scanner-maven/maven-multimodule/README.md at master · SonarSource/sonar-scanning-examples · GitHub

Hey @hylkevds, you caught my attention with your comment that this also happens in the example project. I found why: the correct property to use when passing an aggregate coverage jacoco file is sonar.coverage.jacoco.aggregateXmlReportPaths, and not sonar.coverage.jacoco.xmlReportPaths. This is reflected in the docs that Ann graciously linked:

<properties>
  <sonar.coverage.jacoco.aggregateXmlReportPaths>
    ${maven.multiModuleProjectDirectory}/report-aggregate/target/site/
      jacoco-aggregate/jacoco.xml
  </sonar.coverage.jacoco.aggregateXmlReportPaths>
</properties>

However, the example project uses sonar.coverage.jacoco.xmlReportPaths, both in the pom.xml and in the instructions in the readme. I tried running the example with the correct one and the warnings are gone (and coverage is still reported correctly), so it’s a simple fix. I have a PR to fix it.

Could you give this a try?

Yes, that fixes it!
Now it only warns about the generated classes, as expected.

Thanks!

@hylkevds, I would also add that, from the Gradle side, I think we can also make an improvement. Given that you used the wrong property, Sonar was running with default JaCoCo report path settings, and probably the reason Sonar picked up the JaCoCo reports at the default path is because such subproject-only JaCoCO report tasks were not disabled. To disable such tasks, this Groovy syntax, but you could something like this:

subprojects {
    pluginManager.withPlugin('java') {
        tasks.withType(JacocoReport).configureEach {
            enabled = false
    		// Also hide from regular task list
	    	group = null
        }
    }
}

This code is essentially part of the one I shared years ago at Allow Jacoco Aggregate plugin to create aggregated/merged report with multiple test suites · Issue #23223 · gradle/gradle · GitHub.

It is worth disabling such tasks because they will never be complete if code from one subproject call code from another subproject. And this is a well-known problem, for more details here’s the link I could quickly find:

Full disclosure: I enhanced this post with AI since I am not the best writer. So please forgive the artificial smell the rest of this post has.

Coming back to my own thread with findings from actually testing the proposed solutions on our Gradle build (~60 modules, Gradle 9.6.1, SonarScanner for Gradle 7.3.1.8318, SonarQube Server Enterprise 2026.3.1, sonar-jacoco 1.5.1), since the results may save other Gradle users some pain.

The accepted solution silently breaks coverage under Gradle

sonar.coverage.jacoco.aggregateXmlReportPaths works for Maven, but on our Gradle multi-module project it made the warnings disappear and silently dropped coverage from ~85% to 0, which is easy to miss, because the log looks cleaner than before. What we observed:

  • The aggregate report is handled by a separate project-level sensor (JaCoCo Aggregate XML Report Importer). It ran once at the end of the analysis and failed to resolve almost every file in the report: 2,532 of ~2,570 source files were logged as File '…' not found in project sources, regular sources, not just generated ones. Only files of a single module resolved.
  • The docs only document aggregateXmlReportPaths for Maven, there is no Gradle example, and in our testing it effectively does not work there.

So for Gradle users: if you apply the accepted fix, verify your coverage number afterwards, don’t trust the quieter log.

Gotcha: the Gradle scanner plugin re-creates the problem by itself

Even after removing our explicit per-module sonar.coverage.jacoco.xmlReportPaths, the warning storm continued. Cause: SonarScanner for Gradle auto-configures sonar.coverage.jacoco.xmlReportPaths from any JacocoReport task with XML output enabled (you can see it evaluating tasks in the logs: “JaCoCo report task detected, but XML report is not enabled or it was not produced”). Our root-level aggregate report task qualified, root properties are inherited by all modules, and every module went right back to importing the aggregate. To actually stop that, the root project must set the property explicitly empty:

// root project
sonarqube {
    properties {
        property 'sonar.coverage.jacoco.xmlReportPaths', ''
    }
}

What ended up working: per-module report slices

The root cause of the warnings is inherent to importing an aggregated report into every module: each module warns about every file that belongs to a sibling. The fix that eliminated all warnings and kept cross-module coverage (our integration-test module exercises code in every other module) was to give each module its own JacocoReport built from all execution data but only its own classes, and point each module’s xmlReportPaths at its own slice:

// root build.gradle
subprojects {
    pluginManager.withPlugin('java') {
        sonarqube {
            properties {
                property 'sonar.coverage.jacoco.xmlReportPaths',
                    layout.buildDirectory.file('reports/jacoco/sonarCoverageReport/sonarCoverageReport.xml').get().asFile.absolutePath
            }
        }
        tasks.register('sonarCoverageReport', JacocoReport) {
            // Ordering only (no dependsOn): our CI analysis job reuses exec data and classes
            // from an earlier build stage and must not re-run tests or compilation.
            mustRunAfter(rootProject.subprojects.collect { it.tasks.withType(Test) })
            mustRunAfter(rootProject.subprojects.collect { it.tasks.withType(JavaCompile) })
            executionData.setFrom(rootProject.fileTree(rootProject.rootDir) {
                include '**/build/jacoco/*.exec'
                exclude '**/.git/**', '**/.gradle/**'
            })
            sourceDirectories.setFrom(sourceSets.main.java.sourceDirectories)
            classDirectories.setFrom(sourceSets.main.output.classesDirs)
            reports.xml.required = true
            reports.html.required = false
        }
    }
}

tasks.named('sonar') {
    dependsOn(subprojects.collect { sp -> sp.tasks.matching { it.name == 'sonarCoverageReport' } })
}

Results on our project, verified on real analyses:

Aggregate into every module (before) aggregateXmlReportPaths Per-module slices
“File not found” warnings ~155,000 ~2,500 (once, project level) 0
Coverage correct (~85%) 0 :warning: correct (~85%, identical)

Each slice re-parses all exec data, but the tasks run in parallel and the whole analysis job still completes in ~2 to 3 minutes for us.

(We also adopted Gradle’s jacoco-report-aggregation plugin for the aggregate XML we still publish to our CI platform, but Sonar no longer consumes that report.)

Bonus: keeping generated classes out of the coverage report

Krisztian’s suggestion (excluding generated classes from the JaCoCo report) applied to us too, with a twist: our generated sources live inside src/main/java, interleaved with hand-written code, so they can’t be excluded by directory the way build/generated/** output can. They are only identifiable by their @Generated annotation. We derive class-file exclusion patterns by scanning the sources for the marker:

// Memoized: every per-module report task queries this, and the scan reads all main sources.
def generatedPatternsCache = new java.util.concurrent.ConcurrentHashMap<String, List<String>>()
def generatedClassPatterns = providers.provider {
    generatedPatternsCache.computeIfAbsent('patterns') {
        def patterns = []
        def marker = '@Generated("com.example.MyGenerator")'
        def sourceRoot = '/src/main/java/'
        fileTree(rootDir) {
            include "**${sourceRoot}**/*.java"
            exclude '**/build/**', '**/.git/**', '**/.gradle/**'
        }.each { source ->
            if (source.text.contains(marker)) {
                def path = source.path.replace(File.separator, '/')
                def rel = path.substring(path.indexOf(sourceRoot) + sourceRoot.length(), path.length() - '.java'.length())
                patterns << (rel + '.class')      // com/example/Foo.class
                patterns << (rel + '$*.class')    // ...and its inner classes
            }
        }
        patterns
    }
}

And in the sonarCoverageReport task above, swap the classDirectories line for:

def moduleClassDirs = sourceSets.main.output.classesDirs
classDirectories.setFrom(providers.provider {
    moduleClassDirs.files.collect { dir -> fileTree(dir) { exclude generatedClassPatterns.get() } }
})

Notes from doing this:

  • The same exclusion list is applied to our aggregate report task, so all coverage reports agree.
  • These files were already excluded from Sonar analysis (sonar.exclusions), so the Sonar coverage % doesn’t change. What changes is that the JaCoCo report itself stops counting generated code, and the residual “generated class” Hylke mentioned disappear because the report no longer references files Sonar doesn’t know.

Suggestion for SonarSource

Two things would help here:

  1. Document (or fix) sonar.coverage.jacoco.aggregateXmlReportPaths behaviour for Gradle, right now it fails silently, producing 0 coverage with a cleaner log than the misconfiguration it’s meant to fix.
  2. Consider demoting the per-file File '…' not found in project sources message, or summarizing it (N files in the report were not found in this module's sources), at tens of thousands of lines it truncates CI logs, and the per-file detail is rarely actionable.

Hope this saves the next Gradle multi-module user a few days.

Hello Daniël,

Thank you for the feedback and detailed explanation on your findings. I have created two tickets for us to address the issues you have reported here and will hopefully fix them soon.

Best regards,
Aurélien