RemoveDuplicateDependencies: preserve Maven's effective dependency model - #8446
Draft
martinfrancois wants to merge 6 commits into
Draft
Conversation
The recipe kept the first of a set of duplicate declarations and deleted the later ones, which is not how Maven builds the effective model. In <dependencies> the last declaration is the effective one, so deleting it changed the resolved version, optionality or exclusions. In <dependencyManagement> duplicates merge field-wise, each field coming from the first declaration that sets it while exclusions accumulate across all of them, so a later declaration can be the only source of the managed version. A repeated BOM import at another version can likewise manage entries the first import does not. Duplicates are now resolved over the whole <dependencies> or <dependencyManagement> list instead of one tag at a time. A direct duplicate keeps the later declaration in the position of the first, so the resolved dependency order is unchanged, and declarations are compared with properties resolved so that a version written through a property still collapses onto an identical literal one. A managed duplicate is removed only when it sets no field the earlier declarations leave unset and carries no exclusion they do not already carry; a repeated BOM import only when it resolves to the same version. Two things for review: the existing test removeDependencyWithDifferentVersion expected the earlier version to survive and now expects the later one, and the recipe now leaves differing managed duplicates in place rather than collapsing them, giving up removals it used to make in order to keep the effective model intact.
…ntary A BOM import declared through a property built its key from raw tag text while the classifier and type guard compared resolved values, so it was never recognised as a duplicate.
This was referenced Aug 11, 2026
…dependencies-keep-maven-winner
…keep-maven-winner' into fix/remove-duplicate-dependencies-keep-maven-winner # Conflicts: # rewrite-maven/src/main/resources/META-INF/rewrite/recipes.csv
martinfrancois
force-pushed
the
fix/remove-duplicate-dependencies-keep-maven-winner
branch
from
August 16, 2026 20:17
285f823 to
7b8da6d
Compare
martinfrancois
marked this pull request as draft
August 17, 2026 08:08
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Suggested review order: 21 of 52 (Score: 6)
Review first: openrewrite/rewrite-testing-frameworks#1095
What's changed?
RemoveDuplicateDependenciesnow keeps the declaration Maven resolves to, rather than always the first one.In
<dependencies>the last declaration wins as a whole - with its<version>, its<optional>flag and its<exclusions>.ResolvedPom.doResolveDependenciessays so in a comment (// For direct dependencies that are duplicated, last declaration wins), matching Maven's ownDefaultModelNormalizer#mergeDuplicates. For duplicate declarations, losing the later declaration's<exclusions>is the damaging case, since those are the exclusions Maven was honouring, so the excluded transitive dependency comes back into the build.Content and position are now decided separately: the surviving tag holds the later declaration's children, at the earlier declaration's list position and leading whitespace. That is the position the resolved model already gives the dependency, since
rootDependenciesis aLinkedHashMapwhere a secondputreplaces the value but keeps the key's place. The two are compared by the values Maven reads, with properties resolved, so a duplicate that only spells a version out where the other reaches it through a property leaves the earlier tag as written.<dependencyManagement>merges by a different rule: each field takes the value of the first declaration that sets it, and exclusions accumulate across all of them. A later managed duplicate is removed only when it sets no field an earlier one leaves unset and carries no exclusion an earlier one lacks; otherwise all declarations stay. Declarations are never merged into one tag. A repeated BOM import counts as a duplicate only at the same version, since the first import wins for the entries both manage while the second may manage entries the first does not.What's your motivation?
Recipe:
org.openrewrite.maven.RemoveDuplicateDependencies.Before
Actual after the recipe
Expected after the recipe
Anything in particular you'd like reviewers to focus on?
One existing expectation changed.
removeDependencyWithDifferentVersiondeclarescom.google.inject:guiceas4.2.1then4.2.2and expected4.2.1to survive; it now expects4.2.2, which is what Maven resolves.Three limits:
<exclusions>block belonging to the earlier of two duplicates in<dependencies>. That is intended: Maven was already ignoring it.<scope>are still left alone, as on main, because scope is part ofDependencyKey. Recorded as a limit, not changed here.removeDuplicatesbuilds the replacement child list by hand rather than withListUtils, because a later duplicate has to replace an element at an earlier index. It copies into a new list and mutates no existing LST node.The recipe description gained a sentence, since the recipe no longer only removes tags, and
recipes.csvwas regenerated with./gradlew :rewrite-maven:recipeCsvGenerate.Have you considered any alternatives or workarounds?
For the survivor's position: delete the earlier declaration and leave the later one where it was written. That keeps the later declaration's own indentation and any comment above it, but moves the dependency down the list. The change is contained in
removeDuplicates- say the word and I will make it.Any additional context
Pre-existing tests changed:
RemoveDuplicateDependenciesTest.java.removeDependencyWithDifferentVersion(updated).Brings
RemoveDuplicateDependenciesTestfrom 13 to 25 tests. Ten fail without the code change, covering a later declaration carrying the<version>,<optional>or<exclusions>Maven honours; the survivor's position, indentation and comments; a managed duplicate that contributes a field or exclusion to the effective entry; and a repeated BOM import at a different version, or declared through a property.This change was prepared with AI assistance (Claude Code). I reviewed the code, the tests and this description.
Why this still warrants a recipe fix
A duplicate dependency is already a POM defect and SHOULD be reviewed by the project. That does not make it safe for a cleanup recipe to keep Maven's losing declaration. A user who trusts the recipe to remove the duplicate receives a valid-looking POM whose effective dependency, exclusions, or optional flag differs from the model Maven resolved before the rewrite. The recipe MUST preserve Maven's deterministic last-direct-duplicate semantics while removing the ambiguity.
Checklist
./gradlew buildlocally, and committed any resulting changes torecipes.csv