From 0916af008163f8c42907bf468da46bb49aa61360 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Fri, 29 May 2026 00:35:14 +0200 Subject: [PATCH 1/3] Trust resolved version over declared dep in dependency search PR #179 extended ModuleHasDependency / RepositoryHasDependency to also match against requested/declared dependencies so they still match when resolution fails. This introduced false positives whenever a version range is supplied: the declared version string could satisfy the comparator even when the resolved version did not (e.g. a Gradle resolutionStrategy.force or platform alignment overriding the declared coordinate). - When a coordinate is present in the resolved dependencies, trust the resolved check and skip the declared-dependency fallback for that group:artifact. Only fall back for declared deps not already resolved. - Tighten versionMatches so a null declared version no longer auto-matches when a version constraint is supplied (same treatment as a ${...} property reference). Adds regression tests covering both rules in ModuleHasDependencyTest and RepositoryHasDependencyTest. --- .../search/ModuleHasDependency.java | 20 ++++-- .../search/RepositoryHasDependency.java | 22 +++++-- .../search/ModuleHasDependencyTest.java | 61 +++++++++++++++++++ .../search/RepositoryHasDependencyTest.java | 61 +++++++++++++++++++ 4 files changed, 156 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/openrewrite/java/dependencies/search/ModuleHasDependency.java b/src/main/java/org/openrewrite/java/dependencies/search/ModuleHasDependency.java index 0b73c394..469fc9df 100644 --- a/src/main/java/org/openrewrite/java/dependencies/search/ModuleHasDependency.java +++ b/src/main/java/org/openrewrite/java/dependencies/search/ModuleHasDependency.java @@ -112,12 +112,17 @@ private boolean hasDependency(Tree tree) { if (mavenResult != null) { Scope requestedScope = scope == null ? null : Scope.fromName(scope); List dependencies = mavenResult.findDependencies(groupIdPattern, artifactIdPattern, requestedScope); + Set resolvedGAs = new HashSet<>(); for (ResolvedDependency dependency : dependencies) { + resolvedGAs.add(dependency.getGroupId() + ":" + dependency.getArtifactId()); if (versionComparator == null || versionComparator.isValid(null, dependency.getVersion())) { return true; } } for (Dependency requested : mavenResult.getPom().getRequestedDependencies()) { + if (resolvedGAs.contains(requested.getGroupId() + ":" + requested.getArtifactId())) { + continue; + } if (matchesRequested(requested, requestedScope, versionComparator)) { return true; } @@ -127,16 +132,23 @@ private boolean hasDependency(Tree tree) { GradleProject gp = tree.getMarkers().findFirst(GradleProject.class).orElse(null); if (gp != null) { + Set resolvedGAs = new HashSet<>(); for (GradleDependencyConfiguration c : gp.getConfigurations()) { for (ResolvedDependency resolvedDependency : c.getDirectResolved()) { ResolvedDependency found = resolvedDependency.findDependency(groupIdPattern, artifactIdPattern); - if (found != null && (versionComparator == null || versionComparator.isValid(null, found.getVersion()))) { - return true; + if (found != null) { + resolvedGAs.add(found.getGroupId() + ":" + found.getArtifactId()); + if (versionComparator == null || versionComparator.isValid(null, found.getVersion())) { + return true; + } } } } for (GradleDependencyConfiguration c : gp.getConfigurations()) { for (Dependency requested : c.getRequested()) { + if (resolvedGAs.contains(requested.getGroupId() + ":" + requested.getArtifactId())) { + continue; + } if (matchesRequested(requested, null, versionComparator)) { return true; } @@ -166,10 +178,10 @@ private boolean matchesRequested(Dependency dep, @Nullable Scope requestedScope, } private static boolean versionMatches(@Nullable String version, @Nullable VersionComparator cmp) { - if (cmp == null || version == null) { + if (cmp == null) { return true; } - if (version.startsWith("${")) { + if (version == null || version.startsWith("${")) { return false; } return cmp.isValid(null, version); diff --git a/src/main/java/org/openrewrite/java/dependencies/search/RepositoryHasDependency.java b/src/main/java/org/openrewrite/java/dependencies/search/RepositoryHasDependency.java index b982d0e2..59c48ee8 100644 --- a/src/main/java/org/openrewrite/java/dependencies/search/RepositoryHasDependency.java +++ b/src/main/java/org/openrewrite/java/dependencies/search/RepositoryHasDependency.java @@ -31,7 +31,9 @@ import org.openrewrite.semver.Semver; import org.openrewrite.semver.VersionComparator; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; @EqualsAndHashCode(callSuper = false) @@ -107,12 +109,17 @@ private boolean hasDependency(Tree tree) { if (mavenResult != null) { Scope requestedScope = scope == null ? null : Scope.fromName(scope); List dependencies = mavenResult.findDependencies(groupIdPattern, artifactIdPattern, requestedScope); + Set resolvedGAs = new HashSet<>(); for (ResolvedDependency dependency : dependencies) { + resolvedGAs.add(dependency.getGroupId() + ":" + dependency.getArtifactId()); if (versionComparator == null || versionComparator.isValid(null, dependency.getVersion())) { return true; } } for (Dependency requested : mavenResult.getPom().getRequestedDependencies()) { + if (resolvedGAs.contains(requested.getGroupId() + ":" + requested.getArtifactId())) { + continue; + } if (matchesRequested(requested, requestedScope, versionComparator)) { return true; } @@ -122,16 +129,23 @@ private boolean hasDependency(Tree tree) { GradleProject gp = tree.getMarkers().findFirst(GradleProject.class).orElse(null); if (gp != null) { + Set resolvedGAs = new HashSet<>(); for (GradleDependencyConfiguration c : gp.getConfigurations()) { for (ResolvedDependency resolvedDependency : c.getDirectResolved()) { ResolvedDependency found = resolvedDependency.findDependency(groupIdPattern, artifactIdPattern); - if (found != null && (versionComparator == null || versionComparator.isValid(null, found.getVersion()))) { - return true; + if (found != null) { + resolvedGAs.add(found.getGroupId() + ":" + found.getArtifactId()); + if (versionComparator == null || versionComparator.isValid(null, found.getVersion())) { + return true; + } } } } for (GradleDependencyConfiguration c : gp.getConfigurations()) { for (Dependency requested : c.getRequested()) { + if (resolvedGAs.contains(requested.getGroupId() + ":" + requested.getArtifactId())) { + continue; + } if (matchesRequested(requested, null, versionComparator)) { return true; } @@ -161,10 +175,10 @@ private boolean matchesRequested(Dependency dep, @Nullable Scope requestedScope, } private static boolean versionMatches(@Nullable String version, @Nullable VersionComparator cmp) { - if (cmp == null || version == null) { + if (cmp == null) { return true; } - if (version.startsWith("${")) { + if (version == null || version.startsWith("${")) { return false; } return cmp.isValid(null, version); diff --git a/src/test/java/org/openrewrite/java/dependencies/search/ModuleHasDependencyTest.java b/src/test/java/org/openrewrite/java/dependencies/search/ModuleHasDependencyTest.java index 0efe3269..44bf2bb7 100644 --- a/src/test/java/org/openrewrite/java/dependencies/search/ModuleHasDependencyTest.java +++ b/src/test/java/org/openrewrite/java/dependencies/search/ModuleHasDependencyTest.java @@ -454,6 +454,67 @@ void gradleVersionRangeOnRequestedDoesNotMatchWhenOutOfRange() { ) ); } + + @Language("groovy") + private final static String GradleNoRepositoriesNoVersion = """ + plugins { + id 'java-library' + } + dependencies { + implementation 'org.springframework:spring-beans' + } + """; + + @Test + void gradleRequestedWithoutVersionAndConstraintDoesNotMatch() { + // Resolution fails (no repositories), so the requested fallback fires. The declared + // dependency omits a version (supplied elsewhere by a platform/constraint), so its + // requested version is null and must NOT match the supplied version constraint (same + // treatment as a ${...} property reference). + rewriteRun( + spec -> spec.recipe(new ModuleHasDependency(GroupId, ArtifactId, null, "[1.0,)", null)), + mavenProject("project-gradle", + buildGradle(GradleNoRepositoriesNoVersion), + java(GradleJava) + ) + ); + } + } + + @Nested + class WhenResolvedVersionIsSourceOfTruth { + + @Language("groovy") + private final static String GradleForcedOutOfRange = """ + plugins { + id 'java-library' + } + repositories { + mavenCentral() + } + configurations.all { + resolutionStrategy { + force 'org.springframework:spring-beans:6.0.0' + } + } + dependencies { + implementation 'org.springframework:spring-beans:5.3.0' + } + """; + + @Test + void gradleVersionRangeDoesNotMatchDeclaredWhenResolvedVersionIsOutOfRange() { + // Declared spring-beans 5.3.0 (in range), but resolutionStrategy forces resolved 6.0.0 + // (out of range). The resolved version is the source of truth, so the declared-dependency + // fallback must be skipped for an already-resolved coordinate and [5.0,6.0) must NOT match. + rewriteRun( + spec -> spec.recipe(new ModuleHasDependency(GroupId, ArtifactId, null, "[5.0,6.0)", null)), + mavenProject("project-gradle", + buildGradle(GradleForcedOutOfRange), + java(GradleJava) + ) + ); + } } @Nested diff --git a/src/test/java/org/openrewrite/java/dependencies/search/RepositoryHasDependencyTest.java b/src/test/java/org/openrewrite/java/dependencies/search/RepositoryHasDependencyTest.java index c3058c0b..ddf6d071 100644 --- a/src/test/java/org/openrewrite/java/dependencies/search/RepositoryHasDependencyTest.java +++ b/src/test/java/org/openrewrite/java/dependencies/search/RepositoryHasDependencyTest.java @@ -15,12 +15,15 @@ */ package org.openrewrite.java.dependencies.search; +import org.intellij.lang.annotations.Language; import org.junit.jupiter.api.Test; import org.openrewrite.DocumentExample; import org.openrewrite.test.RecipeSpec; 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.java; import static org.openrewrite.java.Assertions.mavenProject; import static org.openrewrite.maven.Assertions.pomXml; @@ -84,4 +87,62 @@ void usedAsDeclarativePrecondition() { ) ); } + + @Language("java") + private final static String GradleJava = """ + public class AGradle {} + """; + + @Test + void gradleVersionRangeDoesNotMatchDeclaredWhenResolvedVersionIsOutOfRange() { + // Declared spring-beans 5.3.0 (in range), but resolutionStrategy forces resolved 6.0.0 + // (out of range). The resolved version is the source of truth, so the declared-dependency + // fallback must be skipped for an already-resolved coordinate and [5.0,6.0) must NOT match. + rewriteRun( + spec -> spec.recipe(new RepositoryHasDependency("org.springframework", "spring-beans", null, "[5.0,6.0)")), + mavenProject("project-gradle", + //language=groovy + buildGradle(""" + plugins { + id 'java-library' + } + repositories { + mavenCentral() + } + configurations.all { + resolutionStrategy { + force 'org.springframework:spring-beans:6.0.0' + } + } + dependencies { + implementation 'org.springframework:spring-beans:5.3.0' + } + """), + java(GradleJava) + ) + ); + } + + @Test + void gradleRequestedWithoutVersionAndConstraintDoesNotMatch() { + // Resolution fails (no repositories), so the requested fallback fires. The declared + // dependency omits a version (supplied elsewhere by a platform/constraint), so its + // requested version is null and must NOT match the supplied version constraint (same + // treatment as a ${...} property reference). + rewriteRun( + spec -> spec.recipe(new RepositoryHasDependency("org.springframework", "spring-beans", null, "[1.0,)")), + mavenProject("project-gradle", + //language=groovy + buildGradle(""" + plugins { + id 'java-library' + } + dependencies { + implementation 'org.springframework:spring-beans' + } + """), + java(GradleJava) + ) + ); + } } From 7b38702d44531e12d88fad1491abfe1a457c42e5 Mon Sep 17 00:00:00 2001 From: Jente Sondervorst Date: Fri, 29 May 2026 09:19:08 +0200 Subject: [PATCH 2/3] Apply suggestions from code review Co-authored-by: Jente Sondervorst --- .../dependencies/search/ModuleHasDependencyTest.java | 9 ++------- .../dependencies/search/RepositoryHasDependencyTest.java | 9 ++------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/src/test/java/org/openrewrite/java/dependencies/search/ModuleHasDependencyTest.java b/src/test/java/org/openrewrite/java/dependencies/search/ModuleHasDependencyTest.java index 44bf2bb7..9f2bad9b 100644 --- a/src/test/java/org/openrewrite/java/dependencies/search/ModuleHasDependencyTest.java +++ b/src/test/java/org/openrewrite/java/dependencies/search/ModuleHasDependencyTest.java @@ -467,10 +467,7 @@ void gradleVersionRangeOnRequestedDoesNotMatchWhenOutOfRange() { @Test void gradleRequestedWithoutVersionAndConstraintDoesNotMatch() { - // Resolution fails (no repositories), so the requested fallback fires. The declared - // dependency omits a version (supplied elsewhere by a platform/constraint), so its - // requested version is null and must NOT match the supplied version constraint (same - // treatment as a ${...} property reference). + // Force resolution failure (no repositories), so the requested fallback fires. rewriteRun( spec -> spec.recipe(new ModuleHasDependency(GroupId, ArtifactId, null, "[1.0,)", null)), mavenProject("project-gradle", @@ -504,9 +501,7 @@ class WhenResolvedVersionIsSourceOfTruth { @Test void gradleVersionRangeDoesNotMatchDeclaredWhenResolvedVersionIsOutOfRange() { - // Declared spring-beans 5.3.0 (in range), but resolutionStrategy forces resolved 6.0.0 - // (out of range). The resolved version is the source of truth, so the declared-dependency - // fallback must be skipped for an already-resolved coordinate and [5.0,6.0) must NOT match. + // The declared-dependency fallback must be skipped for an already-resolved coordinate (resolutionStrategy). rewriteRun( spec -> spec.recipe(new ModuleHasDependency(GroupId, ArtifactId, null, "[5.0,6.0)", null)), mavenProject("project-gradle", diff --git a/src/test/java/org/openrewrite/java/dependencies/search/RepositoryHasDependencyTest.java b/src/test/java/org/openrewrite/java/dependencies/search/RepositoryHasDependencyTest.java index ddf6d071..5a5bf1a8 100644 --- a/src/test/java/org/openrewrite/java/dependencies/search/RepositoryHasDependencyTest.java +++ b/src/test/java/org/openrewrite/java/dependencies/search/RepositoryHasDependencyTest.java @@ -95,9 +95,7 @@ public class AGradle {} @Test void gradleVersionRangeDoesNotMatchDeclaredWhenResolvedVersionIsOutOfRange() { - // Declared spring-beans 5.3.0 (in range), but resolutionStrategy forces resolved 6.0.0 - // (out of range). The resolved version is the source of truth, so the declared-dependency - // fallback must be skipped for an already-resolved coordinate and [5.0,6.0) must NOT match. + // The declared-dependency fallback must be skipped for an already-resolved coordinate (resolutionStrategy). rewriteRun( spec -> spec.recipe(new RepositoryHasDependency("org.springframework", "spring-beans", null, "[5.0,6.0)")), mavenProject("project-gradle", @@ -125,10 +123,7 @@ void gradleVersionRangeDoesNotMatchDeclaredWhenResolvedVersionIsOutOfRange() { @Test void gradleRequestedWithoutVersionAndConstraintDoesNotMatch() { - // Resolution fails (no repositories), so the requested fallback fires. The declared - // dependency omits a version (supplied elsewhere by a platform/constraint), so its - // requested version is null and must NOT match the supplied version constraint (same - // treatment as a ${...} property reference). + // Force resolution failure (no repositories), so the requested fallback fires. rewriteRun( spec -> spec.recipe(new RepositoryHasDependency("org.springframework", "spring-beans", null, "[1.0,)")), mavenProject("project-gradle", From 9f30338aad38ffc1446b84b30db84dc16c40017a Mon Sep 17 00:00:00 2001 From: Jente Sondervorst Date: Fri, 29 May 2026 09:29:49 +0200 Subject: [PATCH 3/3] Add Maven regression tests for BOM-managed dependency version range --- .../search/ModuleHasDependencyTest.java | 38 +++++++++++++++++++ .../search/RepositoryHasDependencyTest.java | 35 +++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/src/test/java/org/openrewrite/java/dependencies/search/ModuleHasDependencyTest.java b/src/test/java/org/openrewrite/java/dependencies/search/ModuleHasDependencyTest.java index 9f2bad9b..a5ae2358 100644 --- a/src/test/java/org/openrewrite/java/dependencies/search/ModuleHasDependencyTest.java +++ b/src/test/java/org/openrewrite/java/dependencies/search/ModuleHasDependencyTest.java @@ -510,6 +510,44 @@ void gradleVersionRangeDoesNotMatchDeclaredWhenResolvedVersionIsOutOfRange() { ) ); } + + @Language("xml") + private final static String MavenBomManagedOutOfRange = """ + + com.example + foo + 1.0.0 + + + + org.springframework.boot + spring-boot-dependencies + 3.0.0 + pom + import + + + + + + org.springframework + spring-beans + + + + """; + + @Test + void mavenVersionRangeDoesNotMatchBomManagedDependencyWhenResolvedIsOutOfRange() { + // Regression for rewrite-third-party#76: BOM-managed version (null on requested) must not match the range. + rewriteRun( + spec -> spec.recipe(new ModuleHasDependency(GroupId, ArtifactId, null, "[5.0,6.0)", null)), + mavenProject("project-maven", + pomXml(MavenBomManagedOutOfRange), + java(MavenJava) + ) + ); + } } @Nested diff --git a/src/test/java/org/openrewrite/java/dependencies/search/RepositoryHasDependencyTest.java b/src/test/java/org/openrewrite/java/dependencies/search/RepositoryHasDependencyTest.java index 5a5bf1a8..becc7434 100644 --- a/src/test/java/org/openrewrite/java/dependencies/search/RepositoryHasDependencyTest.java +++ b/src/test/java/org/openrewrite/java/dependencies/search/RepositoryHasDependencyTest.java @@ -121,6 +121,41 @@ void gradleVersionRangeDoesNotMatchDeclaredWhenResolvedVersionIsOutOfRange() { ); } + @Test + void mavenVersionRangeDoesNotMatchBomManagedDependencyWhenResolvedIsOutOfRange() { + // Regression for rewrite-third-party#76: BOM-managed version (null on requested) must not match the range. + rewriteRun( + spec -> spec.recipe(new RepositoryHasDependency("org.springframework", "spring-beans", null, "[5.0,6.0)")), + mavenProject("project-maven", + //language=xml + pomXml(""" + + com.example + foo + 1.0.0 + + + + org.springframework.boot + spring-boot-dependencies + 3.0.0 + pom + import + + + + + + org.springframework + spring-beans + + + + """) + ) + ); + } + @Test void gradleRequestedWithoutVersionAndConstraintDoesNotMatch() { // Force resolution failure (no repositories), so the requested fallback fires.