Skip to content

fix(manifest): keep Maven dependencies below a skipped scope - #1720

Open
Jeppe Fredsgaard Blaabjerg (jfblaa) wants to merge 2 commits into
v1.xfrom
jfblaa/rea-1162-maven-facts-excluding-the-test-scope-drops
Open

Jeppe Fredsgaard Blaabjerg (jfblaa) wants to merge 2 commits into
v1.xfrom
jfblaa/rea-1162-maven-facts-excluding-the-test-scope-drops

Conversation

@jfblaa

Copy link
Copy Markdown
Contributor

LLM Description written by Claude Code:claude-opus-5-5

Fixes REA-1162.

With --exclude-configs or --include-configs, the Maven facts extension cut off the whole subtree of a dependency whose scope it skips. Maven keeps a node below such a dependency when the node's own scope passes, so dependencies Maven ships were missing from the facts, with nothing reported.

How Maven behaves (checked across the supported range, 3.2.5 to 4.0.0-rc-6)

  • Maven collects a project's artifacts by testing each node of the resolved graph against its scope filter, and it recurses into the node's children either way: maven-core RepositoryUtils.toArtifacts (3.9.9, called by LifecycleDependencyResolver; the same in 3.2.5 and 4.0.0-rc-6).
  • A child normally takes its scope from its parent, so everything below a test dependency is test (JavaScopeDeriver). A scope set by dependencyManagement is not derived that way (ConflictResolver.deriveScope in resolver 1.9.22; ClassicConflictResolver.deriveScope in 2.0.21).
  • So a dependency managed to compile below a test one ships. With Maven 3.9.9 and 4.0.0-rc-6, dependency:build-classpath for the runtime scope and the WEB-INF/lib of a packaged war both include it. mvn dependency:tree -Dscope=compile does not show it, because that view's filter hides a node's whole subtree.

Change

  • The extension walks the whole resolved graph, then drops the skipped nodes, keeping the edges Maven resolved between the nodes it keeps.
  • Only a kept node reached solely through skipped ones gets an edge Maven did not resolve. It is linked to its nearest kept ancestors, and it is direct when the module is one of them. Every other node keeps only resolved edges, so a library that both a test dependency and a compile one pull in is not marked direct.

Trade-off to weigh: that link skips the excluded nodes between, and a dependency reached only through an excluded direct dependency is marked direct, though no <dependency> declares it. The alternative, keeping the excluded parents as structure, would put the excluded scope back into the SBOM.

socket fix (Coana master, run on facts from this branch with test excluded)

  • When the module's own dependencyManagement sets the scope, Coana rewrites both versions in place.
  • When an external parent sets it, the dependency marked direct gets a <dependencies> entry and stays on the runtime classpath. The transitive one gets a dependencyManagement entry without <scope>, which replaces the parent's compile and moves it off the runtime classpath. That is Coana's existing behaviour for any dependency whose managed scope comes from outside, with or without this change.

Tests: a new compat smoke test, smoke-test-excluded-scope.sh, passes on Maven 3.2.5, 3.6.3, 3.8.8, 3.9.9 and 4.0.0-rc-6, and the full Maven matrix passes. The v1.x jar fails it, missing managed and deep. So does a first version that linked every kept node through skipped ones: it marked shared and e direct and added a lib -> e edge.

🤖 Generated with Claude Code

With --exclude-configs or --include-configs, the Maven facts extension
cut off the whole subtree of a dependency whose scope it skips. Maven
filters each node by its own scope and still walks past the nodes it
drops, and a dependencyManagement scope is not derived from the
parent's. A dependency managed to compile below a test dependency is on
Maven's runtime classpath and in a packaged war, but was missing from
the facts.

The extension now walks the whole resolved graph and drops skipped
nodes afterwards, keeping the edges Maven resolved between kept nodes.
A kept node reached only through skipped ones is linked to its nearest
kept ancestors, and is direct when the module is one of them; no other
node gets an edge Maven did not resolve.
@cursor

cursor Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes Maven dependency graph assembly used for SBOMs and scans; behavior is regression-tested across the Maven compat matrix but incorrect edges could affect downstream fix and reachability logic.

Overview
When Maven Socket facts run with --exclude-configs or --include-configs, dependencies whose own scope still passes were dropped if they only appeared under an excluded parent. The Maven extension now walks the full resolved graph, marks scope-filtered nodes as skipped, then dropSkipped removes them while preserving kept nodes and rewiring edges so compile-scoped artifacts Maven still ships (for example dependencyManagement overrides below a test dependency) stay in the facts with correct parent links and direct flags.

A Maven compat regression (smoke-test-excluded-scope.sh with fixture excluded-scope/pom.xml) is added to the local matrix in run-compat.sh, and CHANGELOG documents the fix.

Reviewed by Cursor Bugbot for commit 96bad72. Configure here.

@jfblaa

Copy link
Copy Markdown
Contributor Author

[agent] Benjamin Barslev Nielsen (@barslev), one open question for you on the socket fix side.

With a scope excluded, the facts drop the excluded dependencies but keep the nodes Maven keeps below them. An excluded parent can still constrain a kept child, for example by declaring a version range for it. Facts carry resolved versions only, so that constraint never reaches depscan's fix computation, which works from the chain metadata stored in its backend.

Our current thinking is to catch it with a validation step in Coana rather than changing the fix computation. Coana would check each Maven upgrade before or after applying it, possibly by re-resolving the patched POMs with Maven. That check would also catch REA-1166, where a managed entry without a <scope> moves a dependency off the runtime classpath. Would you rather augment the fix computation instead?

We're also checking whether a range in some other dependency's dependencyManagement can reach the same artifact. Findings will follow here.

@jfblaa

Copy link
Copy Markdown
Contributor Author

[agent] Findings on version ranges, as promised above. Checked in source and by experiment on Maven 3.2.5, 3.9.9 and 4.0.0-rc-6.

  • A range on a test dependency limits the version that ships. Every range in a conflict group is a hard constraint, whatever its scope (NearestVersionSelector; ConfigurableVersionSelector in resolver 2.x). With testlib (test) declaring X [1.0,1.5] and a compile dependency q wanting X 2.0, Maven resolves X 1.5, and 1.5 is what lands in WEB-INF/lib. A direct <version>2.0</version> in the module also loses to the range. This holds with or without --exclude-configs. With the test scope excluded, the facts show X 1.5 under q but no longer show testlib, the reason it's 1.5.
  • A managed version replaces ranges. A dependencyManagement version on the module is applied before ranges are resolved, so a fix written as a managed entry takes effect whatever the excluded dependency declares. The excluded dependency then runs against a version outside its declared range, which is a compatibility risk rather than a resolution failure.
  • Rewriting a plain version fails silently when the target lies outside such a range: the module's direct <version>, or a parent's declaration. Maven keeps resolving the range's maximum.
  • Unrelated subtrees: a dependency's own dependencyManagement never reaches X outside its own subtree. In Maven 4 (transitive management on by default) it does reach X anywhere below it, and a managed range there constrains X across the whole graph. In Maven 3 it only fills versions for that dependency's direct children.

So the constraint the facts lose matters only for fixes that rewrite a plain version. A post-fix re-resolution that checks the target version is now resolved catches exactly that case.

… scope

A range is a hard constraint whatever the scope declaring it, so an
excluded test dependency's range still picks the version Maven ships.
The kept dependency must keep its edge from the compile dependency
whose own version lost the conflict, not become an orphan.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant