Repository navigation
fix: support Gradle version catalog (libs.versions.toml) in analysis - #668
Conversation
Reviewer's GuideGradle version catalog files are now accepted by both Gradle providers, resolved to a parent build script for SBOM and license analysis, and rejected early when orphaned; provider tests cover Groovy and Kotlin paths, resolution, metadata, and failure cases. Sequence diagram for Gradle version catalog analysissequenceDiagram
participant Caller
participant Match
participant GradleProvider
participant FileSystem
participant Analysis
participant LicenseCheck
Caller->>Match: match(libs.versions.toml)
Match->>GradleProvider: isSupported(manifestName)
GradleProvider-->>Match: true
Match->>GradleProvider: validateLockFile(manifestDir)
GradleProvider->>FileSystem: existsSync(build.gradle[.kts])
FileSystem-->>GradleProvider: parent build file exists
GradleProvider-->>Match: true
Caller->>GradleProvider: provideStack(libs.versions.toml)
GradleProvider->>FileSystem: existsSync(build.gradle.kts)
FileSystem-->>GradleProvider: path or fallback to build.gradle
GradleProvider-->>Caller: Provided(SBOM, manifestPath=build.gradle[.kts])
Caller->>Analysis: requestStack(provider, manifest)
Analysis->>LicenseCheck: runLicenseCheck(content, manifestPath)
LicenseCheck-->>Analysis: license summary
Flow diagram for Gradle version catalog resolutionflowchart TD
A[libs.versions.toml discovered] --> B[Java_gradle.isSupported]
B -->|supported| C[Java_gradle.validateLockFile]
C --> D{Parent build.gradle.kts exists?}
D -->|yes| E[Resolve build.gradle.kts]
D -->|no| F{Parent build.gradle exists?}
F -->|yes| G[Resolve build.gradle]
F -->|no| H[Reject orphan TOML]
E --> I[provideStack or provideComponent]
G --> I
I --> J[Generate SBOM]
I --> K[Expose manifestPath]
K --> L[License check uses resolved build file]
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 7 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="src/providers/java_gradle.js" line_range="70" />
<code_context>
isSupported(manifestName) {
return this._getManifestName() === manifestName
+ || manifestName.endsWith('.versions.toml')
}
</code_context>
<issue_to_address>
**Interrupted writes corrupt catalogs**
When a catalog update is interrupted or the filesystem runs out of space during the write, a remediation that reaches the TOML updater writes the entire catalog directly to its existing path; if the write fails after truncation, such as from a full disk, the catalog is left partial or empty and subsequent Gradle builds fail.
Write the updated content to a temporary file in the same directory and atomically rename it over the catalog only after the write succeeds.
</issue_to_address>
### Comment 2
<location path="src/providers/java_gradle.js" line_range="112-114" />
<code_context>
+ }
+ const projectRoot = path.dirname(path.dirname(manifest))
+ for (const name of ['build.gradle.kts', 'build.gradle']) {
+ const candidate = path.join(projectRoot, name)
+ if (fs.existsSync(candidate)) {
+ return candidate
+ }
+ }
</code_context>
<issue_to_address>
**The wrong Gradle DSL is parsed**
When both `build.gradle` and `build.gradle.kts` exist, `match()` selects the Groovy provider, but `#resolveBuildManifest()` prefers the Kotlin build file. The Groovy provider then parses Kotlin exclusions or ignore directives with Groovy rules, so excluded or ignored dependencies remain in the SBOM.
Resolve the build file that corresponds to the provider selected by `match()`.
Also at `src/providers/java_gradle.js:70`.
</issue_to_address>
### Comment 3
<location path="src/providers/java_gradle.js" line_range="124" />
<code_context>
return {
ecosystem: ecosystem_gradle,
- content: await this.#createSbomStackAnalysis(manifest, opts),
+ content: await this.#createSbomStackAnalysis(buildManifest, opts),
contentType: 'application/vnd.cyclonedx+json'
}
</code_context>
<issue_to_address>
**Uneditable dependencies reported as fixes**
When the Gradle dependency graph contains a vulnerable dependency that has no matching library entry in the selected TOML catalog, the build-file SBOM includes transitive dependencies and dependencies declared outside the version catalog. `runRemediation()` retains those recommendations when `perDependencyChanges` is false, while `updateTomlVersions()` skips entries absent from the catalog; the report therefore shows fixes that were not applied.
Only return a remediation when the TOML updater confirms it can apply that change, including in the default remediation mode.
</issue_to_address>
### Comment 4
<location path="src/providers/java_gradle.js" line_range="140" />
<code_context>
return {
ecosystem: ecosystem_gradle,
- content: await this.#getSbomForComponentAnalysis(manifest, opts),
+ content: await this.#getSbomForComponentAnalysis(buildManifest, opts),
contentType: 'application/vnd.cyclonedx+json'
}
</code_context>
<issue_to_address>
**Project license is missed**
When component analysis or project-license reporting uses a catalog under `gradle/` and the project `LICENSE` is at the root, `requestComponent()` passes the original TOML path to `runLicenseCheck()`, which searches beside the catalog instead of the project root. The project license is missed, so component analysis omits project-license results and the compatibility baseline.
Run the license check using the resolved build path or otherwise search from the project root.
Also at `src/providers/java_gradle.js:70`.
</issue_to_address>
### Comment 5
<location path="src/providers/java_gradle.js" line_range="70" />
<code_context>
isSupported(manifestName) {
return this._getManifestName() === manifestName
+ || manifestName.endsWith('.versions.toml')
}
</code_context>
<issue_to_address>
**Catalog remediation entries are duplicated**
When a Gradle project has multiple discovered version catalogs, `runRemediation()` analyzes the same parent build once per catalog and appends the project-wide recommendations each time, including fixes for dependencies absent from a catalog. Callers receive duplicate or inapplicable remediation entries.
Avoid appending the same build’s recommendations per catalog and include only fixes applicable to the catalog.
</issue_to_address>
### Comment 6
<location path="src/providers/java_gradle.js" line_range="70" />
<code_context>
isSupported(manifestName) {
return this._getManifestName() === manifestName
+ || manifestName.endsWith('.versions.toml')
}
</code_context>
<issue_to_address>
**Concurrent fixes overwrite each other**
When two remediation jobs update different dependencies in the same version catalog concurrently, both jobs read the catalog and write full replacements from their in-memory copies. The later write discards the earlier job’s fix, so a requested upgrade is missing from the file.
Serialize catalog updates or merge each update with the latest catalog contents before writing.
</issue_to_address>
### Comment 7
<location path="src/providers/java_gradle.js" line_range="82" />
<code_context>
isSupported(manifestName) {
return this._getManifestName() === manifestName
+ || manifestName.endsWith('.versions.toml')
}
/**
</code_context>
<issue_to_address>
**Orphan catalogs stay invisible**
When an orphan `.versions.toml` is processed through remediation, `match` rejects an orphan catalog, but `runRemediation` catches that error and continues, so the newly detected invalid catalog is silently skipped and remediation can still report no fixes without explaining why.
Surface the match error for discovered version catalogs instead of silently continuing.
</issue_to_address>Sourcery assessment
Approval pending. 7 findings to address first.
Blocking findings: src/providers/java_gradle.js:70, src/providers/java_gradle.js:114, src/providers/java_gradle.js:124, src/providers/java_gradle.js:140, src/providers/java_gradle.js:70, and 2 more
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #668 +/- ##
==========================================
+ Coverage 91.94% 91.96% +0.01%
==========================================
Files 45 45
Lines 10996 11045 +49
Branches 2002 2019 +17
==========================================
+ Hits 10110 10157 +47
- Misses 886 888 +2
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
|
@sourcery-ai review |
There was a problem hiding this comment.
Hey - I've found 2 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="src/providers/java_gradle.js" line_range="70" />
<code_context>
isSupported(manifestName) {
return this._getManifestName() === manifestName
+ || manifestName.endsWith('.versions.toml')
}
</code_context>
<issue_to_address>
**Shared version updates falsely succeed**
When the analysis report recommends different fixed versions for libraries sharing one `version.ref`, `updateTomlVersions` reads the original version for each change but applies replacements to already-updated content. When two dependencies sharing a `version.ref` have different target versions, the first replacement wins, the next replacement does not match, and the updater still records it as applied; the catalog retains one version while remediation reports both fixes.
Detect conflicting changes per shared ref and either reconcile them before editing or report the conflict without claiming every remediation was applied.
</issue_to_address>
### Comment 2
<location path="test/providers/java_gradle_groovy.test.js" line_range="94" />
<code_context>
+
+ let result = await provider.provideStack(`${dir}/gradle/libs.versions.toml`)
+
+ expect(JSON.parse(result.content)).to.deep.equal(JSON.parse(expectedSbom))
+ }).timeout(process.env.GITHUB_ACTIONS ? 40000 : 10000)
+
</code_context>
<issue_to_address>
**Wrong build path passes SBOM tests**
When the provider invokes Gradle using the original TOML path rather than the resolved build-file path, the TOML-path SBOM tests still pass if `provideStack()` passes the TOML path to Gradle instead of the resolved build file: `mockInvokeCommand` returns the same fixture output regardless of the command arguments or working directory, so the deep-equality assertions do not verify which path was used.
Have the stub assert the Gradle invocation’s working directory or otherwise verify that the resolved build path is used.
Also at `test/providers/java_gradle_kotlin.test.js:94`.
</issue_to_address>Sourcery assessment
Approval pending. 2 findings to address first.
Blocking findings: src/providers/java_gradle.js:70, test/providers/java_gradle_groovy.test.js:94
The Gradle provider's isSupported() only matched build.gradle[.kts], so match() threw for .versions.toml files — causing silent 0 results in remediate mode and a hard failure in check mode. Teach the Gradle provider to recognise .versions.toml files and resolve them to the parent build.gradle[.kts] for SBOM generation. The updater layer (updateTomlVersions) already handles writing fixes back to the TOML file, so no changes are needed outside the provider. validateLockFile() now checks the parent directory for a build file when the manifest lives in a subdirectory (the gradle/ convention), which also means match() correctly rejects orphan TOML files with no parent build file. Implements TC-6860 Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> Assisted-by: Claude Code
When component analysis receives a TOML version catalog path (e.g. gradle/libs.versions.toml), the license check searched for LICENSE files next to the catalog instead of the project root. The provider now returns the resolved build file path via an optional manifestPath field in the Provided type, and requestComponent uses it for the license check. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
… parent dir of a catalog file
Summary
.versions.toml) were discovered byfindManifests()but rejected bymatch()— no provider recognised them.This caused silent 0 remediations and check-mode hard failures.
.versions.tomlinisSupported()andresolves to the parent
build.gradle[.kts]for SBOM generation viaprovideStack()/provideComponent().validateLockFile()checks for a parent build file so orphan TOML filesare rejected at match time rather than failing downstream.
Knock-on effects
stackAnalysis()andcomponentAnalysis()inindex.jsalso flowthrough
match(), so they gain TOML support with no code changes.(only
build.gradle[.kts]), so this change has no effect there untilthe extension adds its own TOML support.
validateLockFile()now performs twoexistsSyncchecks where itpreviously returned
trueunconditionally. For regularbuild.gradlefiles the first check trivially passes (the file is in its own directory).
Test plan
provideStackwith TOML path produces same SBOM asbuild.gradlepath (Groovy + Kotlin)provideStackthrows for TOML with no parent build file (Groovy + Kotlin)isSupportedreturns true forlibs.versions.toml(Groovy + Kotlin)Implements TC-6860
🤖 Generated with Claude Code
Summary by Sourcery
Enable Gradle analysis to accept version catalog manifests by resolving them to their parent build files.
New Features:
libs.versions.toml) in stack and component analysis.Bug Fixes:
Enhancements:
build.gradleorbuild.gradle.ktsfiles and expose the resolved manifest path.Tests:
Chores: