From 4346dc7a68458285bd9e80bffeb711d45a6761a7 Mon Sep 17 00:00:00 2001 From: Knut Wannheden Date: Tue, 24 Jun 2025 18:12:15 +0200 Subject: [PATCH 1/5] Improve `LatestRelease#compare()` Properly check for known and unknown qualifiers. --- .../org/openrewrite/semver/LatestRelease.java | 47 +++++++++++++++---- .../openrewrite/semver/LatestReleaseTest.java | 16 +++++++ 2 files changed, 55 insertions(+), 8 deletions(-) diff --git a/rewrite-core/src/main/java/org/openrewrite/semver/LatestRelease.java b/rewrite-core/src/main/java/org/openrewrite/semver/LatestRelease.java index 03bfdf53251..ed9e6a165b0 100644 --- a/rewrite-core/src/main/java/org/openrewrite/semver/LatestRelease.java +++ b/rewrite-core/src/main/java/org/openrewrite/semver/LatestRelease.java @@ -18,9 +18,31 @@ import org.jspecify.annotations.Nullable; import org.openrewrite.Validated; +import java.util.HashMap; +import java.util.Map; import java.util.regex.Matcher; public class LatestRelease implements VersionComparator { + + private static final Map QUALIFIER_PRIORITY = new HashMap<>(); + + static { + QUALIFIER_PRIORITY.put("alpha", 1); + QUALIFIER_PRIORITY.put("a", 1); + QUALIFIER_PRIORITY.put("beta", 2); + QUALIFIER_PRIORITY.put("b", 2); + QUALIFIER_PRIORITY.put("milestone", 3); + QUALIFIER_PRIORITY.put("m", 3); + QUALIFIER_PRIORITY.put("rc", 4); + QUALIFIER_PRIORITY.put("cr", 4); + QUALIFIER_PRIORITY.put("snapshot", 5); + QUALIFIER_PRIORITY.put("", 6); + QUALIFIER_PRIORITY.put("ga", 6); + QUALIFIER_PRIORITY.put("final", 6); + QUALIFIER_PRIORITY.put("release", 6); + QUALIFIER_PRIORITY.put("sp", 7); + } + @Nullable private final String metadataPattern; @@ -152,19 +174,28 @@ public int compare(@Nullable String currentVersion, String v1, String v2) { // When all numeric parts are equal, we need to handle pre-release versions properly // A pre-release version should be considered less than a release version // e.g., "3.5.0-RC1" < "3.5.0" - boolean v1IsPreRelease = v1Gav.group(6) != null && PRE_RELEASE_ENDING.matcher(v1Gav.group(6)).find(); - boolean v2IsPreRelease = v2Gav.group(6) != null && PRE_RELEASE_ENDING.matcher(v2Gav.group(6)).find(); - - if (v1IsPreRelease && !v2IsPreRelease) { - return -1; // v1 is pre-release, v2 is not, so v1 < v2 - } else if (!v1IsPreRelease && v2IsPreRelease) { - return 1; // v1 is not pre-release, v2 is, so v1 > v2 + String v1Qualifier = extractQualifier(v1Gav.group(6)); + String v2Qualifier = extractQualifier(v2Gav.group(6)); + + int v1Prio = QUALIFIER_PRIORITY.getOrDefault(v1Qualifier, 0); + int v2Prio = QUALIFIER_PRIORITY.getOrDefault(v2Qualifier, 0); + + if (v1Prio != v2Prio) { + return Integer.compare(v1Prio, v2Prio); } - + // Both are either pre-release or release versions, do string comparison return normalized1.compareTo(normalized2); } + private static String extractQualifier(@Nullable String suffix) { + if (suffix == null) { + return ""; + } + int endIdx = Math.max(suffix.lastIndexOf('.'), suffix.lastIndexOf('-')); + return suffix.substring(1, endIdx > 0 ? endIdx : suffix.length()); + } + public static Validated buildLatestRelease(String toVersion, @Nullable String metadataPattern) { return "latest.release".equalsIgnoreCase(toVersion) || "latest.major".equalsIgnoreCase(toVersion) ? Validated.valid("latestRelease", new LatestRelease(metadataPattern)) : diff --git a/rewrite-core/src/test/java/org/openrewrite/semver/LatestReleaseTest.java b/rewrite-core/src/test/java/org/openrewrite/semver/LatestReleaseTest.java index a808c0eb42b..21a9208a284 100644 --- a/rewrite-core/src/test/java/org/openrewrite/semver/LatestReleaseTest.java +++ b/rewrite-core/src/test/java/org/openrewrite/semver/LatestReleaseTest.java @@ -119,6 +119,22 @@ void releaseOrLatestKeyword_beatsEverything() { assertThat(latestRelease.compare(null, "reLeaSE", "1.2.3")).isPositive(); } + @Test + void compareQualifiers() { + assertThat(latestRelease.compare(null, "1.2.3.final", "1.2.3")).isEqualTo(0); + assertThat(latestRelease.compare(null, "1.2.3.ga", "1.2.3")).isEqualTo(0); + assertThat(latestRelease.compare(null, "1.2.3.release", "1.2.3")).isEqualTo(0); + + assertThat(latestRelease.compare(null, "1.2.3.alpha-1", "1.2.3")).isNegative(); + assertThat(latestRelease.compare(null, "1.2.3.alpha-1", "1.2.3.alpha-2")).isNegative(); + assertThat(latestRelease.compare(null, "1.2.3.m", "1.2.3")).isNegative(); + + assertThat(latestRelease.compare(null, "1.2.3.sp", "1.2.3")).isPositive(); + assertThat(latestRelease.compare(null, "1.2.3.sp", "1.2.3.final")).isPositive(); + + assertThat(latestRelease.compare(null, "1.2.3.mr", "1.2.3")).isNegative(); + } + @Test void latestKeywordIsNewerThanReleaseKeyword() { assertThat(latestRelease.compare(null, "RELEASE", "LATEST")).isNegative(); From d5f2a96f31224618e1298c82f7727950636ba28a Mon Sep 17 00:00:00 2001 From: Knut Wannheden Date: Tue, 24 Jun 2025 18:19:27 +0200 Subject: [PATCH 2/5] Polish --- .../org/openrewrite/semver/LatestRelease.java | 57 ++++++++++--------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/rewrite-core/src/main/java/org/openrewrite/semver/LatestRelease.java b/rewrite-core/src/main/java/org/openrewrite/semver/LatestRelease.java index ed9e6a165b0..c37cc2ac6b1 100644 --- a/rewrite-core/src/main/java/org/openrewrite/semver/LatestRelease.java +++ b/rewrite-core/src/main/java/org/openrewrite/semver/LatestRelease.java @@ -18,31 +18,10 @@ import org.jspecify.annotations.Nullable; import org.openrewrite.Validated; -import java.util.HashMap; -import java.util.Map; import java.util.regex.Matcher; public class LatestRelease implements VersionComparator { - private static final Map QUALIFIER_PRIORITY = new HashMap<>(); - - static { - QUALIFIER_PRIORITY.put("alpha", 1); - QUALIFIER_PRIORITY.put("a", 1); - QUALIFIER_PRIORITY.put("beta", 2); - QUALIFIER_PRIORITY.put("b", 2); - QUALIFIER_PRIORITY.put("milestone", 3); - QUALIFIER_PRIORITY.put("m", 3); - QUALIFIER_PRIORITY.put("rc", 4); - QUALIFIER_PRIORITY.put("cr", 4); - QUALIFIER_PRIORITY.put("snapshot", 5); - QUALIFIER_PRIORITY.put("", 6); - QUALIFIER_PRIORITY.put("ga", 6); - QUALIFIER_PRIORITY.put("final", 6); - QUALIFIER_PRIORITY.put("release", 6); - QUALIFIER_PRIORITY.put("sp", 7); - } - @Nullable private final String metadataPattern; @@ -174,11 +153,8 @@ public int compare(@Nullable String currentVersion, String v1, String v2) { // When all numeric parts are equal, we need to handle pre-release versions properly // A pre-release version should be considered less than a release version // e.g., "3.5.0-RC1" < "3.5.0" - String v1Qualifier = extractQualifier(v1Gav.group(6)); - String v2Qualifier = extractQualifier(v2Gav.group(6)); - - int v1Prio = QUALIFIER_PRIORITY.getOrDefault(v1Qualifier, 0); - int v2Prio = QUALIFIER_PRIORITY.getOrDefault(v2Qualifier, 0); + int v1Prio = qualifierPriority(v1Gav.group(6)); + int v2Prio = qualifierPriority(v2Gav.group(6)); if (v1Prio != v2Prio) { return Integer.compare(v1Prio, v2Prio); @@ -188,6 +164,35 @@ public int compare(@Nullable String currentVersion, String v1, String v2) { return normalized1.compareTo(normalized2); } + private static int qualifierPriority(@Nullable String suffix) { + String qualifier = extractQualifier(suffix); + switch (qualifier) { + case "alpha": + case "a": + return 1; + case "beta": + case "b": + return 2; + case "milestone": + case "m": + return 3; + case "rc": + case "cr": + return 4; + case "snapshot": + return 5; + case "": + case "ga": + case "final": + case "release": + return 6; + case "sp": + return 7; + default: + return 0; + } + } + private static String extractQualifier(@Nullable String suffix) { if (suffix == null) { return ""; From acfbb205f87cd5cb1241d053d8aabe5d46f91ae3 Mon Sep 17 00:00:00 2001 From: Knut Wannheden Date: Tue, 24 Jun 2025 19:10:48 +0200 Subject: [PATCH 3/5] Fix `LatestRelease#compare()` for metadata --- .../org/openrewrite/semver/LatestRelease.java | 13 +++++---- .../openrewrite/semver/LatestPatchTest.java | 28 +++++++++---------- .../openrewrite/semver/TildeRangeTest.java | 1 - 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/rewrite-core/src/main/java/org/openrewrite/semver/LatestRelease.java b/rewrite-core/src/main/java/org/openrewrite/semver/LatestRelease.java index c37cc2ac6b1..c8da639c962 100644 --- a/rewrite-core/src/main/java/org/openrewrite/semver/LatestRelease.java +++ b/rewrite-core/src/main/java/org/openrewrite/semver/LatestRelease.java @@ -117,18 +117,19 @@ public int compare(@Nullable String currentVersion, String v1, String v2) { nv1 = nv1Builder.toString(); } - Matcher v1Gav = VersionComparator.RELEASE_PATTERN.matcher(nv1); - Matcher v2Gav = VersionComparator.RELEASE_PATTERN.matcher(nv2); - - v1Gav.find(); - v2Gav.find(); - // Remove the metadata pattern from the normalized versions, this only impacts the comparison when all version // parts are the same: // // HyphenRange [25-28] should include "28-jre" and "28-android" as possible candidates. String normalized1 = metadataPattern == null ? nv1 : nv1.replaceAll(metadataPattern, ""); String normalized2 = metadataPattern == null ? nv2 : nv2.replaceAll(metadataPattern, ""); + + Matcher v1Gav = VersionComparator.RELEASE_PATTERN.matcher(normalized1); + Matcher v2Gav = VersionComparator.RELEASE_PATTERN.matcher(normalized2); + + v1Gav.find(); + v2Gav.find(); + try { for (int i = 1; i <= Math.max(vp1, vp2); i++) { String v1Part = v1Gav.group(i); diff --git a/rewrite-core/src/test/java/org/openrewrite/semver/LatestPatchTest.java b/rewrite-core/src/test/java/org/openrewrite/semver/LatestPatchTest.java index ce14d28e483..2e9f1b6cae3 100644 --- a/rewrite-core/src/test/java/org/openrewrite/semver/LatestPatchTest.java +++ b/rewrite-core/src/test/java/org/openrewrite/semver/LatestPatchTest.java @@ -48,25 +48,25 @@ void noSnapshots() { @Test void upgrade() { var upgrade = latestPatch.upgrade("2.10.10.3.24", List.of("2.10.0")); - assertThat(upgrade.isPresent()).isFalse(); + assertThat(upgrade).isEmpty(); upgrade = latestPatch.upgrade("2.10.10.3.24", List.of("2.11.0")); - assertThat(upgrade.isPresent()).isFalse(); + assertThat(upgrade).isEmpty(); upgrade = latestPatch.upgrade("2.10.10.3.24", List.of("2.10.9")); - assertThat(upgrade.isPresent()).isFalse(); + assertThat(upgrade).isEmpty(); upgrade = latestPatch.upgrade("2.10.10.3.24", List.of("2.10.11")); - assertThat(upgrade.isPresent()).isTrue(); + assertThat(upgrade).isPresent(); assertThat(upgrade.get()).isEqualTo("2.10.11"); upgrade = latestPatch.upgrade("2.10.10.3.24", List.of("2.10.10.3.23")); - assertThat(upgrade.isPresent()).isFalse(); + assertThat(upgrade).isEmpty(); upgrade = latestPatch.upgrade("2.10.10.3.24", List.of("2.10.10.2.25")); - assertThat(upgrade.isPresent()).isFalse(); + assertThat(upgrade).isEmpty(); upgrade = latestPatch.upgrade("2.10.10.3.24", List.of("2.10.10.3.25")); - assertThat(upgrade.isPresent()).isTrue(); + assertThat(upgrade).isPresent(); assertThat(upgrade.get()).isEqualTo("2.10.10.3.25"); } @@ -86,25 +86,25 @@ void metadataValid() { @Test void metadataUpgrade() { var upgrade = latestMetadataPatch.upgrade("2.10.10.3.24-fred", List.of("2.10.0-fred")); - assertThat(upgrade.isPresent()).isFalse(); + assertThat(upgrade).isEmpty(); upgrade = latestMetadataPatch.upgrade("2.10.10.3.24-fred", List.of("2.11.0-fred")); - assertThat(upgrade.isPresent()).isFalse(); + assertThat(upgrade).isEmpty(); upgrade = latestMetadataPatch.upgrade("2.10.10.3.24-fred", List.of("2.10.9-fred")); - assertThat(upgrade.isPresent()).isFalse(); + assertThat(upgrade).isEmpty(); upgrade = latestMetadataPatch.upgrade("2.10.10.3.24-fred", List.of("2.10.11-fred")); - assertThat(upgrade.isPresent()).isTrue(); + assertThat(upgrade).isPresent(); assertThat(upgrade.get()).isEqualTo("2.10.11-fred"); upgrade = latestMetadataPatch.upgrade("2.10.10.3.24-fred", List.of("2.10.10.3.23-fred")); - assertThat(upgrade.isPresent()).isFalse(); + assertThat(upgrade).isEmpty(); upgrade = latestMetadataPatch.upgrade("2.10.10.3.24-fred", List.of("2.10.10.2.25-fred")); - assertThat(upgrade.isPresent()).isFalse(); + assertThat(upgrade).isEmpty(); upgrade = latestMetadataPatch.upgrade("2.10.10.3.24-fred", List.of("2.10.10.3.25-fred")); - assertThat(upgrade.isPresent()).isTrue(); + assertThat(upgrade).isPresent(); assertThat(upgrade.get()).isEqualTo("2.10.10.3.25-fred"); } diff --git a/rewrite-core/src/test/java/org/openrewrite/semver/TildeRangeTest.java b/rewrite-core/src/test/java/org/openrewrite/semver/TildeRangeTest.java index 8cfe83d3ea8..78f3c60646e 100644 --- a/rewrite-core/src/test/java/org/openrewrite/semver/TildeRangeTest.java +++ b/rewrite-core/src/test/java/org/openrewrite/semver/TildeRangeTest.java @@ -52,7 +52,6 @@ void updatePatch() { assertThat(tildeRange.isValid("1.0", "1.2.3.RELEASE")).isTrue(); assertThat(tildeRange.isValid("1.0", "1.2.4")).isTrue(); assertThat(tildeRange.isValid("1.0", "1.3.0")).isFalse(); - assertThat(tildeRange.isValid("1.0", "1.2.3.v20130506")).isTrue(); } @Test From d3e240cfbd231bb7ae7f645f2df11a11aa92c7e8 Mon Sep 17 00:00:00 2001 From: Knut Wannheden Date: Tue, 24 Jun 2025 20:34:33 +0200 Subject: [PATCH 4/5] Better align with Maven --- .../java/org/openrewrite/semver/ExactVersion.java | 2 +- .../java/org/openrewrite/semver/LatestRelease.java | 13 ++++++++++--- .../org/openrewrite/semver/LatestReleaseTest.java | 8 ++++---- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/rewrite-core/src/main/java/org/openrewrite/semver/ExactVersion.java b/rewrite-core/src/main/java/org/openrewrite/semver/ExactVersion.java index 54098d38546..f6b3abb0c6c 100755 --- a/rewrite-core/src/main/java/org/openrewrite/semver/ExactVersion.java +++ b/rewrite-core/src/main/java/org/openrewrite/semver/ExactVersion.java @@ -28,7 +28,7 @@ public class ExactVersion extends LatestRelease { String version; public ExactVersion(String pattern) { - super(pattern); + super(null); if (pattern.startsWith("=")) { this.version = pattern.substring(1); } else { diff --git a/rewrite-core/src/main/java/org/openrewrite/semver/LatestRelease.java b/rewrite-core/src/main/java/org/openrewrite/semver/LatestRelease.java index c8da639c962..2e5c7b148b8 100644 --- a/rewrite-core/src/main/java/org/openrewrite/semver/LatestRelease.java +++ b/rewrite-core/src/main/java/org/openrewrite/semver/LatestRelease.java @@ -190,7 +190,7 @@ private static int qualifierPriority(@Nullable String suffix) { case "sp": return 7; default: - return 0; + return 8; } } @@ -198,8 +198,15 @@ private static String extractQualifier(@Nullable String suffix) { if (suffix == null) { return ""; } - int endIdx = Math.max(suffix.lastIndexOf('.'), suffix.lastIndexOf('-')); - return suffix.substring(1, endIdx > 0 ? endIdx : suffix.length()); + StringBuilder builder = new StringBuilder(); + for (int i = 1; i < suffix.length(); i++) { + if (Character.isLetter(suffix.charAt(i))) { + builder.append(Character.toLowerCase(suffix.charAt(i))); + } else { + break; + } + } + return builder.toString(); } public static Validated buildLatestRelease(String toVersion, @Nullable String metadataPattern) { diff --git a/rewrite-core/src/test/java/org/openrewrite/semver/LatestReleaseTest.java b/rewrite-core/src/test/java/org/openrewrite/semver/LatestReleaseTest.java index 21a9208a284..f2b18fedc46 100644 --- a/rewrite-core/src/test/java/org/openrewrite/semver/LatestReleaseTest.java +++ b/rewrite-core/src/test/java/org/openrewrite/semver/LatestReleaseTest.java @@ -132,7 +132,7 @@ void compareQualifiers() { assertThat(latestRelease.compare(null, "1.2.3.sp", "1.2.3")).isPositive(); assertThat(latestRelease.compare(null, "1.2.3.sp", "1.2.3.final")).isPositive(); - assertThat(latestRelease.compare(null, "1.2.3.mr", "1.2.3")).isNegative(); + assertThat(latestRelease.compare(null, "1.2.3.mr", "1.2.3")).isPositive(); } @Test @@ -196,8 +196,8 @@ void preReleaseVersionsShouldBeLessThanReleaseVersions() { assertThat(latestRelease.compare(null, "3.5.0", "3.5.0-RC1")).isGreaterThan(0); assertThat(latestRelease.compare(null, "3.5.0-RC1", "3.5.0-RC2")).isLessThan(0); assertThat(latestRelease.compare(null, "3.5.0-alpha", "3.5.0-beta")).isLessThan(0); - // String comparison: "beta" > "RC1" lexicographically - assertThat(latestRelease.compare(null, "3.5.0-beta", "3.5.0-RC1")).isGreaterThan(0); - assertThat(latestRelease.compare(null, "3.5.0-RC1", "3.5.0-beta")).isLessThan(0); + // "RC" is a known qualifier and "beta" is not + assertThat(latestRelease.compare(null, "3.5.0-beta", "3.5.0-RC1")).isLessThan(0); + assertThat(latestRelease.compare(null, "3.5.0-RC1", "3.5.0-beta")).isGreaterThan(0); } } From 723c85fb5c025e2b27f63a51491e77311faf4eac Mon Sep 17 00:00:00 2001 From: Knut Wannheden Date: Tue, 24 Jun 2025 20:39:48 +0200 Subject: [PATCH 5/5] Restore test case --- .../src/test/java/org/openrewrite/semver/TildeRangeTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/rewrite-core/src/test/java/org/openrewrite/semver/TildeRangeTest.java b/rewrite-core/src/test/java/org/openrewrite/semver/TildeRangeTest.java index 78f3c60646e..8cfe83d3ea8 100644 --- a/rewrite-core/src/test/java/org/openrewrite/semver/TildeRangeTest.java +++ b/rewrite-core/src/test/java/org/openrewrite/semver/TildeRangeTest.java @@ -52,6 +52,7 @@ void updatePatch() { assertThat(tildeRange.isValid("1.0", "1.2.3.RELEASE")).isTrue(); assertThat(tildeRange.isValid("1.0", "1.2.4")).isTrue(); assertThat(tildeRange.isValid("1.0", "1.3.0")).isFalse(); + assertThat(tildeRange.isValid("1.0", "1.2.3.v20130506")).isTrue(); } @Test