• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/* @internal */
2namespace ts {
3    // https://semver.org/#spec-item-2
4    // > A normal version number MUST take the form X.Y.Z where X, Y, and Z are non-negative
5    // > integers, and MUST NOT contain leading zeroes. X is the major version, Y is the minor
6    // > version, and Z is the patch version. Each element MUST increase numerically.
7    //
8    // NOTE: We differ here in that we allow X and X.Y, with missing parts having the default
9    // value of `0`.
10    const versionRegExp = /^(0|[1-9]\d*)(?:\.(0|[1-9]\d*)(?:\.(0|[1-9]\d*)(?:\-([a-z0-9-.]+))?(?:\+([a-z0-9-.]+))?)?)?$/i;
11
12    // https://semver.org/#spec-item-9
13    // > A pre-release version MAY be denoted by appending a hyphen and a series of dot separated
14    // > identifiers immediately following the patch version. Identifiers MUST comprise only ASCII
15    // > alphanumerics and hyphen [0-9A-Za-z-]. Identifiers MUST NOT be empty. Numeric identifiers
16    // > MUST NOT include leading zeroes.
17    const prereleaseRegExp = /^(?:0|[1-9]\d*|[a-z-][a-z0-9-]*)(?:\.(?:0|[1-9]\d*|[a-z-][a-z0-9-]*))*$/i;
18
19    // https://semver.org/#spec-item-10
20    // > Build metadata MAY be denoted by appending a plus sign and a series of dot separated
21    // > identifiers immediately following the patch or pre-release version. Identifiers MUST
22    // > comprise only ASCII alphanumerics and hyphen [0-9A-Za-z-]. Identifiers MUST NOT be empty.
23    const buildRegExp = /^[a-z0-9-]+(?:\.[a-z0-9-]+)*$/i;
24
25    // https://semver.org/#spec-item-9
26    // > Numeric identifiers MUST NOT include leading zeroes.
27    const numericIdentifierRegExp = /^(0|[1-9]\d*)$/;
28
29    /**
30     * Describes a precise semantic version number, https://semver.org
31     */
32    export class Version {
33        static readonly zero = new Version(0, 0, 0);
34
35        readonly major: number;
36        readonly minor: number;
37        readonly patch: number;
38        readonly prerelease: readonly string[];
39        readonly build: readonly string[];
40
41        constructor(text: string);
42        constructor(major: number, minor?: number, patch?: number, prerelease?: string, build?: string);
43        constructor(major: number | string, minor = 0, patch = 0, prerelease = "", build = "") {
44            if (typeof major === "string") {
45                const result = Debug.checkDefined(tryParseComponents(major), "Invalid version");
46                ({ major, minor, patch, prerelease, build } = result);
47            }
48
49            Debug.assert(major >= 0, "Invalid argument: major");
50            Debug.assert(minor >= 0, "Invalid argument: minor");
51            Debug.assert(patch >= 0, "Invalid argument: patch");
52            Debug.assert(!prerelease || prereleaseRegExp.test(prerelease), "Invalid argument: prerelease");
53            Debug.assert(!build || buildRegExp.test(build), "Invalid argument: build");
54            this.major = major;
55            this.minor = minor;
56            this.patch = patch;
57            this.prerelease = prerelease ? prerelease.split(".") : emptyArray;
58            this.build = build ? build.split(".") : emptyArray;
59        }
60
61        static tryParse(text: string) {
62            const result = tryParseComponents(text);
63            if (!result) return undefined;
64
65            const { major, minor, patch, prerelease, build } = result;
66            return new Version(major, minor, patch, prerelease, build);
67        }
68
69        compareTo(other: Version | undefined) {
70            // https://semver.org/#spec-item-11
71            // > Precedence is determined by the first difference when comparing each of these
72            // > identifiers from left to right as follows: Major, minor, and patch versions are
73            // > always compared numerically.
74            //
75            // https://semver.org/#spec-item-11
76            // > Precedence for two pre-release versions with the same major, minor, and patch version
77            // > MUST be determined by comparing each dot separated identifier from left to right until
78            // > a difference is found [...]
79            //
80            // https://semver.org/#spec-item-11
81            // > Build metadata does not figure into precedence
82            if (this === other) return Comparison.EqualTo;
83            if (other === undefined) return Comparison.GreaterThan;
84            return compareValues(this.major, other.major)
85                || compareValues(this.minor, other.minor)
86                || compareValues(this.patch, other.patch)
87                || comparePrereleaseIdentifiers(this.prerelease, other.prerelease);
88        }
89
90        increment(field: "major" | "minor" | "patch") {
91            switch (field) {
92                case "major": return new Version(this.major + 1, 0, 0);
93                case "minor": return new Version(this.major, this.minor + 1, 0);
94                case "patch": return new Version(this.major, this.minor, this.patch + 1);
95                default: return Debug.assertNever(field);
96            }
97        }
98
99        toString() {
100            let result = `${this.major}.${this.minor}.${this.patch}`;
101            if (some(this.prerelease)) result += `-${this.prerelease.join(".")}`;
102            if (some(this.build)) result += `+${this.build.join(".")}`;
103            return result;
104        }
105    }
106
107    function tryParseComponents(text: string) {
108        const match = versionRegExp.exec(text);
109        if (!match) return undefined;
110
111        const [, major, minor = "0", patch = "0", prerelease = "", build = ""] = match;
112        if (prerelease && !prereleaseRegExp.test(prerelease)) return undefined;
113        if (build && !buildRegExp.test(build)) return undefined;
114        return {
115            major: parseInt(major, 10),
116            minor: parseInt(minor, 10),
117            patch: parseInt(patch, 10),
118            prerelease,
119            build
120        };
121    }
122
123    function comparePrereleaseIdentifiers(left: readonly string[], right: readonly string[]) {
124        // https://semver.org/#spec-item-11
125        // > When major, minor, and patch are equal, a pre-release version has lower precedence
126        // > than a normal version.
127        if (left === right) return Comparison.EqualTo;
128        if (left.length === 0) return right.length === 0 ? Comparison.EqualTo : Comparison.GreaterThan;
129        if (right.length === 0) return Comparison.LessThan;
130
131        // https://semver.org/#spec-item-11
132        // > Precedence for two pre-release versions with the same major, minor, and patch version
133        // > MUST be determined by comparing each dot separated identifier from left to right until
134        // > a difference is found [...]
135        const length = Math.min(left.length, right.length);
136        for (let i = 0; i < length; i++) {
137            const leftIdentifier = left[i];
138            const rightIdentifier = right[i];
139            if (leftIdentifier === rightIdentifier) continue;
140
141            const leftIsNumeric = numericIdentifierRegExp.test(leftIdentifier);
142            const rightIsNumeric = numericIdentifierRegExp.test(rightIdentifier);
143            if (leftIsNumeric || rightIsNumeric) {
144                // https://semver.org/#spec-item-11
145                // > Numeric identifiers always have lower precedence than non-numeric identifiers.
146                if (leftIsNumeric !== rightIsNumeric) return leftIsNumeric ? Comparison.LessThan : Comparison.GreaterThan;
147
148                // https://semver.org/#spec-item-11
149                // > identifiers consisting of only digits are compared numerically
150                const result = compareValues(+leftIdentifier, +rightIdentifier);
151                if (result) return result;
152            }
153            else {
154                // https://semver.org/#spec-item-11
155                // > identifiers with letters or hyphens are compared lexically in ASCII sort order.
156                const result = compareStringsCaseSensitive(leftIdentifier, rightIdentifier);
157                if (result) return result;
158            }
159        }
160
161        // https://semver.org/#spec-item-11
162        // > A larger set of pre-release fields has a higher precedence than a smaller set, if all
163        // > of the preceding identifiers are equal.
164        return compareValues(left.length, right.length);
165    }
166
167    /**
168     * Describes a semantic version range, per https://github.com/npm/node-semver#ranges
169     */
170    export class VersionRange {
171        private _alternatives: readonly (readonly Comparator[])[];
172
173        constructor(spec: string) {
174            this._alternatives = spec ? Debug.checkDefined(parseRange(spec), "Invalid range spec.") : emptyArray;
175        }
176
177        static tryParse(text: string) {
178            const sets = parseRange(text);
179            if (sets) {
180                const range = new VersionRange("");
181                range._alternatives = sets;
182                return range;
183            }
184            return undefined;
185        }
186
187        test(version: Version | string) {
188            if (typeof version === "string") version = new Version(version);
189            return testDisjunction(version, this._alternatives);
190        }
191
192        toString() {
193            return formatDisjunction(this._alternatives);
194        }
195    }
196
197    interface Comparator {
198        readonly operator: "<" | "<=" | ">" | ">=" | "=";
199        readonly operand: Version;
200    }
201
202    // https://github.com/npm/node-semver#range-grammar
203    //
204    // range-set    ::= range ( logical-or range ) *
205    // range        ::= hyphen | simple ( ' ' simple ) * | ''
206    // logical-or   ::= ( ' ' ) * '||' ( ' ' ) *
207    const logicalOrRegExp = /\s*\|\|\s*/g;
208    const whitespaceRegExp = /\s+/g;
209
210    // https://github.com/npm/node-semver#range-grammar
211    //
212    // partial      ::= xr ( '.' xr ( '.' xr qualifier ? )? )?
213    // xr           ::= 'x' | 'X' | '*' | nr
214    // nr           ::= '0' | ['1'-'9'] ( ['0'-'9'] ) *
215    // qualifier    ::= ( '-' pre )? ( '+' build )?
216    // pre          ::= parts
217    // build        ::= parts
218    // parts        ::= part ( '.' part ) *
219    // part         ::= nr | [-0-9A-Za-z]+
220    const partialRegExp = /^([xX*0]|[1-9]\d*)(?:\.([xX*0]|[1-9]\d*)(?:\.([xX*0]|[1-9]\d*)(?:-([a-z0-9-.]+))?(?:\+([a-z0-9-.]+))?)?)?$/i;
221
222    // https://github.com/npm/node-semver#range-grammar
223    //
224    // hyphen       ::= partial ' - ' partial
225    const hyphenRegExp = /^\s*([a-z0-9-+.*]+)\s+-\s+([a-z0-9-+.*]+)\s*$/i;
226
227    // https://github.com/npm/node-semver#range-grammar
228    //
229    // simple       ::= primitive | partial | tilde | caret
230    // primitive    ::= ( '<' | '>' | '>=' | '<=' | '=' ) partial
231    // tilde        ::= '~' partial
232    // caret        ::= '^' partial
233    const rangeRegExp = /^\s*(~|\^|<|<=|>|>=|=)?\s*([a-z0-9-+.*]+)$/i;
234
235    function parseRange(text: string) {
236        const alternatives: Comparator[][] = [];
237        for (const range of text.trim().split(logicalOrRegExp)) {
238            if (!range) continue;
239            const comparators: Comparator[] = [];
240            const match = hyphenRegExp.exec(range);
241            if (match) {
242                if (!parseHyphen(match[1], match[2], comparators)) return undefined;
243            }
244            else {
245                for (const simple of range.split(whitespaceRegExp)) {
246                    const match = rangeRegExp.exec(simple);
247                    if (!match || !parseComparator(match[1], match[2], comparators)) return undefined;
248                }
249            }
250            alternatives.push(comparators);
251        }
252        return alternatives;
253    }
254
255    function parsePartial(text: string) {
256        const match = partialRegExp.exec(text);
257        if (!match) return undefined;
258
259        const [, major, minor = "*", patch = "*", prerelease, build] = match;
260        const version = new Version(
261            isWildcard(major) ? 0 : parseInt(major, 10),
262            isWildcard(major) || isWildcard(minor) ? 0 : parseInt(minor, 10),
263            isWildcard(major) || isWildcard(minor) || isWildcard(patch) ? 0 : parseInt(patch, 10),
264            prerelease,
265            build);
266
267        return { version, major, minor, patch };
268    }
269
270    function parseHyphen(left: string, right: string, comparators: Comparator[]) {
271        const leftResult = parsePartial(left);
272        if (!leftResult) return false;
273
274        const rightResult = parsePartial(right);
275        if (!rightResult) return false;
276
277        if (!isWildcard(leftResult.major)) {
278            comparators.push(createComparator(">=", leftResult.version));
279        }
280
281        if (!isWildcard(rightResult.major)) {
282            comparators.push(
283                isWildcard(rightResult.minor) ? createComparator("<", rightResult.version.increment("major")) :
284                isWildcard(rightResult.patch) ? createComparator("<", rightResult.version.increment("minor")) :
285                createComparator("<=", rightResult.version));
286        }
287
288        return true;
289    }
290
291    function parseComparator(operator: string, text: string, comparators: Comparator[]) {
292        const result = parsePartial(text);
293        if (!result) return false;
294
295        const { version, major, minor, patch } = result;
296        if (!isWildcard(major)) {
297            switch (operator) {
298                case "~":
299                    comparators.push(createComparator(">=", version));
300                    comparators.push(createComparator("<", version.increment(
301                        isWildcard(minor) ? "major" :
302                        "minor")));
303                    break;
304                case "^":
305                    comparators.push(createComparator(">=", version));
306                    comparators.push(createComparator("<", version.increment(
307                        version.major > 0 || isWildcard(minor) ? "major" :
308                        version.minor > 0 || isWildcard(patch) ? "minor" :
309                        "patch")));
310                    break;
311                case "<":
312                case ">=":
313                    comparators.push(createComparator(operator, version));
314                    break;
315                case "<=":
316                case ">":
317                    comparators.push(
318                        isWildcard(minor) ? createComparator(operator === "<=" ? "<" : ">=", version.increment("major")) :
319                        isWildcard(patch) ? createComparator(operator === "<=" ? "<" : ">=", version.increment("minor")) :
320                        createComparator(operator, version));
321                    break;
322                case "=":
323                case undefined:
324                    if (isWildcard(minor) || isWildcard(patch)) {
325                        comparators.push(createComparator(">=", version));
326                        comparators.push(createComparator("<", version.increment(isWildcard(minor) ? "major" : "minor")));
327                    }
328                    else {
329                        comparators.push(createComparator("=", version));
330                    }
331                    break;
332                default:
333                    // unrecognized
334                    return false;
335            }
336        }
337        else if (operator === "<" || operator === ">") {
338            comparators.push(createComparator("<", Version.zero));
339        }
340
341        return true;
342    }
343
344    function isWildcard(part: string) {
345        return part === "*" || part === "x" || part === "X";
346    }
347
348    function createComparator(operator: Comparator["operator"], operand: Version) {
349        return { operator, operand };
350    }
351
352    function testDisjunction(version: Version, alternatives: readonly (readonly Comparator[])[]) {
353        // an empty disjunction is treated as "*" (all versions)
354        if (alternatives.length === 0) return true;
355        for (const alternative of alternatives) {
356            if (testAlternative(version, alternative)) return true;
357        }
358        return false;
359    }
360
361    function testAlternative(version: Version, comparators: readonly Comparator[]) {
362        for (const comparator of comparators) {
363            if (!testComparator(version, comparator.operator, comparator.operand)) return false;
364        }
365        return true;
366    }
367
368    function testComparator(version: Version, operator: Comparator["operator"], operand: Version) {
369        const cmp = version.compareTo(operand);
370        switch (operator) {
371            case "<": return cmp < 0;
372            case "<=": return cmp <= 0;
373            case ">": return cmp > 0;
374            case ">=": return cmp >= 0;
375            case "=": return cmp === 0;
376            default: return Debug.assertNever(operator);
377        }
378    }
379
380    function formatDisjunction(alternatives: readonly (readonly Comparator[])[]) {
381        return map(alternatives, formatAlternative).join(" || ") || "*";
382    }
383
384    function formatAlternative(comparators: readonly Comparator[]) {
385        return map(comparators, formatComparator).join(" ");
386    }
387
388    function formatComparator(comparator: Comparator) {
389        return `${comparator.operator}${comparator.operand}`;
390    }
391}
392