Skip to content

fix(manifest): record every resolved Maven dependency edge - #1711

Merged
Jeppe Fredsgaard Blaabjerg (jfblaa) merged 2 commits into
v1.xfrom
jfblaa/maven-complete-edges
Nov 12, 2026
Merged

Jeppe Fredsgaard Blaabjerg (jfblaa) merged 2 commits into
v1.xfrom
jfblaa/maven-complete-edges

Conversation

@jfblaa

@jfblaa Jeppe Fredsgaard Blaabjerg (jfblaa) commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Heads up, reviewer

We have been missing maven edges in dynamic SBOMs as the APIs for some reason only report dependencies as children of the first parent in the tree/graph rather than all the parents.


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

Maven Socket facts now include every dependency edge Maven resolves, as Gradle and sbt facts already do. Before, an edge onto a dependency Maven kept elsewhere was dropped: jetty-io -> jetty-util when jetty-util is also a direct dependency, or an edge Maven mediated to another version. Dependency chains therefore missed paths to a package.

What changes

  • The extension wraps Maven's own graph transformer, records each node's children before conflict resolution, and points every edge at the node Maven kept for that conflict. No version choice is made outside Maven.
  • Roots are shaped like Gradle's runtime and test classpaths: the test root is the module's whole resolution, the compile root the nodes Maven kept with a prod scope. An edge from a prod node onto a provided one is kept, and the provided dependency stays dev.
  • Prod/dev now follows the scope Maven kept a node with, replacing the reachability split from fix(manifest): keep Maven components reachable from their root #1549. A dependency Maven widens to compile under a test parent is prod, as it is on Maven's runtime classpath.
  • A module whose conflict resolution fails (unsatisfiable hard version ranges) is reported unscannable and emits and fetches nothing. It used to emit the half-resolved candidates, including several versions of one package.

No facts format, assembler or Coana change.

Verification

  • Maven compat matrix 3.2.5, 3.6.3, 3.8.8, 3.9.9, 4.0.0-rc-6 green. New fixture cases (edge onto a direct dependency, mediated edge, prod onto provided, unsolvable conflict) all fail with the v1.x extension.
  • Records are identical with and without aether.conflictResolver.verbose.
  • socket fix --dynamic-sbom-inference on a Maven reactor with a vulnerable package reached both directly and transitively gives the same fix as before.

🤖 Generated with Claude Code

Maven facts only had the edges that survive conflict resolution, so an
edge onto a dependency Maven kept elsewhere (a direct dependency, or a
mediated version) was missing, unlike Gradle and sbt facts. The Maven
extension now records each node's children before conflict resolution
and points every edge at the node Maven kept.

Roots follow Gradle's runtime and test classpaths: the test root is the
module's whole resolution, the compile root the nodes Maven kept with a
prod scope. A dependency Maven widens to compile under a test parent is
therefore prod, matching Maven's runtime classpath.

A module whose conflict resolution fails has no resolved graph, so it is
reported unscannable and contributes no dependencies.
@cursor

cursor Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes core Maven dependency graph emission used by reachability and dynamic SBOM inference; behavior shifts for scope conflicts and failed resolution, though compat matrix and assertion tests guard regressions.

Overview
Maven Socket facts now record the full resolved dependency graph and classify prod vs dev like Gradle/sbt, aligning reachability and SBOM inference with Maven’s actual classpath.

The Maven extension wraps resolution with an EdgeRecorder that snapshots children before conflict resolution and rewrites edges to the winner Maven kept. That restores edges that were previously dropped (duplicate direct deps, mediated versions, prod → provided). Compile/test roots are reshaped: compile holds nodes Maven kept with a prod scope; test holds the full resolution with all edges.

Prod/dev labeling no longer follows reachability from direct prod deps—it uses the effective scope Maven kept on each node (so compile-scoped artifacts under a test parent count as prod). Modules where conflict resolution fails entirely are marked unscannable with no partial nodes or jar fetches.

Coverage adds compat smoke cases (dup-io/dup-util, mediated edges, container-lib → provided API, unsolvable conflicts) and expands assert-records.py / Vitest fixtures.

Reviewed by Cursor Bugbot for commit 25c8a4f. Configure here.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[agent] A simplification review (reuse, simplification, efficiency, altitude), not a bug hunt. The approach is at the right depth: recording edges fixes what the reachability-based prod/dev split was working around, the new root layout matches Gradle's runtime/test classpaths, and the assembler needs no Maven-specific code. The inline comments are mostly about how much machinery EdgeRecorder needs.

Looked at and not flagged:

  • The copies map: it only saves memory, but it keeps the pre-walk linear.
  • The per-module session copy: microseconds.
  • Prod records appearing in both roots: this is the intended Gradle-shaped contract. Buffering every record until the run ends predates this PR, though the PR doubles what that buffer holds.
  • Follow-up idea, older than this PR: store a sibling module's jar and test-jar under their own ids and let the assembler collapse them, the way it does for Gradle variants. That would remove the jar/test-jar caveats in visit.
  • Dead code: none. NODE_DATA_CONFLICT_WINNER, ArrayDeque and Deque are all still used.

The new unsolvable-conflict smoke test follows the existing one-script-per-failure-fixture pattern.

@mtorp Martin Torp (mtorp) left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Holding for a pending review

Build the resolved edges inside the transformer, so a broken assumption
about Maven's graph becomes a collection error that marks only that
module unscannable. A module whose graph never passed through the
recorder is now reported instead of emitting nothing silently.

Walk each artifact once again before touching its node, leave self-edges
to the assembler, and exclude libdep from mid's lib dependency so the
fixture catches a walk keyed by the shared jar/test-jar id.

@mtorp Martin Torp (mtorp) left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM ✅

@jfblaa
Jeppe Fredsgaard Blaabjerg (jfblaa) merged commit 7bd93ab into v1.x Nov 12, 2026
17 checks passed
@jfblaa
Jeppe Fredsgaard Blaabjerg (jfblaa) deleted the jfblaa/maven-complete-edges branch November 12, 2026 12:01
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.

3 participants