openrewrite / openrewrite/rewrite

Maven 4 transitive dependencyManagement changes resolved versions vs. rewrite-maven

Open
#8,347 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

maven
Dominant language
Java
Stars
3.7k
Forks
570
Avg merge
13h 12m
Merged PRs (30d)
261

Description

Problem

Maven 4 changed which <dependencyManagement> sections affect transitive dependency versions, and rewrite-maven follows the Maven 3 rule. On a project built with Maven 4, our LST can therefore record different transitive versions than the build actually resolves.

  • Maven 3 wires ClassicDependencyManager — only the root/current project's dependencyManagement contributes managed versions. dependencyManagement declared in intermediate dependency POMs is ignored.
  • Maven 4 wires TransitiveDependencyManager by default — dependencyManagement is collected at every depth and applied from depth ≥ 2, so an intermediate POM's dependencyManagement now governs its own transitives. Nearer-to-root still wins on conflict.

Verified divergence

Four synthetic artifacts, installed to a local repo:

dm-c:1.0, dm-c:2.0        leaf, no dependencies
dm-b:1.0                  -> dm-c:1.0
dm-a:1.0                  -> dm-b:1.0, and <dependencyManagement> pins dm-c to 2.0
dm-app:1.0                -> dm-a:1.0        (no dependencyManagement of its own)

Resolving dm-app, same POMs, same local repository:

Resolver dm-c resolves to
Maven 3.9.16 dependency:tree 1.0
Maven 4.0.0-rc-5 dependency:tree 2.0
rewrite-maven MavenResolutionResult 1.0
# Maven 3.9.16
io.example.dmtest:dm-app:jar:1.0
\- io.example.dmtest:dm-a:jar:1.0:compile
   \- io.example.dmtest:dm-b:jar:1.0:compile
      \- io.example.dmtest:dm-c:jar:1.0:compile

# Maven 4.0.0-rc-5 (no flags)
io.example.dmtest:dm-app:jar:1.0
\- io.example.dmtest:dm-a:jar:1.0:compile
   \- io.example.dmtest:dm-b:jar:1.0:compile
      \- io.example.dmtest:dm-c:jar:2.0:compile

This is the same shape as Maven's own integration test mng-7982-transitive-dependency-management (MavenITmng7982DependencyManagementTransitivityTest).

Scope is narrower than it first looks

A control with the same dependency graph, but the dm-c pin moved to the root project's dependencyManagement, resolves to 2.0 under both Maven 3 and Maven 4. So:

  • Root-project dependencyManagement and imported BOMs — the common case — are unaffected.
  • Only dependencyManagement declared in intermediate/transitive POMs diverges.
  • Managed scope and optional are still root-only in Maven 4 (isInheritedDerived() returns depth == 0); only version, exclusions and localPath became transitive.

Where we sit today

ResolvedPom.getValues(Dependency, int depth) (~line 1313):

if (d.getVersion() == null || depth > 0) {
    // dependency management overrides transitive dependency versions
    version = getManagedVersion(d.getGroupId(), d.getArtifactId(), d.getType(), d.getClassifier());

getManagedVersion reads ResolvedPom.this.dependencyManagement — the root project's resolved management, including its parents and imported BOMs. Intermediate POMs' management is never consulted. That is ClassicDependencyManager, and it is correct for Maven 3.

The design question

This isn't a straightforward bug fix, because nothing in the POM tells us which resolver a project uses. A modelVersion 4.0.0 POM builds fine under Maven 4, so model version is not a signal. Options, roughly:

  1. Keep Maven 3 semantics — correct today, increasingly wrong as Maven 4 adoption grows.
  2. Switch to Maven 4 semantics — wrong for the large majority still on Maven 3.
  3. Make it configurable on MavenExecutionContextView, defaulting to Maven 3 for now.
  4. Infer from the project: .mvn/wrapper/maven-wrapper.properties distributionUrl, enforcer requireMavenVersion, or .mvn/maven.config. Best-effort and fallible, but it is real signal, and the parser already reads .mvn/-adjacent files in places.

Worth noting for whichever route: Maven 4 users can opt back into the old behavior with -Dmaven.resolver.dependencyManagerTransitivity=false (or .mvn/maven.config), so "is this project on Maven 4" isn't sufficient on its own either.

Impact

Silent. Nothing errors — we just record a different version than the build uses. That feeds anything reading transitive versions: UpgradeTransitiveDependencyVersion, dependency insight/vulnerability searches, and dependency-graph reporting.

Low blast radius today given Maven 4 adoption, which is why I'd rather we pick a direction deliberately than discover it through a support ticket.

Related

  • apache/maven#12302 — the user-visible symptom upstream (enforcer RequireUpperBoundDeps failures from downgraded transitives). Closed, but the fix PR apache/maven#12316 was closed unmerged and the behavior is unchanged as of 4.0.0-rc-6.
  • Maven 4.0.0-rc-6 release notes call this out under "Transitive dependency resolution changes".
  • #6869, #8345 — other Maven 4 gaps, unrelated code paths.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start at ResolvedPom.getValues(Dependency, int depth) around line 1313 and its getManagedVersion call; compare that path with Maven's mng-7982-transitive-dependency-management test and the Maven 3/4 synthetic graph described here. First determine the resolver-selection or configuration behavior; done means an agreed approach is covered by tests for intermediate-POM management without changing root-project or imported-BOM behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
java
Domain
build-system
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Needs clarification
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.