Skip to content

Commit 4b10319

Browse files
committed
Cleanup Version implementation
Signed-off-by: BoykoAlex <alex.boyko@broadcom.com>
1 parent 30865a5 commit 4b10319

2 files changed

Lines changed: 57 additions & 137 deletions

File tree

  • headless-services/commons/commons-lsp-extensions/src

headless-services/commons/commons-lsp-extensions/src/main/java/org/springframework/ide/vscode/commons/Version.java

Lines changed: 31 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ public final class Version implements Comparable<Version> {
3333
// Groups: 1=major, 2=minor, 3=patch, 4=build (4th numeric part), 5=fifth part, 6=qualifier suffix
3434
private static final Pattern RELEASE_PATTERN = Pattern.compile(
3535
"(\\d+)(?:\\.(\\d+))?(?:\\.(\\d+))?(?:\\.(\\d+))?(?:\\.(\\d+))?([-.+].*)?");
36-
private static final String[] RELEASE_SUFFIXES = { ".final", ".ga", ".release" };
3736
private static final int QUALIFIER_GROUP = 6;
3837

3938
private static final Pattern QUALIFIER_TYPE_PATTERN =
@@ -81,7 +80,9 @@ public boolean isSnapshot() {
8180
/** Qualifier text without its leading separator (e.g. {@code "M1"}, {@code "SNAPSHOT"}), or {@code null}. */
8281
private final String qualifier;
8382
private final ReleaseType releaseType;
84-
/** Original parsed string — used for display and comparison. */
83+
/** Numeric suffix of the qualifier (e.g. {@code 1} for {@code M1} or {@code RC2→2}); 0 if absent. */
84+
private final int qualifierNumber;
85+
/** Original parsed string — used for display. */
8586
private final String versionString;
8687

8788
private Version(int major, int minor, int patch, int build, String qualifier, String originalString) {
@@ -91,6 +92,7 @@ private Version(int major, int minor, int patch, int build, String qualifier, St
9192
this.build = build;
9293
this.qualifier = qualifier;
9394
this.releaseType = toReleaseType(qualifier);
95+
this.qualifierNumber = toQualifierNumber(qualifier);
9496
this.versionString = originalString;
9597
}
9698

@@ -123,6 +125,11 @@ public ReleaseType getReleaseType() {
123125
return releaseType;
124126
}
125127

128+
/** Numeric suffix of the qualifier; 0 when absent (e.g. {@code SNAPSHOT→0}, {@code M1→1}, {@code RC2→2}). */
129+
public int getQualifierNumber() {
130+
return qualifierNumber;
131+
}
132+
126133
/** True for GA releases and service packs. */
127134
public boolean isRelease() {
128135
return releaseType == ReleaseType.RELEASE || releaseType == ReleaseType.SERVICE_PACK;
@@ -160,7 +167,13 @@ public String toString() {
160167
*/
161168
@Override
162169
public int compareTo(Version o) {
163-
return compareVersionStrings(this.versionString, o.versionString);
170+
int d;
171+
if ((d = Integer.compare(major, o.major)) != 0) return d;
172+
if ((d = Integer.compare(minor, o.minor)) != 0) return d;
173+
if ((d = Integer.compare(patch, o.patch)) != 0) return d;
174+
if ((d = Integer.compare(build, o.build)) != 0) return d;
175+
if ((d = Integer.compare(releaseType.getPriority(), o.releaseType.getPriority())) != 0) return d;
176+
return Integer.compare(qualifierNumber, o.qualifierNumber);
164177
}
165178

166179
@Override
@@ -214,120 +227,6 @@ public static Version parse(String versionStr) {
214227
}
215228
}
216229

217-
// ---- comparison logic (derived from org.openrewrite.semver.LatestRelease, Apache 2.0) ----
218-
219-
private static int compareVersionStrings(String v1, String v2) {
220-
if (v1.equalsIgnoreCase(v2)) {
221-
return 0;
222-
}
223-
224-
String nv1 = normalizeVersion(v1);
225-
String nv2 = normalizeVersion(v2);
226-
227-
int vp1 = countVersionParts(nv1);
228-
int vp2 = countVersionParts(nv2);
229-
230-
// Pad the shorter version with ".0" segments so both have the same number of parts
231-
if (vp1 > vp2) {
232-
StringBuilder sb = new StringBuilder(nv2);
233-
for (int i = vp2; i < vp1; i++) sb.append(".0");
234-
nv2 = sb.toString();
235-
} else if (vp2 > vp1) {
236-
StringBuilder sb = new StringBuilder(nv1);
237-
for (int i = vp1; i < vp2; i++) sb.append(".0");
238-
nv1 = sb.toString();
239-
}
240-
241-
Matcher m1 = RELEASE_PATTERN.matcher(nv1);
242-
Matcher m2 = RELEASE_PATTERN.matcher(nv2);
243-
m1.find();
244-
m2.find();
245-
246-
int maxParts = Math.max(vp1, vp2);
247-
for (int i = 1; i <= maxParts; i++) {
248-
String p1 = m1.group(i);
249-
String p2 = m2.group(i);
250-
if (p1 == null) {
251-
return p2 == null ? nv1.compareTo(nv2) : -1;
252-
} else if (p2 == null) {
253-
return 1;
254-
}
255-
long diff = Long.parseLong(p1) - Long.parseLong(p2);
256-
if (diff != 0) {
257-
return diff > 0 ? 1 : -1;
258-
}
259-
}
260-
261-
// All numeric parts equal — compare by qualifier priority
262-
int prio1 = qualifierPriority(m1.group(QUALIFIER_GROUP));
263-
int prio2 = qualifierPriority(m2.group(QUALIFIER_GROUP));
264-
if (prio1 != prio2) {
265-
return Integer.compare(prio1, prio2);
266-
}
267-
return nv1.compareTo(nv2);
268-
}
269-
270-
/** Strips trailing {@code .final} / {@code .ga} / {@code .release} and pads to at least 3 parts. */
271-
private static String normalizeVersion(String version) {
272-
int lastDotIdx = version.lastIndexOf('.');
273-
for (String suffix : RELEASE_SUFFIXES) {
274-
if (version.regionMatches(true, lastDotIdx, suffix, 0, suffix.length())) {
275-
version = version.substring(0, lastDotIdx);
276-
break;
277-
}
278-
}
279-
int parts = countVersionParts(version);
280-
if (parts <= 2) {
281-
String[] split = version.split("(?=[-+])");
282-
for (; parts <= 2; parts++) {
283-
split[0] += ".0";
284-
}
285-
version = split.length > 1 ? split[0] + split[1] : split[0];
286-
}
287-
return version;
288-
}
289-
290-
private static int countVersionParts(String version) {
291-
int count = 0;
292-
int len = version.length();
293-
int lastSepIdx = -1;
294-
for (int i = 0; i < len; i++) {
295-
char c = version.charAt(i);
296-
if (c == '.' || c == '-' || c == '$') {
297-
if (lastSepIdx == i - 1) return count;
298-
lastSepIdx = i;
299-
} else if (lastSepIdx == i - 1) {
300-
if (!Character.isDigit(c)) break;
301-
count++;
302-
}
303-
}
304-
return count;
305-
}
306-
307-
private static int qualifierPriority(String suffix) {
308-
switch (extractQualifier(suffix)) {
309-
case "alpha": case "a": return 1;
310-
case "beta": case "b": return 2;
311-
case "milestone": case "m": return 3;
312-
case "rc": case "cr": return 4;
313-
case "snapshot": return 5;
314-
case "": case "ga": case "final": case "release": return 6;
315-
case "sp": return 7;
316-
default: return 8;
317-
}
318-
}
319-
320-
private static String extractQualifier(String suffix) {
321-
if (suffix == null) return "";
322-
StringBuilder sb = new StringBuilder();
323-
for (int i = 1; i < suffix.length(); i++) {
324-
char c = suffix.charAt(i);
325-
if (Character.isLetter(c)) sb.append(Character.toLowerCase(c));
326-
else break;
327-
}
328-
return sb.toString();
329-
}
330-
331230
// ---- parsing helpers ----
332231

333232
private static int parseGroup(Matcher m, int group) {
@@ -347,6 +246,18 @@ private static String stripSeparator(String qualifierWithSep) {
347246
return qualifierWithSep;
348247
}
349248

249+
private static int toQualifierNumber(String qualifier) {
250+
if (qualifier == null || qualifier.isEmpty()) {
251+
return 0;
252+
}
253+
Matcher m = QUALIFIER_TYPE_PATTERN.matcher(qualifier);
254+
if (m.matches()) {
255+
String num = m.group(2);
256+
return num.isEmpty() ? 0 : Integer.parseInt(num);
257+
}
258+
return 0;
259+
}
260+
350261
private static ReleaseType toReleaseType(String qualifier) {
351262
if (qualifier == null || qualifier.isEmpty()) {
352263
return ReleaseType.RELEASE;
@@ -355,6 +266,9 @@ private static ReleaseType toReleaseType(String qualifier) {
355266
if (lower.equals("release") || lower.equals("ga") || lower.equals("final")) {
356267
return ReleaseType.RELEASE;
357268
}
269+
if (lower.startsWith("snapshot")) {
270+
return ReleaseType.SNAPSHOT;
271+
}
358272
if (lower.startsWith("sp")) {
359273
return ReleaseType.SERVICE_PACK;
360274
}

headless-services/commons/commons-lsp-extensions/src/test/java/org/springframework/ide/vscode/commons/VersionTests.java

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -291,16 +291,13 @@ void toMajorMinorPatchVersionStr() {
291291

292292
@Test
293293
void sortAllThreePartVersionVariants() {
294-
// Covers every qualifier type, both short aliases (A/B/CR) and full names,
295-
// numeric progression within a qualifier (M1<M2, RC1<RC2, SP1<SP2).
294+
// Covers every qualifier type and numeric progression (M1<M2, RC1<RC2, SP1<SP2).
295+
// Aliases (A/ALPHA, B/BETA, CR/RC) compare equal and are tested in aliasesCompareEqual.
296296
assertSortedOrder(
297-
"3.3.0-A1", // alpha (short alias)
298297
"3.3.0-ALPHA1", // alpha
299-
"3.3.0-B1", // beta (short alias)
300298
"3.3.0-BETA1", // beta
301299
"3.3.0-M1", // milestone
302300
"3.3.0-M2", // milestone, higher qualifier number
303-
"3.3.0-CR1", // rc (alias; CR < RC lexicographically)
304301
"3.3.0-RC1", // rc
305302
"3.3.0-RC2", // rc, higher qualifier number
306303
"3.3.0-SNAPSHOT", // snapshot
@@ -314,13 +311,10 @@ void sortAllThreePartVersionVariants() {
314311
void sortAllFourPartVersionVariants() {
315312
// Same qualifier coverage as the 3-part test but with a 4th numeric component.
316313
assertSortedOrder(
317-
"3.3.0.1-A1",
318314
"3.3.0.1-ALPHA1",
319-
"3.3.0.1-B1",
320315
"3.3.0.1-BETA1",
321316
"3.3.0.1-M1",
322317
"3.3.0.1-M2",
323-
"3.3.0.1-CR1",
324318
"3.3.0.1-RC1",
325319
"3.3.0.1-RC2",
326320
"3.3.0.1-SNAPSHOT",
@@ -333,29 +327,23 @@ void sortAllFourPartVersionVariants() {
333327
@Test
334328
void sortThreeAndFourPartVersionsMixed() {
335329
// All 3-part and 4-part variants interleaved in one list.
336-
// The critical boundary is 3.3.0-SP2 < 3.3.0.1-A1: the 4th numeric
330+
// The critical boundary is 3.3.0-SP2 < 3.3.0.1-ALPHA1: the 4th numeric
337331
// component (build=1 vs 0) dominates even the highest 3-part qualifier.
338332
assertSortedOrder(
339-
"3.3.0-A1",
340333
"3.3.0-ALPHA1",
341-
"3.3.0-B1",
342334
"3.3.0-BETA1",
343335
"3.3.0-M1",
344336
"3.3.0-M2",
345-
"3.3.0-CR1",
346337
"3.3.0-RC1",
347338
"3.3.0-RC2",
348339
"3.3.0-SNAPSHOT",
349340
"3.3.0",
350341
"3.3.0-SP1",
351342
"3.3.0-SP2",
352-
"3.3.0.1-A1",
353343
"3.3.0.1-ALPHA1",
354-
"3.3.0.1-B1",
355344
"3.3.0.1-BETA1",
356345
"3.3.0.1-M1",
357346
"3.3.0.1-M2",
358-
"3.3.0.1-CR1",
359347
"3.3.0.1-RC1",
360348
"3.3.0.1-RC2",
361349
"3.3.0.1-SNAPSHOT",
@@ -382,6 +370,17 @@ void numericComponentsDominateQualifier() {
382370
);
383371
}
384372

373+
@Test
374+
void timestampedSnapshotTreatedAsSnapshot() {
375+
// Maven timestamped snapshots (e.g. from a remote repo) must be < the GA release.
376+
Version tsSnap = Version.parse("4.0.2-SNAPSHOT-20250101.123456-1");
377+
Version snap = Version.parse("4.0.2-SNAPSHOT");
378+
Version fin = Version.parse("4.0.2.Final");
379+
assertEquals(ReleaseType.SNAPSHOT, tsSnap.getReleaseType());
380+
assertTrue(tsSnap.compareTo(fin) < 0, "timestamped snapshot < Final");
381+
assertEquals(0, tsSnap.compareTo(snap), "timestamped snapshot == plain snapshot");
382+
}
383+
385384
@Test
386385
void gaAliasesCompareEqualToPlainRelease() {
387386
Version plain = Version.parse("3.3.0");
@@ -397,15 +396,22 @@ void dotSeparatorQualifiersRespectPriority() {
397396
// Old-style dot-separated qualifiers observe the same priority order.
398397
assertTrue(Version.parse("3.3.0.M1").compareTo(Version.parse("3.3.0.RC1")) < 0);
399398
assertTrue(Version.parse("3.3.0.RC1").compareTo(Version.parse("3.3.0.SNAPSHOT")) < 0);
400-
// Dot separator ('.' = 46) > hyphen ('-' = 45), so when qualifier priority
401-
// ties the fallback string comparison puts dot-style after hyphen-style:
402-
// "3.3.0.M1" > "3.3.0-M2" even though M2 > M1 numerically.
403-
assertTrue(Version.parse("3.3.0-M2").compareTo(Version.parse("3.3.0.M1")) < 0);
399+
// Separator style is irrelevant — only the qualifier number matters,
400+
// so "3.3.0-M2" (number=2) > "3.3.0.M1" (number=1).
401+
assertTrue(Version.parse("3.3.0-M2").compareTo(Version.parse("3.3.0.M1")) > 0);
402+
}
403+
404+
@Test
405+
void aliasesCompareEqual() {
406+
// Short and long qualifier aliases have the same ReleaseType and number, so they compare equal.
407+
assertEquals(0, Version.parse("3.3.0-A1").compareTo(Version.parse("3.3.0-ALPHA1")));
408+
assertEquals(0, Version.parse("3.3.0-B1").compareTo(Version.parse("3.3.0-BETA1")));
409+
assertEquals(0, Version.parse("3.3.0-CR1").compareTo(Version.parse("3.3.0-RC1")));
404410
}
405411

406412
@Test
407413
void caseInsensitiveVersionStringsCompareEqual() {
408-
// Identical strings differing only in case short-circuit to 0 without parsing.
414+
// Qualifier parsing is case-insensitive, so mixed-case versions compare equal.
409415
assertEquals(0, Version.parse("3.3.0-SNAPSHOT").compareTo(Version.parse("3.3.0-snapshot")));
410416
assertEquals(0, Version.parse("3.3.0-RC1").compareTo(Version.parse("3.3.0-rc1")));
411417
assertEquals(0, Version.parse("3.3.0-M1").compareTo(Version.parse("3.3.0-m1")));

0 commit comments

Comments
 (0)