Add semver range parsing support

This commit is contained in:
Ron Buckton 2018-08-28 14:11:01 -07:00
parent 37ec065d93
commit 04a524511e
10 changed files with 483 additions and 19 deletions

View File

@ -570,12 +570,12 @@ gulp.task(
});
}, /*timeout*/ 100, { max: 500 });
gulp.watch(watchPatterns, () => project.wait().then(fn));
gulp.watch(watchPatterns, () => project.wait(runTestsSource && runTestsSource.token).then(fn));
// NOTE: gulp.watch is far too slow when watching tests/cases/**/* as it first enumerates *every* file
const testFilePattern = /(\.ts|[\\/]tsconfig\.json)$/;
fs.watch("tests/cases", { recursive: true }, (_, file) => {
if (testFilePattern.test(file)) project.wait().then(fn);
if (testFilePattern.test(file)) project.wait(runTestsSource && runTestsSource.token).then(fn);
});
function runTests() {

View File

@ -0,0 +1,62 @@
// @ts-check
const { CancelToken } = require("./cancellation");
class Countdown {
constructor(initialCount = 0) {
if (initialCount < 0) throw new Error();
this._remainingCount = initialCount;
this._promise = undefined;
this._resolve = undefined;
}
get remainingCount() {
return this._remainingCount;
}
add(count = 1) {
if (count < 1 || !isFinite(count) || Math.trunc(count) !== count) throw new Error();
if (this._remainingCount === 0) {
this._promise = undefined;
this._resolve = undefined;
}
this._remainingCount += count;
}
signal(count = 1) {
if (count < 1 || !isFinite(count) || Math.trunc(count) !== count) throw new Error();
if (this._remainingCount - count < 0) throw new Error();
this._remainingCount -= count;
if (this._remainingCount == 0) {
if (this._resolve) {
this._resolve();
}
return true;
}
return false;
}
/** @param {CancelToken} [token] */
wait(token) {
if (!this._promise) {
this._promise = new Promise(resolve => { this._resolve = resolve; });
}
if (this._remainingCount === 0) {
this._resolve();
}
if (!token) return this._promise;
return new Promise((resolve, reject) => {
const subscription = token.subscribe(reject);
this._promise.then(
value => {
subscription.unsubscribe();
resolve(value);
},
error => {
subscription.unsubscribe();
reject(error);
});
});
}
}
exports.Countdown = Countdown;

View File

@ -25,8 +25,10 @@ function exec(cmd, args, options = {}) {
const command = isWin ? [possiblyQuote(cmd), ...args] : [`${cmd} ${args.join(" ")}`];
const ex = cp.spawn(isWin ? "cmd" : "/bin/sh", [subshellFlag, ...command], { stdio: "inherit", windowsVerbatimArguments: true });
const subscription = options.cancelToken && options.cancelToken.subscribe(() => {
log(`${chalk.red("killing")} '${chalk.green(cmd)} ${args.join(" ")}'...`);
ex.kill("SIGINT");
ex.kill("SIGTERM");
ex.kill();
reject(new CancelError());
});
ex.on("exit", exitCode => {

View File

@ -13,8 +13,27 @@ const del = require("del");
const needsUpdate = require("./needsUpdate");
const mkdirp = require("./mkdirp");
const { reportDiagnostics } = require("./diagnostics");
const { Countdown } = require("./countdown");
const { CancelToken } = require("./cancellation");
const countdown = new Countdown();
class CompilationGulp extends gulp.Gulp {
constructor() {
super();
this.on("start", () => {
const onDone = () => {
this.removeListener("stop", onDone);
this.removeListener("err", onDone);
countdown.signal();
};
this.on("stop", onDone);
this.on("err", onDone);
countdown.add();
});
}
/**
* @param {boolean} [verbose]
*/
@ -38,6 +57,17 @@ class ForkedGulp extends gulp.Gulp {
constructor(tasks) {
super();
this.tasks = tasks;
this.on("start", () => {
const onDone = () => {
this.removeListener("stop", onDone);
this.removeListener("err", onDone);
countdown.signal();
};
this.on("stop", onDone);
this.on("err", onDone);
countdown.add();
});
}
// Do not reset tasks
@ -211,22 +241,10 @@ exports.flatten = flatten;
/**
* Returns a Promise that resolves when all pending build tasks have completed
* @param {CancelToken} [token]
*/
function wait() {
return new Promise(resolve => {
if (compilationGulp.allDone()) {
resolve();
}
else {
const onDone = () => {
compilationGulp.removeListener("onDone", onDone);
compilationGulp.removeListener("err", onDone);
resolve();
};
compilationGulp.on("stop", onDone);
compilationGulp.on("err", onDone);
}
});
function wait(token) {
return countdown.wait(token);
}
exports.wait = wait;

View File

@ -22233,7 +22233,7 @@ namespace ts {
for (const decl of indexSymbol.declarations) {
const declaration = <SignatureDeclaration>decl;
if (declaration.parameters.length === 1 && declaration.parameters[0].type) {
switch (declaration.parameters[0].type!.kind) {
switch (declaration.parameters[0].type.kind) {
case SyntaxKind.StringKeyword:
if (!seenStringIndexer) {
seenStringIndexer = true;

View File

@ -65,6 +65,7 @@ namespace ts {
/* @internal */
namespace ts {
export const emptyArray: never[] = [] as never[];
/** Create a MapLike with good performance. */
function createDictionaryObject<T>(): MapLike<T> {

View File

@ -30,6 +30,8 @@ namespace ts {
* Describes a precise semantic version number, https://semver.org
*/
export class Version {
static readonly zero = new Version(0, 0, 0);
readonly major: number;
readonly minor: number;
readonly patch: number;
@ -85,6 +87,15 @@ namespace ts {
|| comparePrerelaseIdentifiers(this.prerelease, other.prerelease);
}
increment(field: "major" | "minor" | "patch") {
switch (field) {
case "major": return new Version(this.major + 1, 0, 0);
case "minor": return new Version(this.major, this.minor + 1, 0);
case "patch": return new Version(this.major, this.minor, this.patch + 1);
default: return Debug.assertNever(field);
}
}
toString() {
let result = `${this.major}.${this.minor}.${this.patch}`;
if (some(this.prerelease)) result += `-${this.prerelease.join(".")}`;
@ -152,4 +163,229 @@ namespace ts {
// > of the preceding identifiers are equal.
return compareValues(left.length, right.length);
}
/**
* Describes a semantic version range, per https://github.com/npm/node-semver#ranges
*/
export class VersionRange {
private _alternatives: ReadonlyArray<ReadonlyArray<Comparator>>;
constructor(spec: string) {
this._alternatives = spec ? Debug.assertDefined(parseRange(spec), "Invalid range spec.") : emptyArray;
}
static tryParse(text: string) {
const sets = parseRange(text);
if (sets) {
const range = new VersionRange("");
range._alternatives = sets;
return range;
}
return undefined;
}
test(version: Version | string) {
if (typeof version === "string") version = new Version(version);
return testDisjunction(version, this._alternatives);
}
toString() {
return formatDisjunction(this._alternatives);
}
}
interface Comparator {
readonly operator: "<" | "<=" | ">" | ">=" | "=";
readonly operand: Version;
}
// https://github.com/npm/node-semver#range-grammar
//
// range-set ::= range ( logical-or range ) *
// range ::= hyphen | simple ( ' ' simple ) * | ''
// logical-or ::= ( ' ' ) * '||' ( ' ' ) *
const logicalOrRegExp = /\s*\|\|\s*/g;
const whitespaceRegExp = /\s+/g;
// https://github.com/npm/node-semver#range-grammar
//
// partial ::= xr ( '.' xr ( '.' xr qualifier ? )? )?
// xr ::= 'x' | 'X' | '*' | nr
// nr ::= '0' | ['1'-'9'] ( ['0'-'9'] ) *
// qualifier ::= ( '-' pre )? ( '+' build )?
// pre ::= parts
// build ::= parts
// parts ::= part ( '.' part ) *
// part ::= nr | [-0-9A-Za-z]+
const partialRegExp = /^([xX*0]|[1-9]\d*)(?:\.([xX*0]|[1-9]\d*)(?:\.([xX*0]|[1-9]\d*)(?:-([a-z0-9-.]+))?(?:\+([a-z0-9-.]+))?)?)?$/i;
// https://github.com/npm/node-semver#range-grammar
//
// hyphen ::= partial ' - ' partial
const hyphenRegExp = /^\s*([a-z0-9-+.*]+)\s+-\s+([a-z0-9-+.*]+)\s*$/i;
// https://github.com/npm/node-semver#range-grammar
//
// simple ::= primitive | partial | tilde | caret
// primitive ::= ( '<' | '>' | '>=' | '<=' | '=' ) partial
// tilde ::= '~' partial
// caret ::= '^' partial
const rangeRegExp = /^\s*(~|\^|<|<=|>|>=|=)?\s*([a-z0-9-+.*]+)$/i;
function parseRange(text: string) {
const alternatives: Comparator[][] = [];
for (const range of text.trim().split(logicalOrRegExp)) {
if (!range) continue;
const comparators: Comparator[] = [];
const match = hyphenRegExp.exec(range);
if (match) {
if (!parseHyphen(match[1], match[2], comparators)) return undefined;
}
else {
for (const simple of range.split(whitespaceRegExp)) {
const match = rangeRegExp.exec(simple);
if (!match || !parseComparator(match[1], match[2], comparators)) return undefined;
}
}
alternatives.push(comparators);
}
return alternatives;
}
function parsePartial(text: string) {
const match = partialRegExp.exec(text);
if (!match) return undefined;
const [, major, minor = "*", patch = "*", prerelease, build] = match;
const version = new Version(
isWildcard(major) ? 0 : parseInt(major, 10),
isWildcard(major) || isWildcard(minor) ? 0 : parseInt(minor, 10),
isWildcard(major) || isWildcard(minor) || isWildcard(patch) ? 0 : parseInt(patch, 10),
prerelease,
build);
return { version, major, minor, patch };
}
function parseHyphen(left: string, right: string, comparators: Comparator[]) {
const leftResult = parsePartial(left);
if (!leftResult) return false;
const rightResult = parsePartial(right);
if (!rightResult) return false;
if (!isWildcard(leftResult.major)) {
comparators.push(createComparator(">=", leftResult.version));
}
if (!isWildcard(rightResult.major)) {
comparators.push(
isWildcard(rightResult.minor) ? createComparator("<", rightResult.version.increment("major")) :
isWildcard(rightResult.patch) ? createComparator("<", rightResult.version.increment("minor")) :
createComparator("<=", rightResult.version));
}
return true;
}
function parseComparator(operator: string, text: string, comparators: Comparator[]) {
const result = parsePartial(text);
if (!result) return false;
const { version, major, minor, patch } = result;
if (!isWildcard(major)) {
switch (operator) {
case "~":
comparators.push(createComparator(">=", version));
comparators.push(createComparator("<", version.increment(
isWildcard(minor) ? "major" :
"minor")));
break;
case "^":
comparators.push(createComparator(">=", version));
comparators.push(createComparator("<", version.increment(
version.major > 0 || isWildcard(minor) ? "major" :
version.minor > 0 || isWildcard(patch) ? "minor" :
"patch")));
break;
case "<":
case ">=":
comparators.push(createComparator(operator, version));
break;
case "<=":
case ">":
comparators.push(
isWildcard(minor) ? createComparator(operator === "<=" ? "<" : ">=", version.increment("major")) :
isWildcard(patch) ? createComparator(operator === "<=" ? "<" : ">=", version.increment("minor")) :
createComparator(operator, version));
break;
case "=":
case undefined:
if (isWildcard(minor) || isWildcard(patch)) {
comparators.push(createComparator(">=", version));
comparators.push(createComparator("<", version.increment(isWildcard(minor) ? "major" : "minor")));
}
else {
comparators.push(createComparator("=", version));
}
break;
default:
// unrecognized
return false;
}
}
else if (operator === "<" || operator === ">") {
comparators.push(createComparator("<", Version.zero));
}
return true;
}
function isWildcard(part: string) {
return part === "*" || part === "x" || part === "X";
}
function createComparator(operator: Comparator["operator"], operand: Version) {
return { operator, operand };
}
function testDisjunction(version: Version, alternatives: ReadonlyArray<ReadonlyArray<Comparator>>) {
// an empty disjunction is treated as "*" (all versions)
if (alternatives.length === 0) return true;
for (const alternative of alternatives) {
if (testAlternative(version, alternative)) return true;
}
return false;
}
function testAlternative(version: Version, comparators: ReadonlyArray<Comparator>) {
for (const comparator of comparators) {
if (!testComparator(version, comparator.operator, comparator.operand)) return false;
}
return true;
}
function testComparator(version: Version, operator: Comparator["operator"], operand: Version) {
const cmp = version.compareTo(operand);
switch (operator) {
case "<": return cmp < 0;
case "<=": return cmp <= 0;
case ">": return cmp > 0;
case ">=": return cmp >= 0;
case "=": return cmp === 0;
default: return Debug.assertNever(operator);
}
}
function formatDisjunction(alternatives: ReadonlyArray<ReadonlyArray<Comparator>>) {
return map(alternatives, formatAlternative).join(" || ") || "*";
}
function formatAlternative(comparators: ReadonlyArray<Comparator>) {
return map(comparators, formatComparator).join(" ");
}
function formatComparator(comparator: Comparator) {
return `${comparator.operator}${comparator.operand}`;
}
}

View File

@ -14,7 +14,6 @@ namespace ts {
/* @internal */
namespace ts {
export const emptyArray: never[] = [] as never[];
export const resolvingEmptyArray: never[] = [] as never[];
export const emptyMap: ReadonlyMap<never> = createMap<never>();
export const emptyUnderscoreEscapedMap: ReadonlyUnderscoreEscapedMap<never> = emptyMap as ReadonlyUnderscoreEscapedMap<never>;

View File

@ -82,4 +82,16 @@ namespace utils {
export function addUTF8ByteOrderMark(text: string) {
return getByteOrderMarkLength(text) === 0 ? "\u00EF\u00BB\u00BF" + text : text;
}
export function theory<T extends any[]>(name: string, cb: (...args: T) => void, data: T[]) {
for (const entry of data) {
it(`${name}(${entry.map(formatTheoryDatum).join(", ")})`, () => cb(...entry));
}
}
function formatTheoryDatum(value: any) {
return typeof value === "function" ? value.name || "<anonymous function>" :
value === undefined ? "undefined" :
JSON.stringify(value);
}
}

View File

@ -1,4 +1,5 @@
namespace ts {
import theory = utils.theory;
describe("semver", () => {
describe("Version", () => {
function assertVersion(version: Version, [major, minor, patch, prerelease, build]: [number, number, number, string[]?, string[]?]) {
@ -89,6 +90,139 @@ namespace ts {
// > Build metadata does not figure into precedence
assert.strictEqual(new Version("1.0.0+build").compareTo(new Version("1.0.0")), Comparison.EqualTo);
});
it("increment", () => {
assertVersion(new Version(1, 2, 3, "pre.4", "build.5").increment("major"), [2, 0, 0]);
assertVersion(new Version(1, 2, 3, "pre.4", "build.5").increment("minor"), [1, 3, 0]);
assertVersion(new Version(1, 2, 3, "pre.4", "build.5").increment("patch"), [1, 2, 4]);
});
});
describe("VersionRange", () => {
function assertRange(rangeText: string, versionText: string, inRange = true) {
const range = new VersionRange(rangeText);
const version = new Version(versionText);
assert.strictEqual(range.test(version), inRange, `Expected version '${version}' ${inRange ? `to be` : `to not be`} in range '${rangeText}' (${range})`);
}
theory("comparators", assertRange, [
["", "1.0.0"],
["*", "1.0.0"],
["1", "1.0.0"],
["1", "2.0.0", false],
["1.0", "1.0.0"],
["1.0", "1.1.0", false],
["1.0.0", "1.0.0"],
["1.0.0", "1.0.1", false],
["1.*", "1.0.0"],
["1.*", "2.0.0", false],
["1.x", "1.0.0"],
["1.x", "2.0.0", false],
["=1", "1.0.0"],
["=1", "1.1.0"],
["=1", "1.0.1"],
["=1.0", "1.0.0"],
["=1.0", "1.0.1"],
["=1.0.0", "1.0.0"],
["=*", "0.0.0"],
["=*", "1.0.0"],
[">1", "2"],
[">1.0", "1.1"],
[">1.0.0", "1.0.1"],
[">1.0.0", "1.0.1-pre"],
[">*", "0.0.0", false],
[">*", "1.0.0", false],
[">=1", "1.0.0"],
[">=1.0", "1.0.0"],
[">=1.0.0", "1.0.0"],
[">=1.0.0", "1.0.1-pre"],
[">=*", "0.0.0"],
[">=*", "1.0.0"],
["<2", "1.0.0"],
["<2.1", "2.0.0"],
["<2.0.1", "2.0.0"],
["<2.0.0", "2.0.0-pre"],
["<*", "0.0.0", false],
["<*", "1.0.0", false],
["<=2", "2.0.0"],
["<=2.1", "2.1.0"],
["<=2.0.1", "2.0.1"],
["<=*", "0.0.0"],
["<=*", "1.0.0"],
]);
theory("conjunctions", assertRange, [
[">1.0.0 <2.0.0", "1.0.1"],
[">1.0.0 <2.0.0", "2.0.0", false],
[">1.0.0 <2.0.0", "1.0.0", false],
[">1 >2", "3.0.0"],
]);
theory("disjunctions", assertRange, [
[">=1.0.0 <2.0.0 || >=3.0.0 <4.0.0", "1.0.0"],
[">=1.0.0 <2.0.0 || >=3.0.0 <4.0.0", "2.0.0", false],
[">=1.0.0 <2.0.0 || >=3.0.0 <4.0.0", "3.0.0"],
]);
theory("hyphen", assertRange, [
["1.0.0 - 2.0.0", "1.0.0"],
["1.0.0 - 2.0.0", "2.0.0"],
["1.0.0 - 2.0.0", "3.0.0", false],
]);
theory("tilde", assertRange, [
["~0", "0.0.0"],
["~0", "0.1.0"],
["~0", "0.1.2"],
["~0", "0.1.9"],
["~0", "1.0.0", false],
["~0.1", "0.1.0"],
["~0.1", "0.1.2"],
["~0.1", "0.1.9"],
["~0.1", "0.2.0", false],
["~0.1.2", "0.1.2"],
["~0.1.2", "0.1.9"],
["~0.1.2", "0.2.0", false],
["~1", "1.0.0"],
["~1", "1.2.0"],
["~1", "1.2.3"],
["~1", "1.2.0"],
["~1", "1.2.3"],
["~1", "0.0.0", false],
["~1", "2.0.0", false],
["~1.2", "1.2.0"],
["~1.2", "1.2.3"],
["~1.2", "1.1.0", false],
["~1.2", "1.3.0", false],
["~1.2.3", "1.2.3"],
["~1.2.3", "1.2.9"],
["~1.2.3", "1.1.0", false],
["~1.2.3", "1.3.0", false],
]);
theory("caret", assertRange, [
["^0", "0.0.0"],
["^0", "0.1.0"],
["^0", "0.9.0"],
["^0", "0.1.2"],
["^0", "0.1.9"],
["^0", "1.0.0", false],
["^0.1", "0.1.0"],
["^0.1", "0.1.2"],
["^0.1", "0.1.9"],
["^0.1.2", "0.1.2"],
["^0.1.2", "0.1.9"],
["^0.1.2", "0.0.0", false],
["^0.1.2", "0.2.0", false],
["^0.1.2", "1.0.0", false],
["^1", "1.0.0"],
["^1", "1.2.0"],
["^1", "1.2.3"],
["^1", "1.9.0"],
["^1", "0.0.0", false],
["^1", "2.0.0", false],
["^1.2", "1.2.0"],
["^1.2", "1.2.3"],
["^1.2", "1.9.0"],
["^1.2", "1.1.0", false],
["^1.2", "2.0.0", false],
["^1.2.3", "1.2.3"],
["^1.2.3", "1.9.0"],
["^1.2.3", "1.2.2", false],
["^1.2.3", "2.0.0", false],
]);
});
});
}