@@ -57,9 +57,12 @@ public struct SemanticVersion: Sendable, Hashable {
57
57
prereleaseIdentifiers: [ String ] = [ ] ,
58
58
buildMetadataIdentifiers: [ String ] = [ ]
59
59
) {
60
- guard ( prereleaseIdentifiers + buildMetadataIdentifiers) . allSatisfyValidSemverIdentifier else {
60
+ guard ( prereleaseIdentifiers + buildMetadataIdentifiers) . allSatisfy ( { $0 . allSatisfy ( \ . isValidInSemverIdentifier ) } ) else {
61
61
fatalError ( " Invalid character found in semver identifier, must match [A-Za-z0-9-] " )
62
62
}
63
+ guard prereleaseIdentifiers. allSatisfy ( { $0. isValidSemverPrereleaseIdentifier } ) else {
64
+ fatalError ( " Invalid prerelease identifier found, must be alphanumeric, exactly 0, or not start with 0. " )
65
+ }
63
66
self . major = major
64
67
self . minor = minor
65
68
self . patch = patch
@@ -74,22 +77,19 @@ public struct SemanticVersion: Sendable, Hashable {
74
77
/// - TODO: Possibly throw more specific validation errors? Would this be useful?
75
78
///
76
79
/// - TODO: This code, while it does check validity better than what was here before, is ugly as heck. Clean it up.
77
- public init ? ( string: String ) {
78
- guard string. allSatisfy ( { $0 . isASCII } ) else { return nil }
80
+ public init ? ( string: some StringProtocol ) {
81
+ guard string. allSatisfy ( \ . isASCII) else { return nil }
79
82
80
83
var idx = string. startIndex
81
84
func readNumber( usingIdx idx: inout String . Index ) -> UInt ? {
82
85
let startIdx = idx
83
- while idx < string. endIndex, string [ idx] . isNumber { string. formIndex ( after: & idx) }
84
- let endIdx = idx
85
- return string. distance ( from: startIdx, to: endIdx) > 0 ? UInt ( string [ startIdx..< endIdx] ) : nil
86
+ idx = string [ idx... ] . firstIndex ( where: { !$0. isWholeNumber } ) ?? string. endIndex
87
+ return UInt ( string [ startIdx ..< idx] )
86
88
}
87
89
func readIdent( usingIdx idx: inout String . Index ) -> String ? {
88
90
let startIdx = idx
89
- while idx < string. endIndex, string [ idx] . isValidInSemverIdentifier { string. formIndex ( after: & idx) }
90
- let endIdx = idx
91
- guard string. distance ( from: startIdx, to: endIdx) > 0 else { return nil }
92
- return String ( string [ startIdx..< endIdx] )
91
+ idx = string [ idx... ] . firstIndex ( where: { !$0. isValidInSemverIdentifier } ) ?? string. endIndex
92
+ return idx > startIdx ? String ( string [ startIdx ..< idx] ) : nil
93
93
}
94
94
95
95
guard let major = readNumber ( usingIdx: & idx) else { return nil }
@@ -119,8 +119,14 @@ public struct SemanticVersion: Sendable, Hashable {
119
119
}
120
120
string. formIndex ( after: & idx)
121
121
guard let ident = readIdent ( usingIdx: & idx) else { return nil }
122
- if seenPlus { buildMetadataIdentifiers. append ( ident) }
123
- else { prereleaseIdentifiers. append ( ident) }
122
+ if seenPlus {
123
+ buildMetadataIdentifiers. append ( ident)
124
+ } else {
125
+ guard ident. isValidSemverPrereleaseIdentifier else {
126
+ return nil
127
+ }
128
+ prereleaseIdentifiers. append ( ident)
129
+ }
124
130
}
125
131
126
132
self . major = major
@@ -132,7 +138,7 @@ public struct SemanticVersion: Sendable, Hashable {
132
138
}
133
139
134
140
extension SemanticVersion : Comparable {
135
- /// See ``Comparable/<(lhs:rhs:)``. Implements the "precedence" ordering specified by the semver specification.
141
+ /// Implements the "precedence" ordering specified by the semver specification.
136
142
public static func < ( lhs: SemanticVersion , rhs: SemanticVersion ) -> Bool {
137
143
let lhsComponents = [ lhs. major, lhs. minor, lhs. patch]
138
144
let rhsComponents = [ rhs. major, rhs. minor, rhs. patch]
@@ -145,8 +151,8 @@ extension SemanticVersion: Comparable {
145
151
if rhs. prereleaseIdentifiers. isEmpty { return true } // Prerelease lhs < non-prerelease rhs
146
152
147
153
switch zip ( lhs. prereleaseIdentifiers, rhs. prereleaseIdentifiers)
148
- . first ( where: { $0 != $1 } )
149
- . map ( { ( ( Int ( $0) ?? $0) as Any , ( Int ( $1) ?? $1) as Any ) } )
154
+ . first ( where: { $0 != $1 } )
155
+ . map ( { ( ( Int ( $0) ?? $0) as Any , ( Int ( $1) ?? $1) as Any ) } )
150
156
{
151
157
case let . some( ( lId as Int , rId as Int ) ) : return lId < rId
152
158
case let . some( ( lId as String , rId as String ) ) : return lId < rId
@@ -160,32 +166,32 @@ extension SemanticVersion: Comparable {
160
166
}
161
167
162
168
extension SemanticVersion : LosslessStringConvertible {
163
- /// See ``CustomStringConvertible/description``. An additional API guarantee is made that this property will always
164
- /// yield a string which is correctly formatted as a valid semantic version number.
169
+ /// An additional API guarantee is made by this type that this property will always yield a string
170
+ /// which is correctly formatted as a valid semantic version number.
165
171
public var description : String {
166
- return """
167
- \( major) . \
168
- \( minor) . \
169
- \( patch) \
170
- \( prereleaseIdentifiers. joined ( separator: " . " , prefix: " - " ) ) \
171
- \( buildMetadataIdentifiers. joined ( separator: " . " , prefix: " + " ) )
172
- """
172
+ """
173
+ \( self . major) . \
174
+ \( self . minor) . \
175
+ \( self . patch) \
176
+ \( self . prereleaseIdentifiers. joined ( separator: " . " , prefix: " - " ) ) \
177
+ \( self . buildMetadataIdentifiers. joined ( separator: " . " , prefix: " + " ) )
178
+ """
173
179
}
174
180
175
- /// See `` LosslessStringConvertible/ init(_:)``. The semantics are identical to those of ``init?(string:)``.
181
+ // See `LosslessStringConvertible. init(_:)`. Identical semantics to ``init?(string:)``.
176
182
public init ? ( _ description: String ) {
177
183
self . init ( string: description)
178
184
}
179
185
}
180
186
181
187
extension SemanticVersion : Codable {
182
- /// See `` Encodable/ encode(to:)` `.
183
- public func encode( to encoder: Encoder ) throws {
188
+ // See `Encodable. encode(to:)`.
189
+ public func encode( to encoder: any Encoder ) throws {
184
190
try self . description. encode ( to: encoder)
185
191
}
186
192
187
- /// See `` Decodable/ init(from:)` `.
188
- public init ( from decoder: Decoder ) throws {
193
+ // See `Decodable. init(from:)`.
194
+ public init ( from decoder: any Decoder ) throws {
189
195
let container = try decoder. singleValueContainer ( )
190
196
let raw = try container. decode ( String . self)
191
197
@@ -196,46 +202,56 @@ extension SemanticVersion: Codable {
196
202
}
197
203
}
198
204
199
- fileprivate extension Array where Element == String {
205
+ fileprivate extension Array < String > {
200
206
/// Identical to ``joined(separator:)``, except that when the result is non-empty, the provided `prefix` will be
201
207
/// prepended to it. This is a mildly silly solution to the issue of how best to implement "add a joiner character
202
208
/// between one interpolation and the next, but only if the second one is non-empty".
203
209
func joined( separator: String , prefix: String ) -> String {
204
- let result = self . joined ( separator: separator)
205
- return ( result. isEmpty ? " " : prefix) + result
210
+ self . isEmpty ? " " : " \( prefix) \( self . joined ( separator: separator) ) "
206
211
}
207
212
}
208
213
209
214
fileprivate extension Character {
210
- /// Valid characters in a semver identifier are defined by these BNF rules:
215
+ /// Valid characters in a semver identifier are defined by these BNF rules,
216
+ /// taken directly from [the SemVer BNF grammar][semver2bnf]:
217
+ ///
218
+ /// [semver2bnf]: https://semver.org/spec/v2.0.0.html#backusnaur-form-grammar-for-valid-semver-versions
219
+ ///
220
+ /// ```bnf
221
+ /// <identifier character> ::= <digit>
222
+ /// | <non-digit>
223
+ ///
224
+ /// <non-digit> ::= <letter>
225
+ /// | "-"
226
+ ///
227
+ /// <digit> ::= "0"
228
+ /// | <positive digit>
211
229
///
212
- /// <identifier character> ::= <digit>
213
- /// | <non-digit>
214
- /// <non-digit> ::= <letter>
215
- /// | "-"
216
- /// <digit> ::= "0"
217
- /// | <positive digit>
218
- /// <positive digit> ::= "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
219
- /// <letter> ::= "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J"
220
- /// | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" | "S" | "T"
221
- /// | "U" | "V" | "W" | "X" | "Y" | "Z" | "a" | "b" | "c" | "d"
222
- /// | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n"
223
- /// | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x"
224
- /// | "y" | "z"
230
+ /// <positive digit> ::= "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
231
+ ///
232
+ /// <letter> ::= "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J"
233
+ /// | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" | "S" | "T"
234
+ /// | "U" | "V" | "W" | "X" | "Y" | "Z" | "a" | "b" | "c" | "d"
235
+ /// | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n"
236
+ /// | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x"
237
+ /// | "y" | "z"
238
+ /// ```
225
239
var isValidInSemverIdentifier : Bool {
226
- self . isASCII && ( self . isLetter || self . isNumber || self == " - " )
240
+ self . isLetter || self . isWholeNumber || self == " - "
227
241
}
228
242
}
229
243
230
244
fileprivate extension StringProtocol {
231
- /// See ``Character/isValidInSemverIdentifier`` for validity rules.
232
- var isValidSemverIdentifier : Bool {
233
- return self . allSatisfy { $0. isValidInSemverIdentifier }
234
- }
235
- }
236
-
237
- fileprivate extension Collection where Element: StringProtocol {
238
- var allSatisfyValidSemverIdentifier : Bool {
239
- return self . allSatisfy { $0. isValidSemverIdentifier }
245
+ /// A valid prerelease identifier must either:
246
+ ///
247
+ /// - Be exactly "0",
248
+ /// - Not start with "0", or
249
+ /// - Contain non-numeric characters
250
+ ///
251
+ ///
252
+ var isValidSemverPrereleaseIdentifier : Bool {
253
+ self == " 0 " ||
254
+ !self . starts ( with: " 0 " ) ||
255
+ self . contains { !$0. isWholeNumber }
240
256
}
241
257
}
0 commit comments