@@ -29,7 +29,7 @@ namespace DevWinUI;
2929/// </example>
3030// https://github.com/semver/semver/blob/master/semver.md
3131// https://github.com/semver/semver/blob/master/semver.svg
32- public sealed class SemanticVersion : IFormattable , IComparable , IComparable < SemanticVersion > , IEquatable < SemanticVersion > , IParsable < SemanticVersion > , ISpanParsable < SemanticVersion >
32+ public sealed partial class SemanticVersion : IFormattable , IComparable , IComparable < SemanticVersion > , IEquatable < SemanticVersion > , IParsable < SemanticVersion > , ISpanParsable < SemanticVersion >
3333{
3434 private static readonly IReadOnlyList < string > EmptyArray = Array . Empty < string > ( ) ;
3535
@@ -41,6 +41,17 @@ public SemanticVersion(int major, int minor, int patch)
4141 Patch = patch ;
4242 }
4343
44+ // Internal constructor used by the parser. The labels are already validated and frozen,
45+ // so this bypasses the per-label validation and list rebuilding done by the public constructors.
46+ private SemanticVersion ( int major , int minor , int patch , IReadOnlyList < string > prereleaseLabels , IReadOnlyList < string > metadata )
47+ {
48+ Major = major ;
49+ Minor = minor ;
50+ Patch = patch ;
51+ PrereleaseLabels = prereleaseLabels ;
52+ Metadata = metadata ;
53+ }
54+
4455 /// <summary>Creates a new semantic version with the specified major, minor, patch numbers and prerelease label.</summary>
4556 public SemanticVersion ( int major , int minor , int patch , string ? prereleaseLabel )
4657 : this ( major , minor , patch , prereleaseLabel , metadata : null )
@@ -80,7 +91,7 @@ public SemanticVersion(int major, int minor, int patch, IEnumerable<string>? pre
8091 if ( label is null || ! IsPrereleaseIdentifier ( label . AsSpan ( ) ) )
8192 throw new ArgumentException ( $ "Label '{ label } ' is not valid", nameof ( prereleaseLabel ) ) ;
8293
83- labels ??= [ ] ;
94+ labels ??= prereleaseLabel . TryGetNonEnumeratedCount ( out var count ) ? new ReadOnlyList < string > ( count ) : [ ] ;
8495 labels . Add ( label ) ;
8596 }
8697
@@ -99,7 +110,7 @@ public SemanticVersion(int major, int minor, int patch, IEnumerable<string>? pre
99110 if ( label is null || ! IsMetadataIdentifier ( label . AsSpan ( ) ) )
100111 throw new ArgumentException ( $ "Label '{ label } ' is not valid", nameof ( metadata ) ) ;
101112
102- labels ??= [ ] ;
113+ labels ??= metadata . TryGetNonEnumeratedCount ( out var count ) ? new ReadOnlyList < string > ( count ) : [ ] ;
103114 labels . Add ( label ) ;
104115 }
105116
@@ -144,32 +155,30 @@ public string ToString(string? format, IFormatProvider? formatProvider)
144155 if ( IsPrerelease )
145156 {
146157 sb . Append ( '-' ) ;
147- var first = true ;
148- foreach ( var label in PrereleaseLabels )
158+ var labels = PrereleaseLabels ;
159+ for ( var i = 0 ; i < labels . Count ; i ++ )
149160 {
150- if ( ! first )
161+ if ( i != 0 )
151162 {
152163 sb . Append ( '.' ) ;
153164 }
154165
155- sb . Append ( label ) ;
156- first = false ;
166+ sb . Append ( labels [ i ] ) ;
157167 }
158168 }
159169
160170 if ( HasMetadata )
161171 {
162172 sb . Append ( '+' ) ;
163- var first = true ;
164- foreach ( var label in Metadata )
173+ var labels = Metadata ;
174+ for ( var i = 0 ; i < labels . Count ; i ++ )
165175 {
166- if ( ! first )
176+ if ( i != 0 )
167177 {
168178 sb . Append ( '.' ) ;
169179 }
170180
171- sb . Append ( label ) ;
172- first = false ;
181+ sb . Append ( labels [ i ] ) ;
173182 }
174183 }
175184
@@ -294,7 +303,7 @@ public static bool TryParse(ReadOnlySpan<char> versionString, [NotNullWhen(retur
294303 if ( index != versionString . Length )
295304 return false ;
296305
297- version = new SemanticVersion ( major , minor , patch , prereleaseLabels , metadata ) ;
306+ version = new SemanticVersion ( major , minor , patch , prereleaseLabels ?? EmptyArray , metadata ?? EmptyArray ) ;
298307 return true ;
299308 }
300309
@@ -337,19 +346,24 @@ private static bool TryReadPrerelease(ReadOnlySpan<char> versionString, ref int
337346
338347 private static IReadOnlyList < string > ReadPrereleaseIdentifiers ( ReadOnlySpan < char > versionString , ref int index )
339348 {
340- var result = new List < string > ( ) ;
349+ ReadOnlyList < string > ? result = null ;
341350 while ( true )
342351 {
343352 if ( TryReadPrereleaseIdentifier ( versionString , ref index , out var label ) )
344353 {
354+ result ??= [ ] ;
345355 result . Add ( label ) ;
346356 }
347357
348358 if ( ! TryReadDot ( versionString , ref index ) )
349359 break ;
350360 }
351361
352- return result . Count == 0 ? EmptyArray : ReadOnlyList . From ( result ) ;
362+ if ( result is null )
363+ return EmptyArray ;
364+
365+ result . Freeze ( ) ;
366+ return result ;
353367 }
354368
355369 private static bool TryReadMetadata ( ReadOnlySpan < char > versionString , ref int index , [ NotNullWhen ( returnValue : true ) ] out IReadOnlyList < string > ? labels )
@@ -368,7 +382,7 @@ private static bool TryReadMetadata(ReadOnlySpan<char> versionString, ref int in
368382
369383 private static IReadOnlyList < string > TryReadMetadataIdentifiers ( ReadOnlySpan < char > versionString , ref int index )
370384 {
371- List < string > ? result = null ;
385+ ReadOnlyList < string > ? result = null ;
372386 while ( true )
373387 {
374388 if ( TryReadMetadataIdentifier ( versionString , ref index , out var label ) )
@@ -381,7 +395,11 @@ private static IReadOnlyList<string> TryReadMetadataIdentifiers(ReadOnlySpan<cha
381395 break ;
382396 }
383397
384- return result is null ? EmptyArray : ReadOnlyList . From ( result ) ;
398+ if ( result is null )
399+ return EmptyArray ;
400+
401+ result . Freeze ( ) ;
402+ return result ;
385403 }
386404
387405 private static bool IsPrereleaseIdentifier ( ReadOnlySpan < char > label )
@@ -406,9 +424,13 @@ private static bool TryReadPrereleaseIdentifier(ReadOnlySpan<char> versionString
406424
407425 if ( last > index )
408426 {
409- value = versionString [ index ..last ] . ToString ( ) ;
410- if ( value [ 0 ] != '0' || value . Any ( c => ! IsDigit ( c ) ) )
427+ var span = versionString [ index ..last ] ;
428+
429+ // A numeric identifier must not have a leading zero. Identifiers that contain at
430+ // least one non-digit, or that do not start with '0', are always valid.
431+ if ( span [ 0 ] != '0' || ContainsNonDigit ( span ) )
411432 {
433+ value = span . ToString ( ) ;
412434 index = last ;
413435 return true ;
414436 }
@@ -417,6 +439,16 @@ private static bool TryReadPrereleaseIdentifier(ReadOnlySpan<char> versionString
417439 value = default ;
418440 return false ;
419441
442+ static bool ContainsNonDigit ( ReadOnlySpan < char > span )
443+ {
444+ foreach ( var c in span )
445+ {
446+ if ( ! IsDigit ( c ) )
447+ return true ;
448+ }
449+
450+ return false ;
451+ }
420452 }
421453
422454 private static bool IsValidLabelCharacter ( char c )
0 commit comments