Skip to content

Commit 00937f8

Browse files
correct semantic version comparison
`Comparable` does not provide a default implementation for `==`, so the compiler synthesises one composed of [member-wise comparisons](https://github.com/apple/swift-evolution/blob/main/proposals/0185-synthesize-equatable-hashable.md#implementation-details). This leads to a false `false` when 2 semantic versions differ by only their build metadata identifiers, contradicting to SemVer 2.0.0's [comparison rules](https://semver.org/#spec-item-10). This commit adds a manual implementation of `==` for `Version`, along with appropriate tests. One consequence, though, is that now two versions that differ by only their build metadata identifiers are not allowed in the same set.
1 parent 3540865 commit 00937f8

File tree

2 files changed

+214
-5
lines changed

2 files changed

+214
-5
lines changed

Sources/TSCUtility/Version.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,17 @@ public struct Version: Hashable {
4646
}
4747

4848
extension Version: Comparable {
49-
49+
5050
func isEqualWithoutPrerelease(_ other: Version) -> Bool {
5151
return major == other.major && minor == other.minor && patch == other.patch
5252
}
53-
53+
54+
// Although `Comparable` inherits from `Equatable`, it does not provide a new default implementation of `==`, but instead uses `Equatable`'s default synthesised implementation. The compiler-synthesised `==`` is composed of [member-wise comparisons](https://github.com/apple/swift-evolution/blob/main/proposals/0185-synthesize-equatable-hashable.md#implementation-details), which leads to a false `false` when 2 semantic versions differ by only their build metadata identifiers, contradicting SemVer 2.0.0's [comparison rules](https://semver.org/#spec-item-10).
55+
@inlinable
56+
public static func == (lhs: Version, rhs: Version) -> Bool {
57+
!(lhs < rhs) && !(lhs > rhs)
58+
}
59+
5460
public static func < (lhs: Version, rhs: Version) -> Bool {
5561
let lhsComparators = [lhs.major, lhs.minor, lhs.patch]
5662
let rhsComparators = [rhs.major, rhs.minor, rhs.patch]
@@ -88,6 +94,7 @@ extension Version: Comparable {
8894

8995
return lhs.prereleaseIdentifiers.count < rhs.prereleaseIdentifiers.count
9096
}
97+
9198
}
9299

93100
extension Version: CustomStringConvertible {

Tests/TSCUtilityTests/VersionTests.swift

Lines changed: 205 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,210 @@ import struct TSCUtility.Version
1212
import XCTest
1313

1414
class VersionTests: XCTestCase {
15-
16-
func testEquality() {
15+
16+
func testVersionComparison() {
17+
18+
// MARK: version core vs. version core
19+
20+
XCTAssertGreaterThan(Version(2, 1, 1), Version(1, 2, 3))
21+
XCTAssertGreaterThan(Version(1, 3, 1), Version(1, 2, 3))
22+
XCTAssertGreaterThan(Version(1, 2, 4), Version(1, 2, 3))
23+
24+
// MARK: version core vs. version core + pre-release
25+
26+
XCTAssertGreaterThan(Version(1, 2, 3), Version(1, 2, 3, prereleaseIdentifiers: [""]))
27+
XCTAssertGreaterThan(Version(1, 2, 3), Version(1, 2, 3, prereleaseIdentifiers: ["beta"]))
28+
XCTAssertLessThan(Version(1, 2, 2), Version(1, 2, 3, prereleaseIdentifiers: ["beta"]))
29+
30+
// MARK: version core + pre-release vs. version core + pre-release
31+
32+
XCTAssertEqual(Version(1, 2, 3, prereleaseIdentifiers: [""]), Version(1, 2, 3, prereleaseIdentifiers: [""]))
33+
34+
XCTAssertEqual(Version(1, 2, 3, prereleaseIdentifiers: ["beta"]), Version(1, 2, 3, prereleaseIdentifiers: ["beta"]))
35+
XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["alpha"]), Version(1, 2, 3, prereleaseIdentifiers: ["beta"]))
36+
XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["alpha1"]), Version(1, 2, 3, prereleaseIdentifiers: ["alpha2"]))
37+
XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["alpha"]), Version(1, 2, 3, prereleaseIdentifiers: ["alpha-"]))
38+
XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["beta", "alpha"]), Version(1, 2, 3, prereleaseIdentifiers: ["beta", "beta"]))
39+
XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["alpha", "beta"]), Version(1, 2, 3, prereleaseIdentifiers: ["beta", "alpha"]))
40+
41+
XCTAssertEqual(Version(1, 2, 3, prereleaseIdentifiers: ["1"]), Version(1, 2, 3, prereleaseIdentifiers: ["1"]))
42+
XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["1"]), Version(1, 2, 3, prereleaseIdentifiers: ["2"]))
43+
XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["1", "1"]), Version(1, 2, 3, prereleaseIdentifiers: ["1", "2"]))
44+
XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["1", "2"]), Version(1, 2, 3, prereleaseIdentifiers: ["2", "1"]))
45+
46+
XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["123"]), Version(1, 2, 3, prereleaseIdentifiers: ["123alpha"]))
47+
XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["223"]), Version(1, 2, 3, prereleaseIdentifiers: ["123alpha"]))
48+
49+
// MARK: version core vs. version core + build metadata
50+
51+
XCTAssertEqual(Version(1, 2, 3), Version(1, 2, 3, buildMetadataIdentifiers: [""]))
52+
XCTAssertEqual(Version(1, 2, 3), Version(1, 2, 3, buildMetadataIdentifiers: ["beta"]))
53+
XCTAssertLessThan(Version(1, 2, 2), Version(1, 2, 3, buildMetadataIdentifiers: ["beta"]))
54+
55+
// MARK: version core + pre-release vs. version core + build metadata
56+
57+
XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: [""]), Version(1, 2, 3, buildMetadataIdentifiers: [""]))
58+
XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["beta"]), Version(1, 2, 3, buildMetadataIdentifiers: ["alpha"]))
59+
XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["alpha"]), Version(1, 2, 3, buildMetadataIdentifiers: ["beta"]))
60+
XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["beta"]), Version(1, 2, 3, buildMetadataIdentifiers: ["beta"]))
61+
XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["alpha"]), Version(1, 2, 3, buildMetadataIdentifiers: ["alpha-"]))
62+
XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["123"]), Version(1, 2, 3, buildMetadataIdentifiers: ["123alpha"]))
63+
XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["223"]), Version(1, 2, 3, buildMetadataIdentifiers: ["123alpha"]))
64+
XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["123alpha"]), Version(1, 2, 3, buildMetadataIdentifiers: ["123"]))
65+
XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["123alpha"]), Version(1, 2, 3, buildMetadataIdentifiers: ["223"]))
66+
XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["123alpha"]), Version(1, 2, 3, buildMetadataIdentifiers: ["223"]))
67+
XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["alpha"]), Version(1, 2, 3, buildMetadataIdentifiers: ["beta"]))
68+
XCTAssertGreaterThan(Version(2, 2, 3, prereleaseIdentifiers: [""]), Version(1, 2, 3, buildMetadataIdentifiers: [""]))
69+
XCTAssertGreaterThan(Version(1, 3, 3, prereleaseIdentifiers: ["alpha"]), Version(1, 2, 3, buildMetadataIdentifiers: ["beta"]))
70+
XCTAssertGreaterThan(Version(1, 2, 4, prereleaseIdentifiers: ["223"]), Version(1, 2, 3, buildMetadataIdentifiers: ["123alpha"]))
71+
72+
// MARK: version core + build metadata vs. version core + build metadata
73+
74+
XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: [""]), Version(1, 2, 3, buildMetadataIdentifiers: [""]))
75+
76+
XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: ["beta"]), Version(1, 2, 3, buildMetadataIdentifiers: ["beta"]))
77+
XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: ["alpha"]), Version(1, 2, 3, buildMetadataIdentifiers: ["beta"]))
78+
XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: ["alpha1"]), Version(1, 2, 3, buildMetadataIdentifiers: ["alpha2"]))
79+
XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: ["alpha"]), Version(1, 2, 3, buildMetadataIdentifiers: ["alpha-"]))
80+
XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: ["beta", "alpha"]), Version(1, 2, 3, buildMetadataIdentifiers: ["beta", "beta"]))
81+
XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: ["alpha", "beta"]), Version(1, 2, 3, buildMetadataIdentifiers: ["beta", "alpha"]))
82+
83+
XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: ["1"]), Version(1, 2, 3, buildMetadataIdentifiers: ["1"]))
84+
XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: ["1"]), Version(1, 2, 3, buildMetadataIdentifiers: ["2"]))
85+
XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: ["1", "1"]), Version(1, 2, 3, buildMetadataIdentifiers: ["1", "2"]))
86+
XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: ["1", "2"]), Version(1, 2, 3, buildMetadataIdentifiers: ["2", "1"]))
87+
88+
XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: ["123"]), Version(1, 2, 3, buildMetadataIdentifiers: ["123alpha"]))
89+
XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: ["223"]), Version(1, 2, 3, buildMetadataIdentifiers: ["123alpha"]))
90+
91+
// MARK: version core vs. version core + pre-release + build metadata
92+
93+
XCTAssertGreaterThan(Version(1, 2, 3), Version(1, 2, 3, prereleaseIdentifiers: [""], buildMetadataIdentifiers: [""]))
94+
XCTAssertGreaterThan(Version(1, 2, 3), Version(1, 2, 3, prereleaseIdentifiers: [""], buildMetadataIdentifiers: ["123alpha"]))
95+
XCTAssertGreaterThan(Version(1, 2, 3), Version(1, 2, 3, prereleaseIdentifiers: ["alpha"], buildMetadataIdentifiers: ["alpha"]))
96+
XCTAssertGreaterThan(Version(1, 2, 3), Version(1, 2, 3, prereleaseIdentifiers: ["beta"], buildMetadataIdentifiers: ["123"]))
97+
XCTAssertLessThan(Version(1, 2, 2), Version(1, 2, 3, prereleaseIdentifiers: ["beta"], buildMetadataIdentifiers: ["alpha", "beta"]))
98+
XCTAssertLessThan(Version(1, 2, 2), Version(1, 2, 3, prereleaseIdentifiers: ["beta"], buildMetadataIdentifiers: ["alpha-"]))
99+
100+
// MARK: version core + pre-release vs. version core + pre-release + build metadata
101+
102+
XCTAssertEqual(
103+
Version(1, 2, 3, prereleaseIdentifiers: [""]),
104+
Version(1, 2, 3, prereleaseIdentifiers: [""], buildMetadataIdentifiers: [""])
105+
)
106+
107+
XCTAssertEqual(
108+
Version(1, 2, 3, prereleaseIdentifiers: ["beta"]),
109+
Version(1, 2, 3, prereleaseIdentifiers: ["beta"], buildMetadataIdentifiers: [""])
110+
)
111+
XCTAssertLessThan(
112+
Version(1, 2, 3, prereleaseIdentifiers: ["alpha"]),
113+
Version(1, 2, 3, prereleaseIdentifiers: ["beta"], buildMetadataIdentifiers: ["123alpha"])
114+
)
115+
XCTAssertLessThan(
116+
Version(1, 2, 3, prereleaseIdentifiers: ["alpha1"]),
117+
Version(1, 2, 3, prereleaseIdentifiers: ["alpha2"], buildMetadataIdentifiers: ["alpha"])
118+
)
119+
XCTAssertLessThan(
120+
Version(1, 2, 3, prereleaseIdentifiers: ["alpha"]),
121+
Version(1, 2, 3, prereleaseIdentifiers: ["alpha-"], buildMetadataIdentifiers: ["alpha", "beta"])
122+
)
123+
XCTAssertLessThan(
124+
Version(1, 2, 3, prereleaseIdentifiers: ["beta", "alpha"]),
125+
Version(1, 2, 3, prereleaseIdentifiers: ["beta", "beta"], buildMetadataIdentifiers: ["123"])
126+
)
127+
XCTAssertLessThan(
128+
Version(1, 2, 3, prereleaseIdentifiers: ["alpha", "beta"]),
129+
Version(1, 2, 3, prereleaseIdentifiers: ["beta", "alpha"], buildMetadataIdentifiers: ["alpha-"])
130+
)
131+
132+
XCTAssertEqual(
133+
Version(1, 2, 3, prereleaseIdentifiers: ["1"]),
134+
Version(1, 2, 3, prereleaseIdentifiers: ["1"], buildMetadataIdentifiers: [""])
135+
)
136+
XCTAssertLessThan(
137+
Version(1, 2, 3, prereleaseIdentifiers: ["1"]),
138+
Version(1, 2, 3, prereleaseIdentifiers: ["2"], buildMetadataIdentifiers: ["123alpha"])
139+
)
140+
XCTAssertLessThan(
141+
Version(1, 2, 3, prereleaseIdentifiers: ["1", "1"]),
142+
Version(1, 2, 3, prereleaseIdentifiers: ["1", "2"], buildMetadataIdentifiers: ["123"])
143+
)
144+
XCTAssertLessThan(
145+
Version(1, 2, 3, prereleaseIdentifiers: ["1", "2"]),
146+
Version(1, 2, 3, prereleaseIdentifiers: ["2", "1"], buildMetadataIdentifiers: ["alpha", "beta"])
147+
)
148+
149+
XCTAssertLessThan(
150+
Version(1, 2, 3, prereleaseIdentifiers: ["123"]),
151+
Version(1, 2, 3, prereleaseIdentifiers: ["123alpha"], buildMetadataIdentifiers: ["-alpha"])
152+
)
153+
XCTAssertLessThan(
154+
Version(1, 2, 3, prereleaseIdentifiers: ["223"]),
155+
Version(1, 2, 3, prereleaseIdentifiers: ["123alpha"], buildMetadataIdentifiers: ["123"])
156+
)
157+
158+
// MARK: version core + pre-release + build metadata vs. version core + pre-release + build metadata
159+
160+
XCTAssertEqual(
161+
Version(1, 2, 3, prereleaseIdentifiers: [""], buildMetadataIdentifiers: [""]),
162+
Version(1, 2, 3, prereleaseIdentifiers: [""], buildMetadataIdentifiers: [""])
163+
)
164+
165+
XCTAssertEqual(
166+
Version(1, 2, 3, prereleaseIdentifiers: ["beta"], buildMetadataIdentifiers: ["123"]),
167+
Version(1, 2, 3, prereleaseIdentifiers: ["beta"], buildMetadataIdentifiers: [""])
168+
)
169+
XCTAssertLessThan(
170+
Version(1, 2, 3, prereleaseIdentifiers: ["alpha"], buildMetadataIdentifiers: ["-alpha"]),
171+
Version(1, 2, 3, prereleaseIdentifiers: ["beta"], buildMetadataIdentifiers: ["123alpha"])
172+
)
173+
XCTAssertLessThan(
174+
Version(1, 2, 3, prereleaseIdentifiers: ["alpha1"], buildMetadataIdentifiers: ["alpha", "beta"]),
175+
Version(1, 2, 3, prereleaseIdentifiers: ["alpha2"], buildMetadataIdentifiers: ["alpha"])
176+
)
177+
XCTAssertLessThan(
178+
Version(1, 2, 3, prereleaseIdentifiers: ["alpha"], buildMetadataIdentifiers: ["123"]),
179+
Version(1, 2, 3, prereleaseIdentifiers: ["alpha-"], buildMetadataIdentifiers: ["alpha", "beta"])
180+
)
181+
XCTAssertLessThan(
182+
Version(1, 2, 3, prereleaseIdentifiers: ["beta", "alpha"], buildMetadataIdentifiers: ["123alpha"]),
183+
Version(1, 2, 3, prereleaseIdentifiers: ["beta", "beta"], buildMetadataIdentifiers: ["123"])
184+
)
185+
XCTAssertLessThan(
186+
Version(1, 2, 3, prereleaseIdentifiers: ["alpha", "beta"], buildMetadataIdentifiers: [""]),
187+
Version(1, 2, 3, prereleaseIdentifiers: ["beta", "alpha"], buildMetadataIdentifiers: ["alpha-"])
188+
)
189+
190+
XCTAssertEqual(
191+
Version(1, 2, 3, prereleaseIdentifiers: ["1"], buildMetadataIdentifiers: ["alpha-"]),
192+
Version(1, 2, 3, prereleaseIdentifiers: ["1"], buildMetadataIdentifiers: [""])
193+
)
194+
XCTAssertLessThan(
195+
Version(1, 2, 3, prereleaseIdentifiers: ["1"], buildMetadataIdentifiers: ["123"]),
196+
Version(1, 2, 3, prereleaseIdentifiers: ["2"], buildMetadataIdentifiers: ["123alpha"])
197+
)
198+
XCTAssertLessThan(
199+
Version(1, 2, 3, prereleaseIdentifiers: ["1", "1"], buildMetadataIdentifiers: ["alpha", "beta"]),
200+
Version(1, 2, 3, prereleaseIdentifiers: ["1", "2"], buildMetadataIdentifiers: ["123"])
201+
)
202+
XCTAssertLessThan(
203+
Version(1, 2, 3, prereleaseIdentifiers: ["1", "2"], buildMetadataIdentifiers: ["alpha"]),
204+
Version(1, 2, 3, prereleaseIdentifiers: ["2", "1"], buildMetadataIdentifiers: ["alpha", "beta"])
205+
)
206+
207+
XCTAssertLessThan(
208+
Version(1, 2, 3, prereleaseIdentifiers: ["123"], buildMetadataIdentifiers: ["123alpha"]),
209+
Version(1, 2, 3, prereleaseIdentifiers: ["123alpha"], buildMetadataIdentifiers: ["-alpha"])
210+
)
211+
XCTAssertLessThan(
212+
Version(1, 2, 3, prereleaseIdentifiers: ["223"], buildMetadataIdentifiers: ["123alpha"]),
213+
Version(1, 2, 3, prereleaseIdentifiers: ["123alpha"], buildMetadataIdentifiers: ["123"])
214+
)
215+
216+
}
217+
218+
func testAdditionalEquality() {
17219
let versions: [Version] = ["1.2.3", "0.0.0",
18220
"0.0.0-alpha+yol", "0.0.0-alpha.1+pol",
19221
"0.1.2", "10.7.3",
@@ -44,7 +246,7 @@ class VersionTests: XCTestCase {
44246

45247
XCTAssertEqual(Set([Version(1,2,3)]), Set([Version(1,2,3)]))
46248
XCTAssertNotEqual(Set([Version(1,2,3)]), Set([Version(1,2,3, prereleaseIdentifiers: ["alpha"])]))
47-
XCTAssertNotEqual(Set([Version(1,2,3)]), Set([Version(1,2,3, buildMetadataIdentifiers: ["1011"])]))
249+
XCTAssertEqual(Set([Version(1,2,3)]), Set([Version(1,2,3, buildMetadataIdentifiers: ["1011"])]))
48250
}
49251

50252
func testDescription() {

0 commit comments

Comments
 (0)