-
Notifications
You must be signed in to change notification settings - Fork 3k
Expand file tree
/
Copy pathbzlmod-version.ts
More file actions
312 lines (287 loc) · 9.14 KB
/
bzlmod-version.ts
File metadata and controls
312 lines (287 loc) · 9.14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
/**
* @fileoverview Contains classes that represent a Bazel module version.
*/
/**
* Represents a single value in a VersionPart. For example, the version string
* `1.2.3` has three identifiers: `1`, `2`, `3`.
*/
export class Identifier {
/**
* Returns the identifier as a string.
*/
readonly asString: string;
/**
* If the identifier only contains digits, this is the numeric value.
* Otherwise, it is `0`.
*/
readonly asNumber: number;
/**
* Specifies whether the identifier only contains digits.
*/
readonly isDigitsOnly: boolean;
/**
* Regular expression used to identify whether an identifier value only
* contains digits.
*/
static readonly digitsOnlyMatcher = /^[0-9]+$/;
/**
* @param value The value that is parsed for the Bazel module version parts.
*/
constructor(value: string) {
if (value === '') {
throw new Error('Identifier value cannot be empty.');
}
this.asString = value;
if (Identifier.digitsOnlyMatcher.test(value)) {
this.isDigitsOnly = true;
this.asNumber = parseInt(value, 10);
} else {
this.isDigitsOnly = false;
this.asNumber = 0;
}
}
/**
* Determines whether this identifier and another identifier are equal.
*/
equals(other: Identifier): boolean {
return this.asString === other.asString;
}
/**
* Determines whether this identifier comes before the other identifier.
*/
isLessThan(other: Identifier): boolean {
// This logic mirrors the comparison logic in
// https://cs.opensource.google/bazel/bazel/+/refs/heads/master:src/main/java/com/google/devtools/build/lib/bazel/bzlmod/Version.java
// isDigitsOnly: true first
if (this.isDigitsOnly !== other.isDigitsOnly) {
return this.isDigitsOnly;
}
if (this.asNumber !== other.asNumber) {
return this.asNumber < other.asNumber;
}
return this.asString < other.asString;
}
}
/**
* A collection of {@link Identifier} values that represent a portion of a
* Bazel module version.
*/
export class VersionPart extends Array<Identifier> {
/**
* Creates a {@link VersionPart} populated with the provided identifiers.
*/
static create(...items: (Identifier | string)[]): VersionPart {
const idents = items.map((item) => {
if (typeof item === 'string') {
return new Identifier(item);
}
return item;
});
const vp = new VersionPart();
vp.push(...idents);
return vp;
}
/**
* The string representation of the version part.
*/
get asString(): string {
return this.map((ident) => ident.asString).join('.');
}
/**
* Specifies whether this contains any identifiers.
*/
get isEmpty(): boolean {
return this.length === 0;
}
/**
* Returns the equivalent of the a Semver major value.
*/
get major(): number {
return this.length > 0 ? this[0].asNumber : 0;
}
/**
* Returns the equivalent of the a Semver minor value.
*/
get minor(): number {
return this.length > 1 ? this[1].asNumber : 0;
}
/**
* Returns the equivalent of the a Semver patch value.
*/
get patch(): number {
return this.length > 2 ? this[2].asNumber : 0;
}
/**
* Determines whether this version part is equal to the other.
*/
equals(other: VersionPart): boolean {
if (this.length !== other.length) {
return false;
}
for (let i = 0; i < this.length; i++) {
const a = this[i];
const b = other[i];
if (!a.equals(b)) {
return false;
}
}
return true;
}
/**
* Determines whether this version part comes before the other.
*/
isLessThan(other: VersionPart): boolean {
// This logic mirrors the comparison logic in
// https://cs.opensource.google/bazel/bazel/+/refs/heads/master:src/main/java/com/google/devtools/build/lib/bazel/bzlmod/Version.java
if (this.equals(other)) {
return false;
}
// Non-empty are first
if (this.length === 0 && other.length !== 0) {
return false;
}
if (other.length === 0 && this.length !== 0) {
return true;
}
const shortestLen = this.length < other.length ? this.length : other.length;
for (let i = 0; i < shortestLen; i++) {
const a = this[i];
const b = other[i];
if (!a.equals(b)) {
return a.isLessThan(b);
}
}
return this.length < other.length;
}
}
// Represents the capture groups produced by BzlmodVersion.versionMatcher.
interface VersionRegexResult {
release: string;
prerelease: string | undefined;
build: string | undefined;
}
/**
* Represents a version in the Bazel module system. The version format we support is
* `RELEASE[-PRERELEASE][+BUILD]`, where `RELEASE`, `PRERELEASE`, and `BUILD` are
* each a sequence of "identifiers" (defined as a non-empty sequence of ASCII alphanumerical
* characters and hyphens) separated by dots. The `RELEASE` part may not contain hyphens.
*
* Otherwise, this format is identical to SemVer, especially in terms of the comparison algorithm
* (https://semver.org/#spec-item-11). In other words, this format is intentionally looser than
* SemVer; in particular:
*
* - the "release" part isn't limited to exactly 3 segments (major, minor, patch), but can be
* fewer or more;
* - each segment in the "release" part can be identifiers instead of just numbers (so letters
* are also allowed -- although hyphens are not).
*
* Any valid SemVer version is a valid Bazel module version. Additionally, two SemVer versions
* `a` and `b` compare `a < b` iff the same holds when they're compared as Bazel * module versions.
*
* The special "empty string" version can also be used, and compares higher than everything else.
* It signifies that there is a NonRegistryOverride for a module.
*/
export class BzlmodVersion {
readonly original: string;
readonly release: VersionPart;
readonly prerelease: VersionPart;
readonly build: VersionPart;
/**
* The regular expression that identifies a valid Bazel module version.
*/
static readonly versionMatcher =
/^(?<release>[a-zA-Z0-9.]+)(?:-(?<prerelease>[a-zA-Z0-9.-]+))?(?:\+(?<build>[a-zA-Z0-9.-]+))?$/;
/**
* @param version The string that is parsed for the Bazel module version
* values.
*/
constructor(version: string) {
this.original = version;
if (version === '') {
this.release = VersionPart.create();
this.prerelease = VersionPart.create();
this.build = VersionPart.create();
return;
}
const vparts: Partial<VersionRegexResult> | undefined =
BzlmodVersion.versionMatcher.exec(version)?.groups;
if (!vparts) {
throw new Error(`Invalid Bazel module version: ${version}`);
}
// The regex check above ensures that we will have a release group.
const rparts = vparts.release!.split('.');
this.release = VersionPart.create(...rparts);
const pparts = vparts.prerelease ? vparts.prerelease.split('.') : [];
this.prerelease = VersionPart.create(...pparts);
// Do not parse the build value. Treat it as a single value.
const bparts = vparts.build ? [vparts.build] : [];
this.build = VersionPart.create(...bparts);
}
/**
* Specifies whether this is a pre-release version.
*/
get isPrerelease(): boolean {
return !this.prerelease.isEmpty;
}
// Comparison
/**
* Determines whether this Bazel module version is equal to the other.
*
* @param other The other version for the comparison.
* @param ignoreBuild? If specified, determines whether the build value is
* evaluated as part of the equality check. This is useful when
* determining precedence.
*/
equals(other: BzlmodVersion, ignoreBuild?: boolean): boolean {
if (ignoreBuild) {
return (
this.release.equals(other.release) &&
this.prerelease.equals(other.prerelease)
);
}
return (
this.release.equals(other.release) &&
this.prerelease.equals(other.prerelease) &&
this.build.equals(other.build)
);
}
/**
* Determines whether this Bazel module version comes before the other.
*/
isLessThan(other: BzlmodVersion): boolean {
// This logic mirrors the comparison logic in
// https://cs.opensource.google/bazel/bazel/+/refs/heads/master:src/main/java/com/google/devtools/build/lib/bazel/bzlmod/Version.java
if (this.release.isLessThan(other.release)) {
return true;
}
// Ensure that prerelease is listed before regular releases
if (this.isPrerelease && !other.isPrerelease) {
return true;
}
if (this.prerelease.isLessThan(other.prerelease)) {
return true;
}
// NOTE: We ignore the build value for precedence comparison per the Semver spec.
// https://semver.org/#spec-item-10
return false;
}
/**
* Determines whether this Bazel module version comes after the other.
*/
isGreaterThan(other: BzlmodVersion): boolean {
return BzlmodVersion.defaultCompare(this, other) === 1;
}
/**
* Evaluates two Bazel module versions and returns a value specifying whether
* a < b (-1), a == b (0), or a > b (1).
*/
static defaultCompare(a: BzlmodVersion, b: BzlmodVersion): number {
if (a.equals(b, true)) {
return 0;
}
if (a.isLessThan(b)) {
return -1;
}
return 1;
}
}