• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

devonfw / IDEasy / 24882228855

24 Apr 2026 09:22AM UTC coverage: 70.711% (+0.02%) from 70.687%
24882228855

Pull #1848

github

web-flow
Merge dd1b675d1 into 8eeaf911e
Pull Request #1848: #1845: Unexpected version comparison result when comparing versions with stars

4376 of 6834 branches covered (64.03%)

Branch coverage included in aggregate %.

11266 of 15287 relevant lines covered (73.7%)

3.11 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.08
cli/src/main/java/com/devonfw/tools/ide/version/VersionSegment.java
1
package com.devonfw.tools.ide.version;
2

3
import java.util.Objects;
4

5
/**
6
 * Represents a single segment of a {@link VersionIdentifier}.
7
 */
8
public class VersionSegment implements VersionObject<VersionSegment> {
9

10
  /** Pattern to match a any version that matches the prefix. Value is: {@value} */
11
  public static final String PATTERN_MATCH_ANY_VERSION = "*!";
12

13
  /** Pattern to match a {@link VersionPhase#isStable() stable} version that matches the prefix. Value is: {@value} */
14
  public static final String PATTERN_MATCH_ANY_STABLE_VERSION = "*";
15

16
  private static final VersionSegment EMPTY = new VersionSegment("", "", "", "");
9✔
17

18
  private final String separator;
19

20
  private final VersionLetters letters;
21

22
  private final String pattern;
23

24
  private final String digits;
25

26
  private final int number;
27

28
  private VersionSegment next;
29

30
  /**
31
   * The constructor.
32
   *
33
   * @param separator the {@link #getSeparator() separator}.
34
   * @param letters the {@link #getLettersString() letters}.
35
   * @param digits the {@link #getDigits() digits}.
36
   */
37
  VersionSegment(String separator, String letters, String digits) {
38

39
    this(separator, letters, digits, "");
6✔
40
  }
1✔
41

42
  /**
43
   * The constructor.
44
   *
45
   * @param separator the {@link #getSeparator() separator}.
46
   * @param letters the {@link #getLettersString() letters}.
47
   * @param digits the {@link #getDigits() digits}.
48
   * @param pattern the {@link #getPattern() pattern}.
49
   */
50
  VersionSegment(String separator, String letters, String digits, String pattern) {
51

52
    super();
2✔
53
    this.separator = separator;
3✔
54
    boolean isAnyPattern = PATTERN_MATCH_ANY_VERSION.equals(pattern);
4✔
55
    if (isAnyPattern && letters.isEmpty()) {
5!
56
      this.letters = VersionLetters.UNSTABLE;
4✔
57
    } else {
58
      this.letters = VersionLetters.of(letters);
4✔
59
    }
60
    if (!pattern.isEmpty() && !isAnyPattern
7✔
61
        && !PATTERN_MATCH_ANY_STABLE_VERSION.equals(pattern)) {
2!
62
      throw new IllegalArgumentException("Invalid pattern: " + pattern);
×
63
    }
64
    this.pattern = pattern;
3✔
65
    this.digits = digits;
3✔
66
    if (this.digits.isEmpty()) {
4✔
67
      this.number = -1;
4✔
68
    } else {
69
      this.number = Integer.parseInt(this.digits);
5✔
70
    }
71
    if (EMPTY != null) {
2✔
72
      assert (!this.letters.isEmpty() || !this.digits.isEmpty() || !this.separator.isEmpty()
15✔
73
          || !this.pattern.isEmpty());
2!
74
    }
75
  }
1✔
76

77
  private VersionSegment(VersionSegment next, String separator, VersionLetters letters, String digits, int number, String pattern) {
78
    super();
2✔
79
    this.next = next;
3✔
80
    this.separator = separator;
3✔
81
    this.letters = letters;
3✔
82
    this.pattern = pattern;
3✔
83
    this.digits = digits;
3✔
84
    this.number = number;
3✔
85
  }
1✔
86

87
  /**
88
   * @return the separator {@link String} (e.g. "." or "-") or the empty {@link String} ("") for none.
89
   */
90
  public String getSeparator() {
91

92
    return this.separator;
3✔
93
  }
94

95
  /**
96
   * @return the letters or the empty {@link String} ("") for none. In canonical {@link VersionIdentifier}s letters indicate the development phase (e.g. "pre",
97
   *     "rc", "alpha", "beta", "milestone", "test", "dev", "SNAPSHOT", etc.). However, letters are technically any
98
   *     {@link Character#isLetter(char) letter characters} and may also be something like a code-name (e.g. "Cupcake", "Donut", "Eclair", "Froyo",
99
   *     "Gingerbread", "Honeycomb", "Ice Cream Sandwich", "Jelly Bean" in case of Android internals). Please note that in such case it is impossible to
100
   *     properly decide which version is greater than another versions. To avoid mistakes, the comparison supports a strict mode that will let the comparison
101
   *     fail in such case. However, by default (e.g. for {@link Comparable#compareTo(Object)}) the default {@link String#compareTo(String) string comparison}
102
   *     (lexicographical) is used to ensure a natural order.
103
   * @see #getPhase()
104
   */
105
  public String getLettersString() {
106

107
    return this.letters.getLetters();
4✔
108
  }
109

110
  /**
111
   * @return the {@link VersionLetters}.
112
   */
113
  public VersionLetters getLetters() {
114

115
    return this.letters;
3✔
116
  }
117

118
  /**
119
   * @return the {@link VersionPhase} for the {@link #getLettersString() letters}. Will be {@link VersionPhase#UNDEFINED} if unknown and hence never
120
   *     {@code null}.
121
   * @see #getLettersString()
122
   */
123
  public VersionPhase getPhase() {
124

125
    return this.letters.getPhase();
4✔
126
  }
127

128
  /**
129
   * @return the digits or the empty {@link String} ("") for none. This is the actual {@link #getNumber() number} part of this {@link VersionSegment}. So the
130
   *     {@link VersionIdentifier} "1.0.001" will have three segments: The first one with "1" as digits, the second with "0" as digits, and a third with "001"
131
   *     as digits. You can get the same value via {@link #getNumber()} but this {@link String} representation will preserve leading zeros.
132
   */
133
  public String getDigits() {
134

135
    return this.digits;
3✔
136
  }
137

138
  /**
139
   * @return the {@link #getDigits() digits} and integer number. Will be {@code -1} if no {@link #getDigits() digits} are present.
140
   */
141
  public int getNumber() {
142

143
    return this.number;
3✔
144
  }
145

146
  /**
147
   * @return the potential pattern that is {@link #PATTERN_MATCH_ANY_STABLE_VERSION}, {@link #PATTERN_MATCH_ANY_VERSION}, or for no pattern the empty
148
   *     {@link String}.
149
   */
150
  public String getPattern() {
151

152
    return this.pattern;
3✔
153
  }
154

155
  /**
156
   * @return {@code true} if {@link #getPattern() pattern} is NOT {@link String#isEmpty() empty}.
157
   */
158
  public boolean isPattern() {
159

160
    return !this.pattern.isEmpty();
8✔
161
  }
162

163
  /**
164
   * @return the next {@link VersionSegment} or {@code null} if this is the tail of the {@link VersionIdentifier}.
165
   */
166
  public VersionSegment getNextOrNull() {
167

168
    return this.next;
3✔
169
  }
170

171
  /**
172
   * @return the next {@link VersionSegment} or the {@link #ofEmpty() empty segment} if this is the tail of the {@link VersionIdentifier}.
173
   */
174
  public VersionSegment getNextOrEmpty() {
175

176
    if (this.next == null) {
3✔
177
      return EMPTY;
2✔
178
    }
179
    return this.next;
3✔
180
  }
181

182
  /**
183
   * @return {@code true} if this is the empty {@link VersionSegment}, {@code false} otherwise.
184
   */
185
  public boolean isEmpty() {
186

187
    return (this == EMPTY);
7✔
188
  }
189

190
  /**
191
   * A valid {@link VersionSegment} has to meet the following requirements:
192
   * <ul>
193
   * <li>The {@link #getSeparator() separator} may not be {@link String#length() longer} than a single character (e.g.
194
   * ".-_1" or "--1" are not considered valid).</li>
195
   * <li>The {@link #getSeparator() separator} may only contain the characters '.', '-', or '_' (e.g. " 1" or "ö1" are
196
   * not considered valid).</li>
197
   * <li>The combination of {@link #getPhase() phase} and {@link #getNumber() number} has to be
198
   * {@link VersionPhase#isValid(int) valid} (e.g. "pineapple-pen1" or "donut" are not considered valid).</li>
199
   * </ul>
200
   */
201
  @Override
202
  public boolean isValid() {
203

204
    if (!this.pattern.isEmpty()) {
4✔
205
      return false;
2✔
206
    }
207
    int separatorLen = this.separator.length();
4✔
208
    if (separatorLen > 1) {
3✔
209
      return false;
2✔
210
    } else if (separatorLen == 1) {
3✔
211
      if (!CharCategory.isValidSeparator(this.separator.charAt(0))) {
6✔
212
        return false;
2✔
213
      }
214
    }
215
    return this.letters.getPhase().isValid(this.number);
7✔
216
  }
217

218
  @Override
219
  public VersionComparisonResult compareVersion(VersionSegment other) {
220

221
    if (other == null) {
2!
222
      return VersionComparisonResult.GREATER_UNSAFE;
×
223
    }
224

225
    // Check if one version segment is * or *! while the other has no pattern
226
    if (this.letters.isEmpty() && isPattern() && !other.letters.isEmpty()) {
11✔
227
      return VersionComparisonResult.GREATER_UNSAFE;
2✔
228
    } else if (other.letters.isEmpty() && other.isPattern() && !this.letters.isEmpty()) {
11✔
229
      return VersionComparisonResult.LESS_UNSAFE;
2✔
230
    }
231
    // Compare letters and phase
232
    VersionComparisonResult lettersResult = this.letters.compareVersion(other.letters);
6✔
233
    if (!lettersResult.isEqual()) {
3✔
234
      return lettersResult;
2✔
235
    }
236
    if (!"_".equals(this.separator) && "_".equals(other.separator)) {
10✔
237
      if ("".equals(this.separator)) {
5!
238
        return VersionComparisonResult.LESS;
×
239
      } else {
240
        return VersionComparisonResult.GREATER;
2✔
241
      }
242
    } else if ("_".equals(this.separator) && !"_".equals(other.separator)) {
10!
243
      if ("".equals(other.separator)) {
5✔
244
        return VersionComparisonResult.GREATER;
2✔
245
      } else {
246
        return VersionComparisonResult.LESS;
2✔
247
      }
248
    }
249

250
    if (this.number != other.number) {
5✔
251
      if ((this.number < 0) && isPattern()) {
6✔
252
        return VersionComparisonResult.GREATER_UNSAFE;
2✔
253
      } else if ((other.number < 0) && other.isPattern()) {
6✔
254
        return VersionComparisonResult.LESS_UNSAFE;
2✔
255
      } else if (this.number < other.number) {
5✔
256
        return VersionComparisonResult.LESS;
2✔
257
      } else {
258
        return VersionComparisonResult.GREATER;
2✔
259
      }
260
    } else if (this.number < 0 && (isPattern() || other.isPattern())) {
9✔
261
      // Numbers are equal and one of them is a pattern
262
      if (isPattern() && !other.isPattern()) {
6✔
263
        return VersionComparisonResult.GREATER_UNSAFE;
2✔
264
      } else if (!isPattern() && other.isPattern()) {
6!
265
        return VersionComparisonResult.LESS_UNSAFE;
2✔
266
      }
267
      // Both are patterns
268
      else if (this.pattern.equals(PATTERN_MATCH_ANY_STABLE_VERSION) && other.pattern.equals(PATTERN_MATCH_ANY_VERSION)) {
10✔
269
        return VersionComparisonResult.LESS_UNSAFE;
2✔
270
      } else if (this.pattern.equals(PATTERN_MATCH_ANY_VERSION) && other.pattern.equals(PATTERN_MATCH_ANY_STABLE_VERSION)) {
10!
271
        return VersionComparisonResult.GREATER_UNSAFE;
2✔
272
      } else if (this.pattern.equals(other.pattern)) {
6!
273
        return VersionComparisonResult.EQUAL;
2✔
274
      } else {
275
        return VersionComparisonResult.EQUAL_UNSAFE;
×
276
      }
277
    } else if (this.separator.equals(other.separator)) {
6!
278
      return VersionComparisonResult.EQUAL;
2✔
279
    } else {
280
      return VersionComparisonResult.EQUAL_UNSAFE;
×
281
    }
282
  }
283

284
  /**
285
   * Matches a {@link VersionSegment} with a potential {@link #getPattern() pattern} against another {@link VersionSegment}. This operation may not always be
286
   * symmetric.
287
   *
288
   * @param other the {@link VersionSegment} to match against.
289
   * @return the {@link VersionMatchResult} of the match.
290
   */
291
  public VersionMatchResult matches(VersionSegment other) {
292

293
    if (other == null) {
2!
294
      return VersionMatchResult.MISMATCH;
×
295
    }
296
    if (isEmpty() && other.isEmpty()) {
3!
297
      return VersionMatchResult.MATCH;
×
298
    }
299
    boolean isPattern = isPattern();
3✔
300
    if (isPattern) {
2✔
301
      if (!this.digits.isEmpty()) {
4✔
302
        if (this.number != other.number) {
5✔
303
          return VersionMatchResult.MISMATCH;
2✔
304
        }
305
      }
306
      if (!this.separator.isEmpty()) {
4✔
307
        if (!this.separator.equals(other.separator)) {
6✔
308
          return VersionMatchResult.MISMATCH;
2✔
309
        }
310
      }
311
    } else {
312
      if ((this.number != other.number) || !this.separator.equals(other.separator)) {
11!
313
        return VersionMatchResult.MISMATCH;
2✔
314
      }
315
    }
316
    VersionMatchResult result = this.letters.matches(other.letters, isPattern);
7✔
317
    if (isPattern && (result == VersionMatchResult.EQUAL)) {
5✔
318
      if (this.pattern.equals(PATTERN_MATCH_ANY_STABLE_VERSION)) {
5✔
319
        VersionLetters developmentPhase = other.getDevelopmentPhase();
3✔
320
        if (developmentPhase.isUnstable()) {
3✔
321
          return VersionMatchResult.MISMATCH;
2✔
322
        }
323
        return VersionMatchResult.MATCH;
2✔
324
      } else if (this.pattern.equals(PATTERN_MATCH_ANY_VERSION)) {
5!
325
        return VersionMatchResult.MATCH;
2✔
326
      } else {
327
        throw new IllegalStateException("Pattern=" + this.pattern);
×
328
      }
329
    }
330
    return result;
2✔
331
  }
332

333
  /**
334
   * @return the {@link VersionLetters} that represent a {@link VersionLetters#isDevelopmentPhase() development phase} searching from this
335
   *     {@link VersionSegment} to all {@link #getNextOrNull() next segments}. Will be {@link VersionPhase#NONE} if no
336
   *     {@link VersionPhase#isDevelopmentPhase() development phase} was found and {@link VersionPhase#UNDEFINED} if multiple
337
   *     {@link VersionPhase#isDevelopmentPhase() development phase}s have been found.
338
   * @see VersionIdentifier#getDevelopmentPhase()
339
   */
340
  protected VersionLetters getDevelopmentPhase() {
341

342
    VersionLetters result = VersionLetters.EMPTY;
2✔
343
    VersionSegment segment = this;
2✔
344
    while (segment != null) {
2✔
345
      if (segment.letters.isDevelopmentPhase()) {
4✔
346
        if (result == VersionLetters.EMPTY) {
3!
347
          result = segment.letters;
4✔
348
        } else {
349
          result = VersionLetters.UNDEFINED;
×
350
        }
351
      }
352
      segment = segment.next;
4✔
353
    }
354
    return result;
2✔
355
  }
356

357
  /**
358
   * {@link VersionIdentifier#incrementSegment(int, boolean)}  Increments a version} recursively per {@link VersionSegment}.
359
   *
360
   * @param digitKeepCount the number of leading {@link VersionSegment}s with {@link VersionSegment#getDigits() digits} to keep untouched. Will be {@code 0}
361
   *     for the segment to increment and negative for the segments to set to zero.
362
   * @param keepLetters {@code true} to keep {@link VersionSegment#getLetters() letters} from modified segments, {@code false} to drop them.
363
   * @return the new {@link VersionSegment}.
364
   */
365
  VersionSegment increment(int digitKeepCount, boolean keepLetters) {
366

367
    String separator = this.separator;
3✔
368
    VersionLetters letters = this.letters;
3✔
369
    String digits = this.digits;
3✔
370
    int number = this.number;
3✔
371
    String pattern = this.pattern;
3✔
372
    int nextSegmentKeepCount = digitKeepCount;
2✔
373
    if (this.number >= 0) {
3✔
374
      nextSegmentKeepCount--;
1✔
375
    }
376
    if ((digitKeepCount < 0) || ((digitKeepCount == 0) && (this.number >= 0))) {
7✔
377
      if (!keepLetters) {
2✔
378
        letters = VersionLetters.EMPTY;
2✔
379
      }
380
      if (number >= 0) {
2✔
381
        if (digitKeepCount == 0) {
2✔
382
          number++;
2✔
383
        } else {
384
          number = 0;
2✔
385
        }
386
        int digitsLength = digits.length();
3✔
387
        digits = Integer.toString(number);
3✔
388
        int leadingZeros = digitsLength - digits.length();
5✔
389
        if (leadingZeros > 0) {
2✔
390
          StringBuilder newDigits = new StringBuilder(digits);
5✔
391
          while (leadingZeros > 0) {
2✔
392
            newDigits.insert(0, "0");
5✔
393
            leadingZeros--;
2✔
394
          }
395
          digits = newDigits.toString();
3✔
396
        }
397
      } else if (!keepLetters) {
3✔
398
        if (this.next == null) {
3✔
399
          return null;
2✔
400
        }
401
        return this.next.increment(nextSegmentKeepCount, false);
6✔
402
      }
403
    }
404
    VersionSegment nextSegment = null;
2✔
405
    if (this.next != null) {
3✔
406
      nextSegment = this.next.increment(nextSegmentKeepCount, keepLetters);
6✔
407
    }
408
    return new VersionSegment(nextSegment, separator, letters, digits, number, pattern);
10✔
409
  }
410

411
  /**
412
   * @return the number of {@link VersionSegment}s with {@link VersionSegment#getDigits() digits}.
413
   */
414
  int countDigits() {
415

416
    int count = 0;
2✔
417
    if (this.number >= 0) {
3✔
418
      count = 1;
2✔
419
    }
420
    if (this.next != null) {
3✔
421
      count = count + this.next.countDigits();
6✔
422
    }
423
    return count;
2✔
424
  }
425

426
  @Override
427
  public boolean equals(Object obj) {
428

429
    if (obj == this) {
3!
430
      return true;
×
431
    } else if (!(obj instanceof VersionSegment)) {
3✔
432
      return false;
2✔
433
    }
434
    VersionSegment other = (VersionSegment) obj;
3✔
435
    if (!Objects.equals(this.digits, other.digits)) {
6✔
436
      return false;
2✔
437
    } else if (!Objects.equals(this.separator, other.separator)) {
6!
438
      return false;
×
439
    } else if (!Objects.equals(this.letters, other.letters)) {
6!
440
      return false;
×
441
    } else if (!Objects.equals(this.pattern, other.pattern)) {
6!
442
      return false;
×
443
    } else if (!Objects.equals(this.next, other.next)) {
6✔
444
      return false;
2✔
445
    }
446
    return true;
2✔
447
  }
448

449
  @Override
450
  public String toString() {
451

452
    return this.separator + this.letters + this.digits + this.pattern;
11✔
453
  }
454

455
  /**
456
   * @return the {@link #isEmpty() empty} {@link VersionSegment} instance.
457
   */
458
  public static VersionSegment ofEmpty() {
459

460
    return EMPTY;
2✔
461
  }
462

463
  static VersionSegment of(String version) {
464

465
    CharReader reader = new CharReader(version);
5✔
466
    VersionSegment start = null;
2✔
467
    VersionSegment current = null;
2✔
468
    while (reader.hasNext()) {
3✔
469
      VersionSegment segment = parseSegment(reader);
3✔
470
      if (current == null) {
2✔
471
        start = segment;
3✔
472
      } else {
473
        current.next = segment;
3✔
474
      }
475
      current = segment;
2✔
476
    }
1✔
477
    return start;
2✔
478
  }
479

480
  private static VersionSegment parseSegment(CharReader reader) {
481

482
    String separator = reader.readSeparator();
3✔
483
    String letters = reader.readLetters();
3✔
484
    String digits = reader.readDigits();
3✔
485
    String pattern = reader.readPattern();
3✔
486
    return new VersionSegment(separator, letters, digits, pattern);
8✔
487
  }
488

489
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc