From 885c72b1c4cbacaf7594e91466b4974323325fb6 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Tue, 23 Dec 2025 22:52:40 +0100 Subject: [PATCH 1/9] Add RemoveRedundantDependencies recipe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new recipe that removes direct dependencies which are already provided transitively by another dependency matching a specified glob pattern. Only removes when the transitive version exactly matches. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../RemoveRedundantDependencies.java | 310 ++++++++++++++++++ .../RemoveRedundantDependenciesTest.java | 195 +++++++++++ 2 files changed, 505 insertions(+) create mode 100644 src/main/java/org/openrewrite/java/dependencies/RemoveRedundantDependencies.java create mode 100644 src/test/java/org/openrewrite/java/dependencies/RemoveRedundantDependenciesTest.java diff --git a/src/main/java/org/openrewrite/java/dependencies/RemoveRedundantDependencies.java b/src/main/java/org/openrewrite/java/dependencies/RemoveRedundantDependencies.java new file mode 100644 index 0000000..4e51537 --- /dev/null +++ b/src/main/java/org/openrewrite/java/dependencies/RemoveRedundantDependencies.java @@ -0,0 +1,310 @@ +/* + * Copyright 2024 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.java.dependencies; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.jspecify.annotations.Nullable; +import org.openrewrite.*; +import org.openrewrite.gradle.marker.GradleDependencyConfiguration; +import org.openrewrite.gradle.marker.GradleProject; +import org.openrewrite.internal.StringUtils; +import org.openrewrite.maven.tree.MavenResolutionResult; +import org.openrewrite.maven.tree.ResolvedDependency; +import org.openrewrite.maven.tree.ResolvedGroupArtifactVersion; +import org.openrewrite.maven.tree.Scope; + +import java.util.*; + +@EqualsAndHashCode(callSuper = false) +@Value +public class RemoveRedundantDependencies extends ScanningRecipe { + + @Option(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") + String groupId; + + @Option(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") + String artifactId; + + @Option(displayName = "Scope", + description = "Only remove redundant dependencies from the specified Maven scope. If not specified, all scopes are considered.", + valid = {"compile", "runtime", "provided", "test"}, + example = "compile", + required = false) + @Nullable + String scope; + + @Option(displayName = "Configuration", + description = "Only remove redundant dependencies from the specified Gradle configuration. If not specified, all configurations are considered.", + example = "implementation", + required = false) + @Nullable + String configuration; + + @Override + public String getDisplayName() { + return "Remove redundant explicit dependencies"; + } + + @Override + public String getDescription() { + return "Remove explicit dependencies that are already provided transitively by a specified dependency. " + + "Note: This recipe works best when the redundant dependency is not also explicitly declared elsewhere. " + + "Due to how dependency resolution works, if a dependency is declared directly, it may not appear " + + "in the transitive list of the parent dependency."; + } + + @Value + public static class Accumulator { + // Map from project identifier -> scope/configuration -> Set of transitive GAVs + Map>> transitivesByProjectAndScope; + } + + @Override + public Accumulator getInitialValue(ExecutionContext ctx) { + return new Accumulator(new HashMap<>()); + } + + @Override + public TreeVisitor getScanner(Accumulator acc) { + return new TreeVisitor() { + @Override + public @Nullable Tree visit(@Nullable Tree tree, ExecutionContext ctx) { + if (tree == null) { + return null; + } + + tree.getMarkers().findFirst(GradleProject.class).ifPresent(gradle -> { + String projectId = gradle.getGroup() + ":" + gradle.getName(); + for (GradleDependencyConfiguration conf : gradle.getConfigurations()) { + if (configuration != null && !configuration.equals(conf.getName())) { + continue; + } + for (ResolvedDependency dep : conf.getResolved()) { + if (dep.getDepth() == 0 && + StringUtils.matchesGlob(dep.getGroupId(), groupId) && + StringUtils.matchesGlob(dep.getArtifactId(), artifactId)) { + // This is a matching parent dependency, collect its transitives + Set transitives = acc.transitivesByProjectAndScope + .computeIfAbsent(projectId, k -> new HashMap<>()) + .computeIfAbsent(conf.getName(), k -> new HashSet<>()); + collectTransitives(dep, transitives); + } + } + } + }); + + tree.getMarkers().findFirst(MavenResolutionResult.class).ifPresent(maven -> { + String projectId = maven.getPom().getGroupId() + ":" + maven.getPom().getArtifactId(); + + for (Map.Entry> entry : maven.getDependencies().entrySet()) { + Scope depScope = entry.getKey(); + if (scope != null && !scope.equalsIgnoreCase(depScope.name())) { + continue; + } + for (ResolvedDependency dep : entry.getValue()) { + if (dep.getDepth() == 0 && + StringUtils.matchesGlob(dep.getGroupId(), groupId) && + StringUtils.matchesGlob(dep.getArtifactId(), artifactId)) { + // This is a matching parent dependency, collect its transitives + Set transitives = acc.transitivesByProjectAndScope + .computeIfAbsent(projectId, k -> new HashMap<>()) + .computeIfAbsent(depScope.name().toLowerCase(), k -> new HashSet<>()); + collectTransitives(dep, transitives); + } + } + } + }); + + return tree; + } + + private void collectTransitives(ResolvedDependency dep, Set transitives) { + for (ResolvedDependency transitive : dep.getDependencies()) { + if (transitives.add(transitive.getGav())) { + collectTransitives(transitive, transitives); + } + } + } + }; + } + + @Override + public TreeVisitor getVisitor(Accumulator acc) { + return new TreeVisitor() { + @Override + public @Nullable Tree visit(@Nullable Tree tree, ExecutionContext ctx) { + if (!(tree instanceof SourceFile)) { + return tree; + } + + SourceFile sf = (SourceFile) tree; + Tree result = sf; + + // Handle Gradle + Optional gradleOpt = sf.getMarkers().findFirst(GradleProject.class); + if (gradleOpt.isPresent()) { + GradleProject gradle = gradleOpt.get(); + String projectId = gradle.getGroup() + ":" + gradle.getName(); + Map> scopeToTransitives = + acc.transitivesByProjectAndScope.getOrDefault(projectId, Collections.emptyMap()); + + for (GradleDependencyConfiguration conf : gradle.getConfigurations()) { + if (configuration != null && !configuration.equals(conf.getName())) { + continue; + } + Set transitives = getCompatibleTransitives( + scopeToTransitives, conf.getName(), true); + if (transitives.isEmpty()) { + continue; + } + + for (ResolvedDependency dep : conf.getResolved()) { + if (dep.getDepth() == 0 && + isRedundantDependency(dep) && + transitives.contains(dep.getGav())) { + // This direct dependency is transitively provided, remove it + TreeVisitor removeDep = + new org.openrewrite.gradle.RemoveDependency( + dep.getGroupId(), dep.getArtifactId(), conf.getName() + ).getVisitor(); + result = removeDep.visit(result, ctx); + } + } + } + return result; + } + + // Handle Maven + Optional mavenOpt = sf.getMarkers().findFirst(MavenResolutionResult.class); + if (mavenOpt.isPresent()) { + MavenResolutionResult maven = mavenOpt.get(); + String projectId = maven.getPom().getGroupId() + ":" + maven.getPom().getArtifactId(); + Map> scopeToTransitives = + acc.transitivesByProjectAndScope.getOrDefault(projectId, Collections.emptyMap()); + + for (Map.Entry> entry : maven.getDependencies().entrySet()) { + Scope depScope = entry.getKey(); + if (scope != null && !scope.equalsIgnoreCase(depScope.name())) { + continue; + } + Set transitives = getCompatibleTransitives( + scopeToTransitives, depScope.name().toLowerCase(), false); + if (transitives.isEmpty()) { + continue; + } + + for (ResolvedDependency dep : entry.getValue()) { + if (dep.getDepth() == 0 && + isRedundantDependency(dep) && + isInTransitives(dep, transitives)) { + // This direct dependency is transitively provided, remove it + TreeVisitor removeDep = + new org.openrewrite.maven.RemoveDependency( + dep.getGroupId(), dep.getArtifactId(), depScope.name().toLowerCase() + ).getVisitor(); + result = removeDep.visit(result, ctx); + } + } + } + return result; + } + + return tree; + } + + private boolean isRedundantDependency(ResolvedDependency dep) { + return !StringUtils.matchesGlob(dep.getGroupId(), groupId) || + !StringUtils.matchesGlob(dep.getArtifactId(), artifactId); + } + + private boolean isInTransitives(ResolvedDependency dep, Set transitives) { + // Check if this dependency's GAV matches any transitive + // We match on groupId:artifactId:version exactly + for (ResolvedGroupArtifactVersion transitive : transitives) { + if (dep.getGroupId().equals(transitive.getGroupId()) && + dep.getArtifactId().equals(transitive.getArtifactId()) && + dep.getVersion().equals(transitive.getVersion())) { + return true; + } + } + return false; + } + + /** + * Get transitives from this scope and any broader scopes. + * For Maven: compile covers runtime; compile/runtime cover provided + * For Gradle: api covers implementation; implementation covers runtimeOnly + */ + private Set getCompatibleTransitives( + Map> scopeToTransitives, + String targetScope, boolean isGradle) { + + Set result = new HashSet<>(); + + // Always include transitives from the same scope + Set sameScope = scopeToTransitives.get(targetScope); + if (sameScope != null) { + result.addAll(sameScope); + } + + // Include transitives from broader scopes + List broaderScopes = getBroaderScopes(targetScope, isGradle); + for (String broader : broaderScopes) { + Set broaderTransitives = scopeToTransitives.get(broader); + if (broaderTransitives != null) { + result.addAll(broaderTransitives); + } + } + + return result; + } + + private List getBroaderScopes(String scope, boolean isGradle) { + if (isGradle) { + switch (scope.toLowerCase()) { + case "runtimeonly": + case "runtimeclasspath": + return Arrays.asList("implementation", "api"); + case "implementation": + return Collections.singletonList("api"); + case "testimplementation": + case "testruntimeonly": + return Arrays.asList("implementation", "api", "testImplementation"); + default: + return Collections.emptyList(); + } + } else { + // Maven scopes + switch (scope.toLowerCase()) { + case "runtime": + return Collections.singletonList("compile"); + case "provided": + return Arrays.asList("compile", "runtime"); + case "test": + return Arrays.asList("compile", "runtime", "provided"); + default: + return Collections.emptyList(); + } + } + } + }; + } +} diff --git a/src/test/java/org/openrewrite/java/dependencies/RemoveRedundantDependenciesTest.java b/src/test/java/org/openrewrite/java/dependencies/RemoveRedundantDependenciesTest.java new file mode 100644 index 0000000..3da7429 --- /dev/null +++ b/src/test/java/org/openrewrite/java/dependencies/RemoveRedundantDependenciesTest.java @@ -0,0 +1,195 @@ +/* + * Copyright 2024 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.java.dependencies; + +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.gradle.Assertions.buildGradle; +import static org.openrewrite.gradle.toolingapi.Assertions.withToolingApi; +import static org.openrewrite.java.Assertions.mavenProject; +import static org.openrewrite.maven.Assertions.pomXml; + +class RemoveRedundantDependenciesTest implements RewriteTest { + + @DocumentExample("Demonstrate the limitation: when a dependency is declared directly, it cannot be detected as redundant") + @Test + void noChangeWhenDependencyIsDeclaredDirectly() { + // jackson-core is declared directly, so it appears at depth=0 in the resolved tree + // and is not in jackson-databind's transitives. The recipe cannot detect this case. + rewriteRun( + spec -> spec.recipe(new RemoveRedundantDependencies( + "com.fasterxml.jackson.core", "jackson-databind", null, null)), + mavenProject("my-app", + //language=xml + pomXml( + """ + + 4.0.0 + + com.mycompany.app + my-app + 1 + + + + com.fasterxml.jackson.core + jackson-databind + 2.17.0 + + + com.fasterxml.jackson.core + jackson-core + 2.17.0 + + + + """ + ) + ) + ); + } + + @Test + void doNotRemoveWhenVersionsDiffer() { + rewriteRun( + spec -> spec.recipe(new RemoveRedundantDependencies( + "com.fasterxml.jackson.core", "jackson-databind", null, null)), + mavenProject("my-app", + //language=xml + pomXml( + """ + + 4.0.0 + + com.mycompany.app + my-app + 1 + + + + com.fasterxml.jackson.core + jackson-databind + 2.17.0 + + + com.fasterxml.jackson.core + jackson-core + 2.16.0 + + + + """ + ) + ) + ); + } + + @Test + void doNotRemoveParentDependency() { + rewriteRun( + spec -> spec.recipe(new RemoveRedundantDependencies( + "com.fasterxml.jackson.core", "jackson-databind", null, null)), + mavenProject("my-app", + //language=xml + pomXml( + """ + + 4.0.0 + + com.mycompany.app + my-app + 1 + + + + com.fasterxml.jackson.core + jackson-databind + 2.17.0 + + + + """ + ) + ) + ); + } + + @Test + void noMatchingParentDependency() { + rewriteRun( + spec -> spec.recipe(new RemoveRedundantDependencies( + "org.nonexistent", "nonexistent", null, null)), + mavenProject("my-app", + //language=xml + pomXml( + """ + + 4.0.0 + + com.mycompany.app + my-app + 1 + + + + com.fasterxml.jackson.core + jackson-databind + 2.17.0 + + + com.fasterxml.jackson.core + jackson-core + 2.17.0 + + + + """ + ) + ) + ); + } + + @Test + void noChangeWhenGradleDependencyIsDeclaredDirectly() { + // Same limitation as Maven - declared dependencies appear at depth=0 + rewriteRun( + spec -> spec.beforeRecipe(withToolingApi()) + .recipe(new RemoveRedundantDependencies( + "com.fasterxml.jackson.core", "jackson-databind", null, null)), + 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:2.17.0' + } + """ + ) + ) + ); + } +} From 0ccaf1621eccb965c0e555ac1b0c5cbdb1d51b5e Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Tue, 23 Dec 2025 23:18:25 +0100 Subject: [PATCH 2/9] Download and resolve parent POM to find transitives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of relying on the project's resolved dependency tree (which merges direct dependencies), this downloads the parent dependency's POM and resolves it independently. This allows detecting redundancies even when both dependencies are explicitly declared. Also fixes GAV comparison to ignore datedSnapshotVersion field. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../RemoveRedundantDependencies.java | 96 ++++++++++++++----- .../RemoveRedundantDependenciesTest.java | 96 +++++++++++++++++-- 2 files changed, 161 insertions(+), 31 deletions(-) diff --git a/src/main/java/org/openrewrite/java/dependencies/RemoveRedundantDependencies.java b/src/main/java/org/openrewrite/java/dependencies/RemoveRedundantDependencies.java index 4e51537..f406c7e 100644 --- a/src/main/java/org/openrewrite/java/dependencies/RemoveRedundantDependencies.java +++ b/src/main/java/org/openrewrite/java/dependencies/RemoveRedundantDependencies.java @@ -22,13 +22,15 @@ import org.openrewrite.gradle.marker.GradleDependencyConfiguration; import org.openrewrite.gradle.marker.GradleProject; import org.openrewrite.internal.StringUtils; -import org.openrewrite.maven.tree.MavenResolutionResult; -import org.openrewrite.maven.tree.ResolvedDependency; -import org.openrewrite.maven.tree.ResolvedGroupArtifactVersion; -import org.openrewrite.maven.tree.Scope; +import org.openrewrite.maven.MavenDownloadingException; +import org.openrewrite.maven.MavenDownloadingExceptions; +import org.openrewrite.maven.internal.MavenPomDownloader; +import org.openrewrite.maven.tree.*; import java.util.*; +import static java.util.Collections.emptyList; + @EqualsAndHashCode(callSuper = false) @Value public class RemoveRedundantDependencies extends ScanningRecipe { @@ -66,9 +68,8 @@ public String getDisplayName() { @Override public String getDescription() { return "Remove explicit dependencies that are already provided transitively by a specified dependency. " + - "Note: This recipe works best when the redundant dependency is not also explicitly declared elsewhere. " + - "Due to how dependency resolution works, if a dependency is declared directly, it may not appear " + - "in the transitive list of the parent 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."; } @Value @@ -93,6 +94,14 @@ public TreeVisitor getScanner(Accumulator acc) { tree.getMarkers().findFirst(GradleProject.class).ifPresent(gradle -> { String projectId = gradle.getGroup() + ":" + gradle.getName(); + MavenPomDownloader downloader = new MavenPomDownloader(ctx); + + // For Gradle, store all transitives in a single set per project + // because Gradle's configuration hierarchy is complex + Set projectTransitives = acc.transitivesByProjectAndScope + .computeIfAbsent(projectId, k -> new HashMap<>()) + .computeIfAbsent("all", k -> new HashSet<>()); + for (GradleDependencyConfiguration conf : gradle.getConfigurations()) { if (configuration != null && !configuration.equals(conf.getName())) { continue; @@ -101,11 +110,9 @@ public TreeVisitor getScanner(Accumulator acc) { if (dep.getDepth() == 0 && StringUtils.matchesGlob(dep.getGroupId(), groupId) && StringUtils.matchesGlob(dep.getArtifactId(), artifactId)) { - // This is a matching parent dependency, collect its transitives - Set transitives = acc.transitivesByProjectAndScope - .computeIfAbsent(projectId, k -> new HashMap<>()) - .computeIfAbsent(conf.getName(), k -> new HashSet<>()); - collectTransitives(dep, transitives); + // This is a matching parent dependency, resolve its transitives independently + resolveTransitivesFromPom(dep.getGav(), gradle.getMavenRepositories(), + downloader, ctx, projectTransitives); } } } @@ -113,6 +120,8 @@ public TreeVisitor getScanner(Accumulator acc) { tree.getMarkers().findFirst(MavenResolutionResult.class).ifPresent(maven -> { String projectId = maven.getPom().getGroupId() + ":" + maven.getPom().getArtifactId(); + MavenPomDownloader downloader = new MavenPomDownloader(ctx); + List repositories = maven.getPom().getRepositories(); for (Map.Entry> entry : maven.getDependencies().entrySet()) { Scope depScope = entry.getKey(); @@ -123,11 +132,11 @@ public TreeVisitor getScanner(Accumulator acc) { if (dep.getDepth() == 0 && StringUtils.matchesGlob(dep.getGroupId(), groupId) && StringUtils.matchesGlob(dep.getArtifactId(), artifactId)) { - // This is a matching parent dependency, collect its transitives + // This is a matching parent dependency, resolve its transitives independently Set transitives = acc.transitivesByProjectAndScope .computeIfAbsent(projectId, k -> new HashMap<>()) .computeIfAbsent(depScope.name().toLowerCase(), k -> new HashSet<>()); - collectTransitives(dep, transitives); + resolveTransitivesFromPom(dep.getGav(), repositories, downloader, ctx, transitives); } } } @@ -136,10 +145,45 @@ public TreeVisitor getScanner(Accumulator acc) { return tree; } - private void collectTransitives(ResolvedDependency dep, Set transitives) { - for (ResolvedDependency transitive : dep.getDependencies()) { - if (transitives.add(transitive.getGav())) { - collectTransitives(transitive, transitives); + private void resolveTransitivesFromPom(ResolvedGroupArtifactVersion gav, + List repositories, + MavenPomDownloader downloader, + ExecutionContext ctx, + Set transitives) { + try { + // Download the parent dependency's POM + GroupArtifactVersion parentGav = new GroupArtifactVersion( + gav.getGroupId(), gav.getArtifactId(), gav.getVersion()); + + // Ensure we have Maven Central in the repositories + List effectiveRepos = new ArrayList<>(repositories); + if (effectiveRepos.stream().noneMatch(r -> r.getUri().contains("repo.maven.apache.org") || + r.getUri().contains("repo1.maven.org"))) { + effectiveRepos.add(MavenRepository.MAVEN_CENTRAL); + } + + Pom pom = downloader.download(parentGav, null, null, effectiveRepos); + + // Resolve the POM to get its full dependency tree + ResolvedPom resolvedPom = pom.resolve(emptyList(), downloader, effectiveRepos, ctx); + + // Get the resolved dependencies for compile scope (which includes most transitives) + List resolved = resolvedPom.resolveDependencies(Scope.Compile, downloader, ctx); + + // Collect all dependencies (both direct and transitive of the parent) + for (ResolvedDependency dep : resolved) { + collectAllDependencies(dep, transitives); + } + } catch (MavenDownloadingException | MavenDownloadingExceptions e) { + // If we can't download/resolve the POM, fall back to not detecting redundancies + // This is a best-effort approach + } + } + + private void collectAllDependencies(ResolvedDependency dep, Set transitives) { + if (transitives.add(dep.getGav())) { + for (ResolvedDependency transitive : dep.getDependencies()) { + collectAllDependencies(transitive, transitives); } } } @@ -166,24 +210,26 @@ public TreeVisitor getVisitor(Accumulator acc) { Map> scopeToTransitives = acc.transitivesByProjectAndScope.getOrDefault(projectId, Collections.emptyMap()); + // For Gradle, use the "all" bucket we created in scanner + Set transitives = scopeToTransitives.getOrDefault("all", Collections.emptySet()); + if (transitives.isEmpty()) { + return result; + } + for (GradleDependencyConfiguration conf : gradle.getConfigurations()) { if (configuration != null && !configuration.equals(conf.getName())) { continue; } - Set transitives = getCompatibleTransitives( - scopeToTransitives, conf.getName(), true); - if (transitives.isEmpty()) { - continue; - } for (ResolvedDependency dep : conf.getResolved()) { if (dep.getDepth() == 0 && isRedundantDependency(dep) && - transitives.contains(dep.getGav())) { + isInTransitives(dep, transitives)) { // This direct dependency is transitively provided, remove it + // Don't specify configuration - Gradle's resolved config names differ from declaration names TreeVisitor removeDep = new org.openrewrite.gradle.RemoveDependency( - dep.getGroupId(), dep.getArtifactId(), conf.getName() + dep.getGroupId(), dep.getArtifactId(), configuration ).getVisitor(); result = removeDep.visit(result, ctx); } diff --git a/src/test/java/org/openrewrite/java/dependencies/RemoveRedundantDependenciesTest.java b/src/test/java/org/openrewrite/java/dependencies/RemoveRedundantDependenciesTest.java index 3da7429..1c3fe90 100644 --- a/src/test/java/org/openrewrite/java/dependencies/RemoveRedundantDependenciesTest.java +++ b/src/test/java/org/openrewrite/java/dependencies/RemoveRedundantDependenciesTest.java @@ -26,11 +26,9 @@ class RemoveRedundantDependenciesTest implements RewriteTest { - @DocumentExample("Demonstrate the limitation: when a dependency is declared directly, it cannot be detected as redundant") + @DocumentExample @Test - void noChangeWhenDependencyIsDeclaredDirectly() { - // jackson-core is declared directly, so it appears at depth=0 in the resolved tree - // and is not in jackson-databind's transitives. The recipe cannot detect this case. + void removeRedundantMavenDependency() { rewriteRun( spec -> spec.recipe(new RemoveRedundantDependencies( "com.fasterxml.jackson.core", "jackson-databind", null, null)), @@ -58,6 +56,80 @@ void noChangeWhenDependencyIsDeclaredDirectly() { + """, + """ + + 4.0.0 + + com.mycompany.app + my-app + 1 + + + + com.fasterxml.jackson.core + jackson-databind + 2.17.0 + + + + """ + ) + ) + ); + } + + @Test + void removeMultipleRedundantDependencies() { + rewriteRun( + spec -> spec.recipe(new RemoveRedundantDependencies( + "com.fasterxml.jackson.core", "jackson-databind", null, null)), + mavenProject("my-app", + //language=xml + pomXml( + """ + + 4.0.0 + + com.mycompany.app + my-app + 1 + + + + com.fasterxml.jackson.core + jackson-databind + 2.17.0 + + + com.fasterxml.jackson.core + jackson-core + 2.17.0 + + + com.fasterxml.jackson.core + jackson-annotations + 2.17.0 + + + + """, + """ + + 4.0.0 + + com.mycompany.app + my-app + 1 + + + + com.fasterxml.jackson.core + jackson-databind + 2.17.0 + + + """ ) ) @@ -165,8 +237,7 @@ void noMatchingParentDependency() { } @Test - void noChangeWhenGradleDependencyIsDeclaredDirectly() { - // Same limitation as Maven - declared dependencies appear at depth=0 + void removeRedundantGradleDependency() { rewriteRun( spec -> spec.beforeRecipe(withToolingApi()) .recipe(new RemoveRedundantDependencies( @@ -187,6 +258,19 @@ void noChangeWhenGradleDependencyIsDeclaredDirectly() { implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.0' implementation 'com.fasterxml.jackson.core:jackson-core:2.17.0' } + """, + """ + plugins { + id 'java-library' + } + + repositories { + mavenCentral() + } + + dependencies { + implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.0' + } """ ) ) From 560303daf7b64e482ffd116c1127acf35f066667 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Wed, 24 Dec 2025 13:37:50 +0100 Subject: [PATCH 3/9] Remove the limiting arguments for scopes --- .../RemoveRedundantDependencies.java | 188 ++++++++---------- .../RemoveRedundantDependenciesTest.java | 16 +- 2 files changed, 87 insertions(+), 117 deletions(-) diff --git a/src/main/java/org/openrewrite/java/dependencies/RemoveRedundantDependencies.java b/src/main/java/org/openrewrite/java/dependencies/RemoveRedundantDependencies.java index f406c7e..a121b94 100644 --- a/src/main/java/org/openrewrite/java/dependencies/RemoveRedundantDependencies.java +++ b/src/main/java/org/openrewrite/java/dependencies/RemoveRedundantDependencies.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 the original author or authors. + * Copyright 2025 the original author or authors. *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,7 +29,7 @@ import java.util.*; -import static java.util.Collections.emptyList; +import static java.util.Collections.*; @EqualsAndHashCode(callSuper = false) @Value @@ -45,21 +45,6 @@ public class RemoveRedundantDependencies extends ScanningRecipe getScanner(Accumulator acc) { .computeIfAbsent("all", k -> new HashSet<>()); for (GradleDependencyConfiguration conf : gradle.getConfigurations()) { - if (configuration != null && !configuration.equals(conf.getName())) { - continue; - } for (ResolvedDependency dep : conf.getResolved()) { - if (dep.getDepth() == 0 && - StringUtils.matchesGlob(dep.getGroupId(), groupId) && - StringUtils.matchesGlob(dep.getArtifactId(), artifactId)) { + if (dep.isDirect() && + StringUtils.matchesGlob(dep.getGroupId(), groupId) && + StringUtils.matchesGlob(dep.getArtifactId(), artifactId)) { // This is a matching parent dependency, resolve its transitives independently - resolveTransitivesFromPom(dep.getGav(), gradle.getMavenRepositories(), - downloader, ctx, projectTransitives); + resolveTransitivesFromPom( + dep.getGav(), + gradle.getMavenRepositories(), + downloader, + ctx, + projectTransitives); } } } @@ -121,22 +107,23 @@ public TreeVisitor getScanner(Accumulator acc) { tree.getMarkers().findFirst(MavenResolutionResult.class).ifPresent(maven -> { String projectId = maven.getPom().getGroupId() + ":" + maven.getPom().getArtifactId(); MavenPomDownloader downloader = new MavenPomDownloader(ctx); - List repositories = maven.getPom().getRepositories(); for (Map.Entry> entry : maven.getDependencies().entrySet()) { Scope depScope = entry.getKey(); - if (scope != null && !scope.equalsIgnoreCase(depScope.name())) { - continue; - } for (ResolvedDependency dep : entry.getValue()) { - if (dep.getDepth() == 0 && - StringUtils.matchesGlob(dep.getGroupId(), groupId) && - StringUtils.matchesGlob(dep.getArtifactId(), artifactId)) { + if (dep.isDirect() && + StringUtils.matchesGlob(dep.getGroupId(), groupId) && + StringUtils.matchesGlob(dep.getArtifactId(), artifactId)) { // This is a matching parent dependency, resolve its transitives independently Set transitives = acc.transitivesByProjectAndScope .computeIfAbsent(projectId, k -> new HashMap<>()) .computeIfAbsent(depScope.name().toLowerCase(), k -> new HashSet<>()); - resolveTransitivesFromPom(dep.getGav(), repositories, downloader, ctx, transitives); + resolveTransitivesFromPom( + dep.getGav(), + maven.getPom().getRepositories(), + downloader, + ctx, + transitives); } } } @@ -145,29 +132,23 @@ public TreeVisitor getScanner(Accumulator acc) { return tree; } - private void resolveTransitivesFromPom(ResolvedGroupArtifactVersion gav, - List repositories, - MavenPomDownloader downloader, - ExecutionContext ctx, - Set transitives) { + private void resolveTransitivesFromPom( + ResolvedGroupArtifactVersion gav, + List repositories, + MavenPomDownloader downloader, + ExecutionContext ctx, + Set transitives) { try { - // Download the parent dependency's POM - GroupArtifactVersion parentGav = new GroupArtifactVersion( - gav.getGroupId(), gav.getArtifactId(), gav.getVersion()); - // Ensure we have Maven Central in the repositories List effectiveRepos = new ArrayList<>(repositories); if (effectiveRepos.stream().noneMatch(r -> r.getUri().contains("repo.maven.apache.org") || - r.getUri().contains("repo1.maven.org"))) { + r.getUri().contains("repo1.maven.org"))) { effectiveRepos.add(MavenRepository.MAVEN_CENTRAL); } - Pom pom = downloader.download(parentGav, null, null, effectiveRepos); - - // Resolve the POM to get its full dependency tree - ResolvedPom resolvedPom = pom.resolve(emptyList(), downloader, effectiveRepos, ctx); - // Get the resolved dependencies for compile scope (which includes most transitives) + Pom pom = downloader.download(gav.asGroupArtifactVersion(), null, null, effectiveRepos); + ResolvedPom resolvedPom = pom.resolve(emptyList(), downloader, effectiveRepos, ctx); List resolved = resolvedPom.resolveDependencies(Scope.Compile, downloader, ctx); // Collect all dependencies (both direct and transitive of the parent) @@ -208,7 +189,7 @@ public TreeVisitor getVisitor(Accumulator acc) { GradleProject gradle = gradleOpt.get(); String projectId = gradle.getGroup() + ":" + gradle.getName(); Map> scopeToTransitives = - acc.transitivesByProjectAndScope.getOrDefault(projectId, Collections.emptyMap()); + acc.transitivesByProjectAndScope.getOrDefault(projectId, emptyMap()); // For Gradle, use the "all" bucket we created in scanner Set transitives = scopeToTransitives.getOrDefault("all", Collections.emptySet()); @@ -217,21 +198,15 @@ public TreeVisitor getVisitor(Accumulator acc) { } for (GradleDependencyConfiguration conf : gradle.getConfigurations()) { - if (configuration != null && !configuration.equals(conf.getName())) { - continue; - } - for (ResolvedDependency dep : conf.getResolved()) { - if (dep.getDepth() == 0 && - isRedundantDependency(dep) && - isInTransitives(dep, transitives)) { + if (dep.isDirect() && + doesNotMatchArguments(dep) && + isInTransitives(dep, transitives)) { // This direct dependency is transitively provided, remove it // Don't specify configuration - Gradle's resolved config names differ from declaration names - TreeVisitor removeDep = - new org.openrewrite.gradle.RemoveDependency( - dep.getGroupId(), dep.getArtifactId(), configuration - ).getVisitor(); - result = removeDep.visit(result, ctx); + result = new RemoveDependency( + dep.getGroupId(), dep.getArtifactId(), null, null, null) + .getVisitor().visit(result, ctx); } } } @@ -244,29 +219,24 @@ public TreeVisitor getVisitor(Accumulator acc) { MavenResolutionResult maven = mavenOpt.get(); String projectId = maven.getPom().getGroupId() + ":" + maven.getPom().getArtifactId(); Map> scopeToTransitives = - acc.transitivesByProjectAndScope.getOrDefault(projectId, Collections.emptyMap()); + acc.transitivesByProjectAndScope.getOrDefault(projectId, emptyMap()); for (Map.Entry> entry : maven.getDependencies().entrySet()) { - Scope depScope = entry.getKey(); - if (scope != null && !scope.equalsIgnoreCase(depScope.name())) { - continue; - } + String scope = entry.getKey().name().toLowerCase(); Set transitives = getCompatibleTransitives( - scopeToTransitives, depScope.name().toLowerCase(), false); + scopeToTransitives, scope); if (transitives.isEmpty()) { continue; } for (ResolvedDependency dep : entry.getValue()) { - if (dep.getDepth() == 0 && - isRedundantDependency(dep) && - isInTransitives(dep, transitives)) { + if (dep.isDirect() && + doesNotMatchArguments(dep) && + isInTransitives(dep, transitives)) { // This direct dependency is transitively provided, remove it - TreeVisitor removeDep = - new org.openrewrite.maven.RemoveDependency( - dep.getGroupId(), dep.getArtifactId(), depScope.name().toLowerCase() - ).getVisitor(); - result = removeDep.visit(result, ctx); + result = new RemoveDependency( + dep.getGroupId(), dep.getArtifactId(), null, null, scope) + .getVisitor().visit(result, ctx); } } } @@ -276,9 +246,9 @@ public TreeVisitor getVisitor(Accumulator acc) { return tree; } - private boolean isRedundantDependency(ResolvedDependency dep) { + private boolean doesNotMatchArguments(ResolvedDependency dep) { return !StringUtils.matchesGlob(dep.getGroupId(), groupId) || - !StringUtils.matchesGlob(dep.getArtifactId(), artifactId); + !StringUtils.matchesGlob(dep.getArtifactId(), artifactId); } private boolean isInTransitives(ResolvedDependency dep, Set transitives) { @@ -286,8 +256,8 @@ private boolean isInTransitives(ResolvedDependency dep, Set getCompatibleTransitives( Map> scopeToTransitives, - String targetScope, boolean isGradle) { + String targetScope) { Set result = new HashSet<>(); @@ -312,7 +282,7 @@ private Set getCompatibleTransitives( } // Include transitives from broader scopes - List broaderScopes = getBroaderScopes(targetScope, isGradle); + List broaderScopes = getBroaderMavenScopes(targetScope); for (String broader : broaderScopes) { Set broaderTransitives = scopeToTransitives.get(broader); if (broaderTransitives != null) { @@ -323,32 +293,32 @@ private Set getCompatibleTransitives( return result; } - private List getBroaderScopes(String scope, boolean isGradle) { - if (isGradle) { - switch (scope.toLowerCase()) { - case "runtimeonly": - case "runtimeclasspath": - return Arrays.asList("implementation", "api"); - case "implementation": - return Collections.singletonList("api"); - case "testimplementation": - case "testruntimeonly": - return Arrays.asList("implementation", "api", "testImplementation"); - default: - return Collections.emptyList(); - } - } else { - // Maven scopes - switch (scope.toLowerCase()) { - case "runtime": - return Collections.singletonList("compile"); - case "provided": - return Arrays.asList("compile", "runtime"); - case "test": - return Arrays.asList("compile", "runtime", "provided"); - default: - return Collections.emptyList(); - } + @SuppressWarnings("unused") // Not used currently, as we map Gradle scopes to a single "all" bucket + private List getBroaderGradleScopes(String scope) { + switch (scope.toLowerCase()) { + case "runtimeonly": + case "runtimeclasspath": + return Arrays.asList("implementation", "api"); + case "implementation": + return singletonList("api"); + case "testimplementation": + case "testruntimeonly": + return Arrays.asList("implementation", "api", "testImplementation"); + default: + return emptyList(); + } + } + + private List getBroaderMavenScopes(String scope) { + switch (scope.toLowerCase()) { + case "runtime": + return singletonList("compile"); + case "provided": + return Arrays.asList("compile", "runtime"); + case "test": + return Arrays.asList("compile", "runtime", "provided"); + default: + return emptyList(); } } }; diff --git a/src/test/java/org/openrewrite/java/dependencies/RemoveRedundantDependenciesTest.java b/src/test/java/org/openrewrite/java/dependencies/RemoveRedundantDependenciesTest.java index 1c3fe90..6d29ced 100644 --- a/src/test/java/org/openrewrite/java/dependencies/RemoveRedundantDependenciesTest.java +++ b/src/test/java/org/openrewrite/java/dependencies/RemoveRedundantDependenciesTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 the original author or authors. + * Copyright 2025 the original author or authors. *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,7 +31,7 @@ class RemoveRedundantDependenciesTest implements RewriteTest { void removeRedundantMavenDependency() { rewriteRun( spec -> spec.recipe(new RemoveRedundantDependencies( - "com.fasterxml.jackson.core", "jackson-databind", null, null)), + "com.fasterxml.jackson.core", "jackson-databind")), mavenProject("my-app", //language=xml pomXml( @@ -83,7 +83,7 @@ void removeRedundantMavenDependency() { void removeMultipleRedundantDependencies() { rewriteRun( spec -> spec.recipe(new RemoveRedundantDependencies( - "com.fasterxml.jackson.core", "jackson-databind", null, null)), + "com.fasterxml.jackson.core", "jackson-databind")), mavenProject("my-app", //language=xml pomXml( @@ -140,7 +140,7 @@ void removeMultipleRedundantDependencies() { void doNotRemoveWhenVersionsDiffer() { rewriteRun( spec -> spec.recipe(new RemoveRedundantDependencies( - "com.fasterxml.jackson.core", "jackson-databind", null, null)), + "com.fasterxml.jackson.core", "jackson-databind")), mavenProject("my-app", //language=xml pomXml( @@ -172,10 +172,10 @@ void doNotRemoveWhenVersionsDiffer() { } @Test - void doNotRemoveParentDependency() { + void doNotRemoveDirectDependencyItself() { rewriteRun( spec -> spec.recipe(new RemoveRedundantDependencies( - "com.fasterxml.jackson.core", "jackson-databind", null, null)), + "com.fasterxml.jackson.core", "jackson-databind")), mavenProject("my-app", //language=xml pomXml( @@ -205,7 +205,7 @@ void doNotRemoveParentDependency() { void noMatchingParentDependency() { rewriteRun( spec -> spec.recipe(new RemoveRedundantDependencies( - "org.nonexistent", "nonexistent", null, null)), + "org.nonexistent", "nonexistent")), mavenProject("my-app", //language=xml pomXml( @@ -241,7 +241,7 @@ void removeRedundantGradleDependency() { rewriteRun( spec -> spec.beforeRecipe(withToolingApi()) .recipe(new RemoveRedundantDependencies( - "com.fasterxml.jackson.core", "jackson-databind", null, null)), + "com.fasterxml.jackson.core", "jackson-databind")), mavenProject("my-app", //language=groovy buildGradle( From 75e26e91bcf4c0cf9a0d56877c4a60c68c8cbbad Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Wed, 24 Dec 2025 13:50:35 +0100 Subject: [PATCH 4/9] Remove the limiting arguments for scopes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use specific Gradle configuration buckets instead of a single "all" bucket, with getBroaderGradleScopes to find transitives from broader configurations like api -> implementation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../RemoveRedundantDependencies.java | 61 ++++++++++++------- 1 file changed, 40 insertions(+), 21 deletions(-) diff --git a/src/main/java/org/openrewrite/java/dependencies/RemoveRedundantDependencies.java b/src/main/java/org/openrewrite/java/dependencies/RemoveRedundantDependencies.java index a121b94..0676b7d 100644 --- a/src/main/java/org/openrewrite/java/dependencies/RemoveRedundantDependencies.java +++ b/src/main/java/org/openrewrite/java/dependencies/RemoveRedundantDependencies.java @@ -81,24 +81,21 @@ public TreeVisitor getScanner(Accumulator acc) { String projectId = gradle.getGroup() + ":" + gradle.getName(); MavenPomDownloader downloader = new MavenPomDownloader(ctx); - // For Gradle, store all transitives in a single set per project - // because Gradle's configuration hierarchy is complex - Set projectTransitives = acc.transitivesByProjectAndScope - .computeIfAbsent(projectId, k -> new HashMap<>()) - .computeIfAbsent("all", k -> new HashSet<>()); - for (GradleDependencyConfiguration conf : gradle.getConfigurations()) { for (ResolvedDependency dep : conf.getResolved()) { if (dep.isDirect() && StringUtils.matchesGlob(dep.getGroupId(), groupId) && StringUtils.matchesGlob(dep.getArtifactId(), artifactId)) { // This is a matching parent dependency, resolve its transitives independently + Set transitives = acc.transitivesByProjectAndScope + .computeIfAbsent(projectId, k -> new HashMap<>()) + .computeIfAbsent(conf.getName(), k -> new HashSet<>()); resolveTransitivesFromPom( dep.getGav(), gradle.getMavenRepositories(), downloader, ctx, - projectTransitives); + transitives); } } } @@ -191,13 +188,13 @@ public TreeVisitor getVisitor(Accumulator acc) { Map> scopeToTransitives = acc.transitivesByProjectAndScope.getOrDefault(projectId, emptyMap()); - // For Gradle, use the "all" bucket we created in scanner - Set transitives = scopeToTransitives.getOrDefault("all", Collections.emptySet()); - if (transitives.isEmpty()) { - return result; - } - for (GradleDependencyConfiguration conf : gradle.getConfigurations()) { + Set transitives = getCompatibleGradleTransitives( + scopeToTransitives, conf.getName()); + if (transitives.isEmpty()) { + continue; + } + for (ResolvedDependency dep : conf.getResolved()) { if (dep.isDirect() && doesNotMatchArguments(dep) && @@ -223,7 +220,7 @@ public TreeVisitor getVisitor(Accumulator acc) { for (Map.Entry> entry : maven.getDependencies().entrySet()) { String scope = entry.getKey().name().toLowerCase(); - Set transitives = getCompatibleTransitives( + Set transitives = getCompatibleMavenTransitives( scopeToTransitives, scope); if (transitives.isEmpty()) { continue; @@ -265,11 +262,35 @@ private boolean isInTransitives(ResolvedDependency dep, Set getCompatibleGradleTransitives( + Map> scopeToTransitives, + String targetScope) { + + Set result = new HashSet<>(); + + // Always include transitives from the same scope + Set sameScope = scopeToTransitives.get(targetScope); + if (sameScope != null) { + result.addAll(sameScope); + } + + // Include transitives from broader scopes + for (String broader : getBroaderGradleScopes(targetScope)) { + Set broaderTransitives = scopeToTransitives.get(broader); + if (broaderTransitives != null) { + result.addAll(broaderTransitives); + } + } + + return result; + } + + /** + * Get transitives from this Maven scope and any broader scopes. */ - private Set getCompatibleTransitives( + private Set getCompatibleMavenTransitives( Map> scopeToTransitives, String targetScope) { @@ -282,8 +303,7 @@ private Set getCompatibleTransitives( } // Include transitives from broader scopes - List broaderScopes = getBroaderMavenScopes(targetScope); - for (String broader : broaderScopes) { + for (String broader : getBroaderMavenScopes(targetScope)) { Set broaderTransitives = scopeToTransitives.get(broader); if (broaderTransitives != null) { result.addAll(broaderTransitives); @@ -293,7 +313,6 @@ private Set getCompatibleTransitives( return result; } - @SuppressWarnings("unused") // Not used currently, as we map Gradle scopes to a single "all" bucket private List getBroaderGradleScopes(String scope) { switch (scope.toLowerCase()) { case "runtimeonly": From c12d1b67fa7a935addd9d70673c6f5b2e581a712 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Wed, 24 Dec 2025 13:54:36 +0100 Subject: [PATCH 5/9] Merge getCompatibleTransitives methods with isGradle parameter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../RemoveRedundantDependencies.java | 46 +++++-------------- 1 file changed, 12 insertions(+), 34 deletions(-) diff --git a/src/main/java/org/openrewrite/java/dependencies/RemoveRedundantDependencies.java b/src/main/java/org/openrewrite/java/dependencies/RemoveRedundantDependencies.java index 0676b7d..446f4a3 100644 --- a/src/main/java/org/openrewrite/java/dependencies/RemoveRedundantDependencies.java +++ b/src/main/java/org/openrewrite/java/dependencies/RemoveRedundantDependencies.java @@ -189,8 +189,8 @@ public TreeVisitor getVisitor(Accumulator acc) { acc.transitivesByProjectAndScope.getOrDefault(projectId, emptyMap()); for (GradleDependencyConfiguration conf : gradle.getConfigurations()) { - Set transitives = getCompatibleGradleTransitives( - scopeToTransitives, conf.getName()); + Set transitives = getCompatibleTransitives( + scopeToTransitives, conf.getName(), true); if (transitives.isEmpty()) { continue; } @@ -220,8 +220,8 @@ public TreeVisitor getVisitor(Accumulator acc) { for (Map.Entry> entry : maven.getDependencies().entrySet()) { String scope = entry.getKey().name().toLowerCase(); - Set transitives = getCompatibleMavenTransitives( - scopeToTransitives, scope); + Set transitives = getCompatibleTransitives( + scopeToTransitives, scope, false); if (transitives.isEmpty()) { continue; } @@ -262,11 +262,12 @@ private boolean isInTransitives(ResolvedDependency dep, Set getCompatibleGradleTransitives( + private Set getCompatibleTransitives( Map> scopeToTransitives, - String targetScope) { + String targetScope, + boolean isGradle) { Set result = new HashSet<>(); @@ -277,33 +278,10 @@ private Set getCompatibleGradleTransitives( } // Include transitives from broader scopes - for (String broader : getBroaderGradleScopes(targetScope)) { - Set broaderTransitives = scopeToTransitives.get(broader); - if (broaderTransitives != null) { - result.addAll(broaderTransitives); - } - } - - return result; - } - - /** - * Get transitives from this Maven scope and any broader scopes. - */ - private Set getCompatibleMavenTransitives( - Map> scopeToTransitives, - String targetScope) { - - Set result = new HashSet<>(); - - // Always include transitives from the same scope - Set sameScope = scopeToTransitives.get(targetScope); - if (sameScope != null) { - result.addAll(sameScope); - } - - // Include transitives from broader scopes - for (String broader : getBroaderMavenScopes(targetScope)) { + List broaderScopes = isGradle + ? getBroaderGradleScopes(targetScope) + : getBroaderMavenScopes(targetScope); + for (String broader : broaderScopes) { Set broaderTransitives = scopeToTransitives.get(broader); if (broaderTransitives != null) { result.addAll(broaderTransitives); From 9a2419ee4a742ca750113ae3c48ce4634b4ba153 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Wed, 24 Dec 2025 13:57:43 +0100 Subject: [PATCH 6/9] Merge getCompatibleTransitives methods with isGradle parameter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../java/dependencies/RemoveRedundantDependencies.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/openrewrite/java/dependencies/RemoveRedundantDependencies.java b/src/main/java/org/openrewrite/java/dependencies/RemoveRedundantDependencies.java index 446f4a3..282d190 100644 --- a/src/main/java/org/openrewrite/java/dependencies/RemoveRedundantDependencies.java +++ b/src/main/java/org/openrewrite/java/dependencies/RemoveRedundantDependencies.java @@ -278,9 +278,9 @@ private Set getCompatibleTransitives( } // Include transitives from broader scopes - List broaderScopes = isGradle - ? getBroaderGradleScopes(targetScope) - : getBroaderMavenScopes(targetScope); + List broaderScopes = isGradle ? + getBroaderGradleScopes(targetScope) : + getBroaderMavenScopes(targetScope); for (String broader : broaderScopes) { Set broaderTransitives = scopeToTransitives.get(broader); if (broaderTransitives != null) { From c21e51ae15e3caf6dd71f948212fa507ba750210 Mon Sep 17 00:00:00 2001 From: Steve Elliott Date: Wed, 24 Dec 2025 10:10:48 -0500 Subject: [PATCH 7/9] Adding additional test showing it handling dependencies in `test` scope (transitive `compile`) and a currently failing test for an exclusion scenario. --- .../RemoveRedundantDependenciesTest.java | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/src/test/java/org/openrewrite/java/dependencies/RemoveRedundantDependenciesTest.java b/src/test/java/org/openrewrite/java/dependencies/RemoveRedundantDependenciesTest.java index 6d29ced..de4b0df 100644 --- a/src/test/java/org/openrewrite/java/dependencies/RemoveRedundantDependenciesTest.java +++ b/src/test/java/org/openrewrite/java/dependencies/RemoveRedundantDependenciesTest.java @@ -79,6 +79,61 @@ void removeRedundantMavenDependency() { ); } + @Test + void removeRedundantMavenDependencyInTestScope() { + rewriteRun( + spec -> spec.recipe(new RemoveRedundantDependencies( + "org.junit.jupiter", "junit-jupiter-engine")), + mavenProject("my-app", + //language=xml + pomXml( + """ + + 4.0.0 + + com.mycompany.app + my-app + 1 + + + + org.junit.jupiter + junit-jupiter-engine + 6.0.1 + test + + + org.junit.jupiter + junit-jupiter-api + 6.0.1 + test + + + + """, + """ + + 4.0.0 + + com.mycompany.app + my-app + 1 + + + + org.junit.jupiter + junit-jupiter-engine + 6.0.1 + test + + + + """ + ) + ) + ); + } + @Test void removeMultipleRedundantDependencies() { rewriteRun( @@ -171,6 +226,47 @@ void doNotRemoveWhenVersionsDiffer() { ); } + @Test + void doNoRemoveWhenExcluded() { + rewriteRun( + spec -> spec.recipe(new RemoveRedundantDependencies( + "com.fasterxml.jackson.core", "jackson-databind")), + mavenProject("my-app", + //language=xml + pomXml( + """ + + 4.0.0 + + com.mycompany.app + my-app + 1 + + + + com.fasterxml.jackson.core + jackson-databind + 2.17.0 + + + com.fasterxml.jackson.core + jackson-core + + + + + com.fasterxml.jackson.core + jackson-core + 2.17.0 + + + + """ + ) + ) + ); + } + @Test void doNotRemoveDirectDependencyItself() { rewriteRun( From 81d99c145456cbb53a5b5a514b0de53c9901b82d Mon Sep 17 00:00:00 2001 From: Steve Elliott Date: Wed, 24 Dec 2025 11:16:11 -0500 Subject: [PATCH 8/9] Stripping resolved POM's requested POM's dependencies and the resolved POM's `requestedDependencies` based on the exclusions present on the dependency in our project POM --- .../RemoveRedundantDependencies.java | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/openrewrite/java/dependencies/RemoveRedundantDependencies.java b/src/main/java/org/openrewrite/java/dependencies/RemoveRedundantDependencies.java index 282d190..cb2a7bc 100644 --- a/src/main/java/org/openrewrite/java/dependencies/RemoveRedundantDependencies.java +++ b/src/main/java/org/openrewrite/java/dependencies/RemoveRedundantDependencies.java @@ -21,6 +21,7 @@ import org.openrewrite.*; import org.openrewrite.gradle.marker.GradleDependencyConfiguration; import org.openrewrite.gradle.marker.GradleProject; +import org.openrewrite.internal.ListUtils; import org.openrewrite.internal.StringUtils; import org.openrewrite.maven.MavenDownloadingException; import org.openrewrite.maven.MavenDownloadingExceptions; @@ -92,6 +93,7 @@ public TreeVisitor getScanner(Accumulator acc) { .computeIfAbsent(conf.getName(), k -> new HashSet<>()); resolveTransitivesFromPom( dep.getGav(), + dep.getEffectiveExclusions(), gradle.getMavenRepositories(), downloader, ctx, @@ -117,6 +119,7 @@ public TreeVisitor getScanner(Accumulator acc) { .computeIfAbsent(depScope.name().toLowerCase(), k -> new HashSet<>()); resolveTransitivesFromPom( dep.getGav(), + dep.getEffectiveExclusions(), maven.getPom().getRepositories(), downloader, ctx, @@ -131,6 +134,7 @@ public TreeVisitor getScanner(Accumulator acc) { private void resolveTransitivesFromPom( ResolvedGroupArtifactVersion gav, + List effectiveExclusions, List repositories, MavenPomDownloader downloader, ExecutionContext ctx, @@ -146,7 +150,8 @@ private void resolveTransitivesFromPom( // Get the resolved dependencies for compile scope (which includes most transitives) Pom pom = downloader.download(gav.asGroupArtifactVersion(), null, null, effectiveRepos); ResolvedPom resolvedPom = pom.resolve(emptyList(), downloader, effectiveRepos, ctx); - List resolved = resolvedPom.resolveDependencies(Scope.Compile, downloader, ctx); + ResolvedPom patchedPom = applyExclusions(resolvedPom, effectiveExclusions); + List resolved = patchedPom.resolveDependencies(Scope.Compile, downloader, ctx); // Collect all dependencies (both direct and transitive of the parent) for (ResolvedDependency dep : resolved) { @@ -158,6 +163,24 @@ private void resolveTransitivesFromPom( } } + private ResolvedPom applyExclusions(ResolvedPom resolvedPom, List effectiveExclusions) { + List existingRequested_dependencies = resolvedPom.getRequested().getDependencies(); + ResolvedPom patchedPom = resolvedPom + .withRequested( + resolvedPom.getRequested().withDependencies( + ListUtils.filter( + existingRequested_dependencies, + d -> effectiveExclusions.stream() + .noneMatch(e -> e.getGroupId().equals(d.getGroupId()) && e.getArtifactId().equals(d.getArtifactId())) + ) + ) + ); + patchedPom.getRequestedDependencies() + .removeIf(d -> effectiveExclusions.stream() + .anyMatch(e -> e.getGroupId().equals(d.getGroupId()) && e.getArtifactId().equals(d.getArtifactId()))); + return patchedPom; + } + private void collectAllDependencies(ResolvedDependency dep, Set transitives) { if (transitives.add(dep.getGav())) { for (ResolvedDependency transitive : dep.getDependencies()) { From a3566e5abbb9128c802ffb3e0c84d9b03514ba77 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Wed, 24 Dec 2025 17:42:17 +0100 Subject: [PATCH 9/9] Condense --- .../RemoveRedundantDependencies.java | 19 +++------- .../RemoveRedundantDependenciesTest.java | 37 +++---------------- 2 files changed, 11 insertions(+), 45 deletions(-) diff --git a/src/main/java/org/openrewrite/java/dependencies/RemoveRedundantDependencies.java b/src/main/java/org/openrewrite/java/dependencies/RemoveRedundantDependencies.java index cb2a7bc..c44d7a9 100644 --- a/src/main/java/org/openrewrite/java/dependencies/RemoveRedundantDependencies.java +++ b/src/main/java/org/openrewrite/java/dependencies/RemoveRedundantDependencies.java @@ -164,20 +164,11 @@ private void resolveTransitivesFromPom( } private ResolvedPom applyExclusions(ResolvedPom resolvedPom, List effectiveExclusions) { - List existingRequested_dependencies = resolvedPom.getRequested().getDependencies(); - ResolvedPom patchedPom = resolvedPom - .withRequested( - resolvedPom.getRequested().withDependencies( - ListUtils.filter( - existingRequested_dependencies, - d -> effectiveExclusions.stream() - .noneMatch(e -> e.getGroupId().equals(d.getGroupId()) && e.getArtifactId().equals(d.getArtifactId())) - ) - ) - ); - patchedPom.getRequestedDependencies() - .removeIf(d -> effectiveExclusions.stream() - .anyMatch(e -> e.getGroupId().equals(d.getGroupId()) && e.getArtifactId().equals(d.getArtifactId()))); + ResolvedPom patchedPom = resolvedPom.withRequested(resolvedPom.getRequested().withDependencies( + ListUtils.filter(resolvedPom.getRequested().getDependencies(), d -> effectiveExclusions.stream() + .noneMatch(e -> e.getGroupId().equals(d.getGroupId()) && e.getArtifactId().equals(d.getArtifactId()))))); + patchedPom.getRequestedDependencies().removeIf(d -> effectiveExclusions.stream() + .anyMatch(e -> e.getGroupId().equals(d.getGroupId()) && e.getArtifactId().equals(d.getArtifactId()))); return patchedPom; } diff --git a/src/test/java/org/openrewrite/java/dependencies/RemoveRedundantDependenciesTest.java b/src/test/java/org/openrewrite/java/dependencies/RemoveRedundantDependenciesTest.java index de4b0df..ef8d001 100644 --- a/src/test/java/org/openrewrite/java/dependencies/RemoveRedundantDependenciesTest.java +++ b/src/test/java/org/openrewrite/java/dependencies/RemoveRedundantDependenciesTest.java @@ -25,24 +25,21 @@ import static org.openrewrite.maven.Assertions.pomXml; class RemoveRedundantDependenciesTest implements RewriteTest { - @DocumentExample @Test void removeRedundantMavenDependency() { rewriteRun( spec -> spec.recipe(new RemoveRedundantDependencies( - "com.fasterxml.jackson.core", "jackson-databind")), + "com.fasterxml.jackson.core", "jackson-databind")), mavenProject("my-app", //language=xml pomXml( """ 4.0.0 - com.mycompany.app my-app 1 - com.fasterxml.jackson.core @@ -60,11 +57,9 @@ void removeRedundantMavenDependency() { """ 4.0.0 - com.mycompany.app my-app 1 - com.fasterxml.jackson.core @@ -90,11 +85,9 @@ void removeRedundantMavenDependencyInTestScope() { """ 4.0.0 - com.mycompany.app my-app 1 - org.junit.jupiter @@ -114,11 +107,9 @@ void removeRedundantMavenDependencyInTestScope() { """ 4.0.0 - com.mycompany.app my-app 1 - org.junit.jupiter @@ -138,18 +129,16 @@ void removeRedundantMavenDependencyInTestScope() { void removeMultipleRedundantDependencies() { rewriteRun( spec -> spec.recipe(new RemoveRedundantDependencies( - "com.fasterxml.jackson.core", "jackson-databind")), + "com.fasterxml.jackson.core", "jackson-databind")), mavenProject("my-app", //language=xml pomXml( """ 4.0.0 - com.mycompany.app my-app 1 - com.fasterxml.jackson.core @@ -172,11 +161,9 @@ void removeMultipleRedundantDependencies() { """ 4.0.0 - com.mycompany.app my-app 1 - com.fasterxml.jackson.core @@ -195,18 +182,16 @@ void removeMultipleRedundantDependencies() { void doNotRemoveWhenVersionsDiffer() { rewriteRun( spec -> spec.recipe(new RemoveRedundantDependencies( - "com.fasterxml.jackson.core", "jackson-databind")), + "com.fasterxml.jackson.core", "jackson-databind")), mavenProject("my-app", //language=xml pomXml( """ 4.0.0 - com.mycompany.app my-app 1 - com.fasterxml.jackson.core @@ -237,11 +222,9 @@ void doNoRemoveWhenExcluded() { """ 4.0.0 - com.mycompany.app my-app 1 - com.fasterxml.jackson.core @@ -271,18 +254,16 @@ void doNoRemoveWhenExcluded() { void doNotRemoveDirectDependencyItself() { rewriteRun( spec -> spec.recipe(new RemoveRedundantDependencies( - "com.fasterxml.jackson.core", "jackson-databind")), + "com.fasterxml.jackson.core", "jackson-databind")), mavenProject("my-app", //language=xml pomXml( """ 4.0.0 - com.mycompany.app my-app 1 - com.fasterxml.jackson.core @@ -301,18 +282,16 @@ void doNotRemoveDirectDependencyItself() { void noMatchingParentDependency() { rewriteRun( spec -> spec.recipe(new RemoveRedundantDependencies( - "org.nonexistent", "nonexistent")), + "org.nonexistent", "nonexistent")), mavenProject("my-app", //language=xml pomXml( """ 4.0.0 - com.mycompany.app my-app 1 - com.fasterxml.jackson.core @@ -337,7 +316,7 @@ void removeRedundantGradleDependency() { rewriteRun( spec -> spec.beforeRecipe(withToolingApi()) .recipe(new RemoveRedundantDependencies( - "com.fasterxml.jackson.core", "jackson-databind")), + "com.fasterxml.jackson.core", "jackson-databind")), mavenProject("my-app", //language=groovy buildGradle( @@ -345,11 +324,9 @@ void removeRedundantGradleDependency() { plugins { id 'java-library' } - repositories { mavenCentral() } - dependencies { implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.0' implementation 'com.fasterxml.jackson.core:jackson-core:2.17.0' @@ -359,11 +336,9 @@ void removeRedundantGradleDependency() { plugins { id 'java-library' } - repositories { mavenCentral() } - dependencies { implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.0' }