diff --git a/Gulpfile.js b/Gulpfile.js index b31462755ad..b2fb584d4a2 100644 --- a/Gulpfile.js +++ b/Gulpfile.js @@ -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() { diff --git a/scripts/build/countdown.js b/scripts/build/countdown.js new file mode 100644 index 00000000000..dda05352d28 --- /dev/null +++ b/scripts/build/countdown.js @@ -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; \ No newline at end of file diff --git a/scripts/build/exec.js b/scripts/build/exec.js index 04336321dd4..068bf33b9e9 100644 --- a/scripts/build/exec.js +++ b/scripts/build/exec.js @@ -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 => { diff --git a/scripts/build/project.js b/scripts/build/project.js index 933f7c44c65..8519e4c71ec 100644 --- a/scripts/build/project.js +++ b/scripts/build/project.js @@ -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; diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 1e495bf538e..d3974da9930 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -22233,7 +22233,7 @@ namespace ts { for (const decl of indexSymbol.declarations) { const declaration = 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; diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 1eaab5b1b59..5e4d42a890e 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -65,6 +65,7 @@ namespace ts { /* @internal */ namespace ts { + export const emptyArray: never[] = [] as never[]; /** Create a MapLike with good performance. */ function createDictionaryObject(): MapLike { diff --git a/src/compiler/semver.ts b/src/compiler/semver.ts index 203739f7c95..33623f45542 100644 --- a/src/compiler/semver.ts +++ b/src/compiler/semver.ts @@ -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>; + + 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>) { + // 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) { + 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>) { + return map(alternatives, formatAlternative).join(" || ") || "*"; + } + + function formatAlternative(comparators: ReadonlyArray) { + return map(comparators, formatComparator).join(" "); + } + + function formatComparator(comparator: Comparator) { + return `${comparator.operator}${comparator.operand}`; + } } \ No newline at end of file diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index e7eb352da84..0192aa38270 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -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 = createMap(); export const emptyUnderscoreEscapedMap: ReadonlyUnderscoreEscapedMap = emptyMap as ReadonlyUnderscoreEscapedMap; diff --git a/src/harness/utils.ts b/src/harness/utils.ts index 4650badadaf..14820f10529 100644 --- a/src/harness/utils.ts +++ b/src/harness/utils.ts @@ -82,4 +82,16 @@ namespace utils { export function addUTF8ByteOrderMark(text: string) { return getByteOrderMarkLength(text) === 0 ? "\u00EF\u00BB\u00BF" + text : text; } + + export function theory(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 || "" : + value === undefined ? "undefined" : + JSON.stringify(value); + } } \ No newline at end of file diff --git a/src/testRunner/unittests/semver.ts b/src/testRunner/unittests/semver.ts index edc888e1075..357a24307ca 100644 --- a/src/testRunner/unittests/semver.ts +++ b/src/testRunner/unittests/semver.ts @@ -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], + ]); }); }); } \ No newline at end of file