From 22ca6c7730621799f0771ce9917f4389a970fbc6 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Mon, 17 Aug 2026 20:27:18 +0200 Subject: [PATCH 1/2] Keep dependencies that constrain resolution in `RemoveRedundantDependencies` A Gradle declaration such as implementation('org.yaml:snakeyaml') { version { prefer '2.0' strictly '[2.0,2.1)' } } carries no version in its coordinate and exists to pin resolution, typically above what an imported BOM manages. `isRedundant` compared only groupId, artifactId, resolved version and declared exclusions, so it saw the artifact supplied transitively at the very version the constraint had forced and deleted the whole block, handing the version back to the rest of the graph. Observed on Netflix/conductor, where the Spring Boot 2.7.3 BOM manages snakeyaml 1.30 and jackson-bom 2.13.3 and these blocks are what lift them to 2.0 and 2.15.x. Removal was also partial, dropping the jackson-core and jackson-annotations pins while keeping the jackson-databind one, which cannot resolve to a working Jackson classpath. Skip any candidate whose requested version is a range or dynamic version. The strict range surfaces in `getRequested().getVersion()`, so matching on version syntax rather than on requested-differs-from-resolved keeps versionless BOM-managed declarations removable and does not trip over Maven `${...}` placeholders. Maven ranges use the same syntax, so both ecosystems are covered. --- .../RemoveRedundantDependencies.java | 18 ++++++++++- .../resources/META-INF/rewrite/recipes.csv | 2 +- .../RemoveRedundantDependenciesTest.java | 31 +++++++++++++++++++ 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/openrewrite/java/dependencies/RemoveRedundantDependencies.java b/src/main/java/org/openrewrite/java/dependencies/RemoveRedundantDependencies.java index 43e936f2..59a24b2e 100644 --- a/src/main/java/org/openrewrite/java/dependencies/RemoveRedundantDependencies.java +++ b/src/main/java/org/openrewrite/java/dependencies/RemoveRedundantDependencies.java @@ -52,7 +52,9 @@ public class RemoveRedundantDependencies extends ScanningRecipe declaredExclusions(ResolvedDependency dep) { return exclusions == null || exclusions.isEmpty() ? emptySet() : new HashSet<>(exclusions); } + // A declaration whose requested version is a range or a dynamic version exists to constrain resolution rather + // than to supply the artifact; a Gradle `version { strictly '[2.0,2.1)' }` block surfaces here as the range. + // Removing it silently hands the version back to whatever the rest of the graph, or an imported BOM, asks for, + // which is precisely what the constraint was written to prevent. + private static boolean constrainsResolution(ResolvedDependency dep) { + String requestedVersion = dep.getRequested().getVersion(); + return requestedVersion != null && + (requestedVersion.startsWith("[") || requestedVersion.startsWith("(") || + requestedVersion.endsWith("+") || requestedVersion.startsWith("latest.")); + } + private static List withMavenCentral(List repositories) { List effectiveRepos = new ArrayList<>(repositories); if (effectiveRepos.stream().noneMatch(r -> r.getUri().contains("repo.maven.apache.org") || @@ -286,6 +299,9 @@ public TreeVisitor getVisitor(Accumulator acc) { } private boolean isRedundant(ResolvedDependency dep, Set transitives) { + if (constrainsResolution(dep)) { + return false; + } Set depExclusions = declaredExclusions(dep); for (TransitiveDependency transitive : transitives) { ResolvedGroupArtifactVersion gav = transitive.getGav(); diff --git a/src/main/resources/META-INF/rewrite/recipes.csv b/src/main/resources/META-INF/rewrite/recipes.csv index 4c54eb4e..3f51f310 100644 --- a/src/main/resources/META-INF/rewrite/recipes.csv +++ b/src/main/resources/META-INF/rewrite/recipes.csv @@ -15,7 +15,7 @@ maven,org.openrewrite.recipe:rewrite-java-dependencies,org.openrewrite.java.depe This recipe makes no changes to any source file by default. Add `changeDependencies=true` to change dependencies, but note that you might need to run additional recipes to update imports and adopt other breaking changes.",1,,Dependencies,Java,,,Basic building blocks for transforming Java code.,"[{""name"":""changeDependencies"",""type"":""Boolean"",""displayName"":""Change dependencies"",""description"":""Whether to change dependencies to their relocated groupId and artifactId.""}]","[{""name"":""org.openrewrite.java.dependencies.table.RelocatedDependencyReport"",""displayName"":""Relocated dependencies"",""instanceName"":""Relocated dependencies"",""description"":""A list of dependencies in use that have relocated."",""columns"":[{""name"":""dependencyGroupId"",""type"":""String"",""displayName"":""Dependency group id"",""description"":""The Group ID of the dependency in use.""},{""name"":""dependencyArtifactId"",""type"":""String"",""displayName"":""Dependency artifact id"",""description"":""The Artifact ID of the dependency in use.""},{""name"":""relocatedGroupId"",""type"":""String"",""displayName"":""Relocated dependency group id"",""description"":""The Group ID of the relocated dependency.""},{""name"":""relocatedArtifactId"",""type"":""String"",""displayName"":""Relocated ependency artifact id"",""description"":""The Artifact ID of the relocated dependency.""},{""name"":""context"",""type"":""String"",""displayName"":""Context"",""description"":""Context for the relocation, if any.""}]}]" maven,org.openrewrite.recipe:rewrite-java-dependencies,org.openrewrite.java.dependencies.RemoveDependency,Remove a Gradle or Maven dependency,"For Gradle project, removes a single dependency from the dependencies section of the `build.gradle`. For Maven project, removes a single dependency from the `` section of the pom.xml.",1,,Dependencies,Java,,,Basic building blocks for transforming Java code.,"[{""name"":""groupId"",""type"":""String"",""displayName"":""Group ID"",""description"":""The first part of a dependency coordinate `com.google.guava:guava:VERSION`. This can be a glob expression."",""example"":""com.fasterxml.jackson*"",""required"":true},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact ID"",""description"":""The second part of a dependency coordinate `com.google.guava:guava:VERSION`. This can be a glob expression."",""example"":""jackson-module*"",""required"":true},{""name"":""unlessUsing"",""type"":""String"",""displayName"":""Unless using"",""description"":""Do not remove if type is in use. Supports glob expressions."",""example"":""org.aspectj.lang.*""},{""name"":""configuration"",""type"":""String"",""displayName"":""The dependency configuration"",""description"":""The dependency configuration to remove from."",""example"":""api""},{""name"":""scope"",""type"":""String"",""displayName"":""Scope"",""description"":""Only remove dependencies if they are in this scope. If 'runtime', this willalso remove dependencies in the 'compile' scope because 'compile' dependencies are part of the runtime dependency set"",""example"":""compile"",""valid"":[""compile"",""test"",""runtime"",""provided""]}]", -maven,org.openrewrite.recipe:rewrite-java-dependencies,org.openrewrite.java.dependencies.RemoveRedundantDependencies,Remove redundant explicit dependencies,"Remove explicit dependencies that are already provided transitively by a specified dependency. This recipe downloads and resolves the parent dependency's POM to determine its true transitive dependencies, allowing it to detect redundancies even when both dependencies are explicitly declared. A direct dependency is only removed when the transitive one provides it at the exact same scope and with the same declared exclusions, so that removing it does not change the effective classpath.",1,,Dependencies,Java,,,Basic building blocks for transforming Java code.,"[{""name"":""groupId"",""type"":""String"",""displayName"":""Group ID"",""description"":""The first part of a dependency coordinate `com.google.guava:guava:VERSION` of the parent dependency. This can be a glob expression."",""example"":""com.fasterxml.jackson.core"",""required"":true},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact ID"",""description"":""The second part of a dependency coordinate `com.google.guava:guava:VERSION` of the parent dependency. This can be a glob expression."",""example"":""jackson-databind"",""required"":true}]", +maven,org.openrewrite.recipe:rewrite-java-dependencies,org.openrewrite.java.dependencies.RemoveRedundantDependencies,Remove redundant explicit dependencies,"Remove explicit dependencies that are already provided transitively by a specified dependency. This recipe downloads and resolves the parent dependency's POM to determine its true transitive dependencies, allowing it to detect redundancies even when both dependencies are explicitly declared. A direct dependency is only removed when the transitive one provides it at the exact same scope and with the same declared exclusions, so that removing it does not change the effective classpath. Declarations that constrain resolution through a version range or dynamic version, such as a Gradle `version { strictly ... }` block, are never removed.",1,,Dependencies,Java,,,Basic building blocks for transforming Java code.,"[{""name"":""groupId"",""type"":""String"",""displayName"":""Group ID"",""description"":""The first part of a dependency coordinate `com.google.guava:guava:VERSION` of the parent dependency. This can be a glob expression."",""example"":""com.fasterxml.jackson.core"",""required"":true},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact ID"",""description"":""The second part of a dependency coordinate `com.google.guava:guava:VERSION` of the parent dependency. This can be a glob expression."",""example"":""jackson-databind"",""required"":true}]", maven,org.openrewrite.recipe:rewrite-java-dependencies,org.openrewrite.java.dependencies.UpgradeDependencyVersion,Upgrade Gradle or Maven dependency versions,"For Gradle projects, upgrade the version of a dependency in a `build.gradle` file. Supports updating dependency declarations of various forms: * `String` notation: `""group:artifact:version""` * `Map` notation: `group: 'group', name: 'artifact', version: 'version'` diff --git a/src/test/java/org/openrewrite/java/dependencies/RemoveRedundantDependenciesTest.java b/src/test/java/org/openrewrite/java/dependencies/RemoveRedundantDependenciesTest.java index 787c55ee..bfb9e298 100644 --- a/src/test/java/org/openrewrite/java/dependencies/RemoveRedundantDependenciesTest.java +++ b/src/test/java/org/openrewrite/java/dependencies/RemoveRedundantDependenciesTest.java @@ -781,6 +781,37 @@ void removeRedundantGradleDependency() { ); } + @Test + void doNotRemoveGradleDependencyDeclaringVersionConstraint() { + rewriteRun( + spec -> spec.beforeRecipe(withToolingApi()) + .recipe(new RemoveRedundantDependencies( + "com.fasterxml.jackson.core", "jackson-databind")), + mavenProject("my-app", + //language=groovy + buildGradle( + """ + plugins { + id 'java-library' + } + repositories { + mavenCentral() + } + dependencies { + implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.0' + implementation('com.fasterxml.jackson.core:jackson-core') { + version { + prefer '2.17.0' + strictly '[2.17.0,2.18.0)' + } + } + } + """ + ) + ) + ); + } + @Test void globGroupIdMatchesProvider() { rewriteRun( From 16e1c2f7feb2a3c29092099deacb2acfaf0cf59c Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Mon, 17 Aug 2026 20:33:12 +0200 Subject: [PATCH 2/2] Drop the comment above constrainsResolution --- .../java/dependencies/RemoveRedundantDependencies.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/java/org/openrewrite/java/dependencies/RemoveRedundantDependencies.java b/src/main/java/org/openrewrite/java/dependencies/RemoveRedundantDependencies.java index 59a24b2e..a265b41d 100644 --- a/src/main/java/org/openrewrite/java/dependencies/RemoveRedundantDependencies.java +++ b/src/main/java/org/openrewrite/java/dependencies/RemoveRedundantDependencies.java @@ -199,10 +199,6 @@ private static Set declaredExclusions(ResolvedDependency dep) { return exclusions == null || exclusions.isEmpty() ? emptySet() : new HashSet<>(exclusions); } - // A declaration whose requested version is a range or a dynamic version exists to constrain resolution rather - // than to supply the artifact; a Gradle `version { strictly '[2.0,2.1)' }` block surfaces here as the range. - // Removing it silently hands the version back to whatever the rest of the graph, or an imported BOM, asks for, - // which is precisely what the constraint was written to prevent. private static boolean constrainsResolution(ResolvedDependency dep) { String requestedVersion = dep.getRequested().getVersion(); return requestedVersion != null &&