diff --git a/src/harness/collections.ts b/src/harness/collections.ts deleted file mode 100644 index d311216a915..00000000000 --- a/src/harness/collections.ts +++ /dev/null @@ -1,244 +0,0 @@ -/// -namespace collections { - // NOTE: Some of the functions here duplicate functionality from compiler/core.ts. They have been added - // to reduce the number of direct dependencies on compiler and services to eventually break away - // from depending directly on the compiler to speed up compilation time. - - import binarySearch = ts.binarySearch; - - function compareValues(a: string | number, b: string | number): number { - if (a === b) return 0; - if (a === undefined) return -1; - if (b === undefined) return +1; - return a < b ? -1 : +1; - } - - export function compareNumbers(a: number, b: number): number { - return compareValues(a, b); - } - - export function compareStrings(a: string, b: string, ignoreCase: boolean): number { - return ignoreCase - ? compareStringsCaseInsensitive(a, b) - : compareStringsCaseSensitive(a, b); - } - - export function compareStringsCaseSensitive(a: string, b: string): number { - return compareValues(a, b); - } - - export function compareStringsCaseInsensitive(a: string, b: string): number { - if (a === b) return 0; - if (a === undefined) return -1; - if (b === undefined) return +1; - a = a.toUpperCase(); - b = b.toUpperCase(); - return a < b ? -1 : a > b ? +1 : 0; - } - - export function equateStringsCaseSensitive(a: string, b: string): boolean { - return a === b; - } - - export function equateStringsCaseInsensitive(a: string, b: string): boolean { - return a === b - || a !== undefined - && b !== undefined - && a.toUpperCase() === b.toUpperCase(); - } - - function removeAt(array: T[], index: number): void { - for (let i = index; i < array.length - 1; i++) { - array[i] = array[i + 1]; - } - array.pop(); - } - - function insertAt(array: T[], index: number, value: T) { - if (index === 0) { - array.unshift(value); - } - else if (index === array.length) { - array.push(value); - } - else { - for (let i = array.length; i > index; i--) { - array[i] = array[i - 1]; - } - array[index] = value; - } - } - - /** - * A collection of key/value pairs internally sorted by key. - */ - export class KeyedCollection { - private _comparer: (a: K, b: K) => number; - private _keys: K[] = []; - private _values: V[] = []; - private _order: number[] = []; - private _version = 0; - private _copyOnWrite = false; - - constructor(comparer: (a: K, b: K) => number) { - this._comparer = comparer; - } - - public get size() { - return this._keys.length; - } - - public has(key: K) { - return binarySearch(this._keys, key, ts.identity, this._comparer) >= 0; - } - - public get(key: K) { - const index = binarySearch(this._keys, key, ts.identity, this._comparer); - return index >= 0 ? this._values[index] : undefined; - } - - public set(key: K, value: V) { - const index = binarySearch(this._keys, key, ts.identity, this._comparer); - if (index >= 0) { - this._values[index] = value; - } - else { - this.writePreamble(); - insertAt(this._keys, ~index, key); - insertAt(this._values, ~index, value); - insertAt(this._order, ~index, this._version); - this._version++; - } - return this; - } - - public delete(key: K) { - const index = binarySearch(this._keys, key, ts.identity, this._comparer); - if (index >= 0) { - this.writePreamble(); - removeAt(this._keys, index); - removeAt(this._values, index); - removeAt(this._order, index); - this._version++; - return true; - } - return false; - } - - public clear() { - if (this.size > 0) { - this.writePreamble(); - this._keys.length = 0; - this._values.length = 0; - this._order.length = 0; - this._version = 0; - } - } - - public forEach(callback: (value: V, key: K, collection: this) => void) { - const keys = this._keys; - const values = this._values; - const order = this.getInsertionOrder(); - const version = this._version; - this._copyOnWrite = true; - for (const index of order) { - callback(values[index], keys[index], this); - } - if (version === this._version) { - this._copyOnWrite = false; - } - } - - private writePreamble() { - if (this._copyOnWrite) { - this._keys = this._keys.slice(); - this._values = this._values.slice(); - this._order = this._order.slice(); - this._copyOnWrite = false; - } - } - - private getInsertionOrder() { - return this._order - .map((_, i) => i) - .sort((x, y) => compareNumbers(this._order[x], this._order[y])); - } - } - - const undefinedSentinel = {}; - - function escapeKey(text: string) { - return (text.length >= 2 && text.charAt(0) === "_" && text.charAt(1) === "_" ? "_" + text : text); - } - - function unescapeKey(text: string) { - return (text.length >= 3 && text.charAt(0) === "_" && text.charAt(1) === "_" && text.charAt(2) === "_" ? text.slice(1) : text); - } - - /** - * A collection of metadata that supports inheritance. - */ - export class Metadata { - private _parent: Metadata | undefined; - private _map: { [key: string]: any }; - private _version = 0; - private _size = -1; - private _parentVersion: number | undefined; - - constructor(parent?: Metadata) { - this._parent = parent; - this._map = Object.create(parent ? parent._map : null); // tslint:disable-line:no-null-keyword - } - - public get size(): number { - if (this._size === -1 || (this._parent && this._parent._version !== this._parentVersion)) { - let size = 0; - for (const _ in this._map) size++; - this._size = size; - if (this._parent) { - this._parentVersion = this._parent._version; - } - } - return this._size; - } - - public has(key: string): boolean { - return this._map[escapeKey(key)] !== undefined; - } - - public get(key: string): any { - const value = this._map[escapeKey(key)]; - return value === undefinedSentinel ? undefined : value; - } - - public set(key: string, value: any): this { - this._map[escapeKey(key)] = value === undefined ? undefinedSentinel : value; - this._size = -1; - this._version++; - return this; - } - - public delete(key: string): boolean { - const escapedKey = escapeKey(key); - if (this._map[escapedKey] !== undefined) { - delete this._map[escapedKey]; - this._size = -1; - this._version++; - return true; - } - return false; - } - - public clear(): void { - this._map = Object.create(this._parent ? this._parent._map : null); // tslint:disable-line:no-null-keyword - this._size = -1; - this._version++; - } - - public forEach(callback: (value: any, key: string, map: this) => void) { - for (const key in this._map) { - callback(this._map[key], unescapeKey(key), this); - } - } - } -} \ No newline at end of file diff --git a/src/harness/compiler.ts b/src/harness/compiler.ts index 6b4fcc62422..680f1f60978 100644 --- a/src/harness/compiler.ts +++ b/src/harness/compiler.ts @@ -1,32 +1,30 @@ /// /// -/// +/// /// /// /// -namespace compiler { - import TextDocument = documents.TextDocument; - import SourceMap = documents.SourceMap; - import KeyedCollection = collections.KeyedCollection; - import VirtualFileSystem = vfs.VirtualFileSystem; +// NOTE: The contents of this file are all exported from the namespace 'compiler'. This is to +// support the eventual conversion of harness into a modular system. +namespace compiler { export class CompilerHost { public readonly vfs: vfs.VirtualFileSystem; public readonly defaultLibLocation: string; - public readonly outputs: TextDocument[] = []; + public readonly outputs: documents.TextDocument[] = []; public readonly traces: string[] = []; public readonly shouldAssertInvariants = !Harness.lightMode; private _setParentNodes: boolean; - private _sourceFiles: KeyedCollection; + private _sourceFiles: core.KeyedCollection; private _newLine: string; private _parseConfigHost: ParseConfigHost; - constructor(vfs: VirtualFileSystem, options: ts.CompilerOptions, setParentNodes = false) { + constructor(vfs: vfs.VirtualFileSystem, options: ts.CompilerOptions, setParentNodes = false) { this.vfs = vfs; this.defaultLibLocation = vfs.metadata.get("defaultLibLocation") || ""; - this._sourceFiles = new KeyedCollection(this.vfs.pathComparer); + this._sourceFiles = new core.KeyedCollection(this.vfs.pathComparer); this._newLine = options.newLine === ts.NewLineKind.LineFeed ? "\n" : "\r\n"; this._setParentNodes = setParentNodes; } @@ -66,7 +64,7 @@ namespace compiler { public readFile(path: string): string | undefined { const entry = this.vfs.getFile(path); - const content = entry && entry.content && utils.removeByteOrderMark(entry.content); + const content = entry && entry.content && core.removeByteOrderMark(entry.content); return content === undefined ? undefined : vpath.extname(path) === ".json" ? utils.removeComments(content, utils.CommentRemoval.leadingAndTrailing) : content; @@ -77,7 +75,7 @@ namespace compiler { if (writeByteOrderMark) content = "\u00EF\u00BB\u00BF" + content; const entry = this.vfs.addFile(fileName, content, { overwrite: true }); if (entry) { - const document = new TextDocument(fileName, content); + const document = new documents.TextDocument(fileName, content); document.meta.set("fileName", fileName); entry.metadata.set("document", document); const index = this.outputs.findIndex(output => this.vfs.pathComparer(document.file, output.file) === 0); @@ -115,7 +113,7 @@ namespace compiler { if (file) { let content = file.content; if (content !== undefined) { - content = utils.removeByteOrderMark(content); + content = core.removeByteOrderMark(content); // We cache and reuse source files for files we know shouldn't change. const shouldCache = vpath.isDefaultLibrary(fileName) || @@ -153,9 +151,9 @@ namespace compiler { } export class ParseConfigHost { - public readonly vfs: VirtualFileSystem; + public readonly vfs: vfs.VirtualFileSystem; - constructor(vfs: VirtualFileSystem) { + constructor(vfs: vfs.VirtualFileSystem) { this.vfs = vfs; } @@ -218,10 +216,10 @@ namespace compiler { } export interface CompilationOutput { - readonly input: TextDocument; - readonly js: TextDocument | undefined; - readonly dts: TextDocument | undefined; - readonly map: TextDocument | undefined; + readonly input: documents.TextDocument; + readonly js: documents.TextDocument | undefined; + readonly dts: documents.TextDocument | undefined; + readonly map: documents.TextDocument | undefined; } export class CompilationResult { @@ -230,12 +228,12 @@ namespace compiler { public readonly result: ts.EmitResult | undefined; public readonly options: ts.CompilerOptions; public readonly diagnostics: ts.Diagnostic[]; - public readonly js: KeyedCollection; - public readonly dts: KeyedCollection; - public readonly maps: KeyedCollection; + public readonly js: core.KeyedCollection; + public readonly dts: core.KeyedCollection; + public readonly maps: core.KeyedCollection; - private _inputs: TextDocument[] = []; - private _inputsAndOutputs: KeyedCollection; + private _inputs: documents.TextDocument[] = []; + private _inputsAndOutputs: core.KeyedCollection; constructor(host: CompilerHost, options: ts.CompilerOptions, program: ts.Program | undefined, result: ts.EmitResult | undefined, diagnostics: ts.Diagnostic[]) { this.host = host; @@ -245,9 +243,9 @@ namespace compiler { this.options = program ? program.getCompilerOptions() : options; // collect outputs - this.js = new KeyedCollection(this.vfs.pathComparer); - this.dts = new KeyedCollection(this.vfs.pathComparer); - this.maps = new KeyedCollection(this.vfs.pathComparer); + this.js = new core.KeyedCollection(this.vfs.pathComparer); + this.dts = new core.KeyedCollection(this.vfs.pathComparer); + this.maps = new core.KeyedCollection(this.vfs.pathComparer); for (const document of this.host.outputs) { if (vpath.isJavaScript(document.file)) { this.js.set(document.file, document); @@ -261,11 +259,11 @@ namespace compiler { } // correlate inputs and outputs - this._inputsAndOutputs = new KeyedCollection(this.vfs.pathComparer); + this._inputsAndOutputs = new core.KeyedCollection(this.vfs.pathComparer); if (program) { for (const sourceFile of program.getSourceFiles()) { if (sourceFile) { - const input = new TextDocument(sourceFile.fileName, sourceFile.text); + const input = new documents.TextDocument(sourceFile.fileName, sourceFile.text); this._inputs.push(input); if (!vpath.isDeclaration(sourceFile.fileName)) { const outputs = { @@ -289,11 +287,11 @@ namespace compiler { return this.host.vfs; } - public get inputs(): ReadonlyArray { + public get inputs(): ReadonlyArray { return this._inputs; } - public get outputs(): ReadonlyArray { + public get outputs(): ReadonlyArray { return this.host.outputs; } @@ -318,25 +316,25 @@ namespace compiler { return this._inputsAndOutputs.get(vpath.resolve(this.vfs.currentDirectory, path)); } - public getInput(path: string): TextDocument | undefined { + public getInput(path: string): documents.TextDocument | undefined { const outputs = this.getInputsAndOutputs(path); return outputs && outputs.input; } - public getOutput(path: string, kind: "js" | "dts" | "map"): TextDocument | undefined { + public getOutput(path: string, kind: "js" | "dts" | "map"): documents.TextDocument | undefined { const outputs = this.getInputsAndOutputs(path); return outputs && outputs[kind]; } - public getSourceMap(path: string): SourceMap | undefined { + public getSourceMap(path: string): documents.SourceMap | undefined { if (this.options.noEmit || vpath.isDeclaration(path)) return undefined; if (this.options.inlineSourceMap) { const document = this.getOutput(path, "js"); - return document && SourceMap.fromSource(document.text); + return document && documents.SourceMap.fromSource(document.text); } if (this.options.sourceMap) { const document = this.getOutput(path, "map"); - return document && new SourceMap(document.file, document.text); + return document && new documents.SourceMap(document.file, document.text); } } diff --git a/src/harness/compilerRunner.ts b/src/harness/compilerRunner.ts index 7aa26b9c8cd..5eb78f746df 100644 --- a/src/harness/compilerRunner.ts +++ b/src/harness/compilerRunner.ts @@ -45,151 +45,6 @@ class CompilerBaselineRunner extends RunnerBase { return this.enumerateFiles(this.basePath, /\.tsx?$/, { recursive: true }); } - private makeUnitName(name: string, root: string) { - const path = ts.toPath(name, root, utils.identity); - const pathStart = ts.toPath(Harness.IO.getCurrentDirectory(), "", utils.identity); - return pathStart ? path.replace(pathStart, "/") : path; - } - - public checkTestCodeOutput(fileName: string) { - describe(`${this.testSuiteName} tests for ${fileName}`, () => { - // Mocha holds onto the closure environment of the describe callback even after the test is done. - // Everything declared here should be cleared out in the "after" callback. - let justName: string; - let lastUnit: Harness.TestCaseParser.TestUnitData; - let harnessSettings: Harness.TestCaseParser.CompilerSettings; - let hasNonDtsFiles: boolean; - - let result: Harness.Compiler.CompilerResult; - let options: ts.CompilerOptions; - let tsConfigFiles: Harness.Compiler.TestFile[]; - // equivalent to the files that will be passed on the command line - let toBeCompiled: Harness.Compiler.TestFile[]; - // equivalent to other files on the file system not directly passed to the compiler (ie things that are referenced by other files) - let otherFiles: Harness.Compiler.TestFile[]; - - before(() => { - justName = vpath.basename(fileName); - const content = Harness.IO.readFile(fileName); - const rootDir = fileName.indexOf("conformance") === -1 ? "tests/cases/compiler/" : ts.getDirectoryPath(fileName) + "/"; - const testCaseContent = Harness.TestCaseParser.makeUnitsFromTest(content, fileName, rootDir); - const units = testCaseContent.testUnitData; - harnessSettings = testCaseContent.settings; - let tsConfigOptions: ts.CompilerOptions; - tsConfigFiles = []; - if (testCaseContent.tsConfig) { - assert.equal(testCaseContent.tsConfig.fileNames.length, 0, `list of files in tsconfig is not currently supported`); - - tsConfigOptions = ts.cloneCompilerOptions(testCaseContent.tsConfig.options); - tsConfigFiles.push(this.createHarnessTestFile(testCaseContent.tsConfigFileUnitData, rootDir, ts.combinePaths(rootDir, tsConfigOptions.configFilePath))); - } - else { - const baseUrl = harnessSettings.baseUrl; - if (baseUrl !== undefined && !ts.isRootedDiskPath(baseUrl)) { - harnessSettings.baseUrl = ts.getNormalizedAbsolutePath(baseUrl, rootDir); - } - } - - lastUnit = units[units.length - 1]; - hasNonDtsFiles = ts.forEach(units, unit => !ts.fileExtensionIs(unit.name, ts.Extension.Dts)); - // We need to assemble the list of input files for the compiler and other related files on the 'filesystem' (ie in a multi-file test) - // If the last file in a test uses require or a triple slash reference we'll assume all other files will be brought in via references, - // otherwise, assume all files are just meant to be in the same compilation session without explicit references to one another. - toBeCompiled = []; - otherFiles = []; - - if (testCaseContent.settings.noImplicitReferences || /require\(/.test(lastUnit.content) || /reference\spath/.test(lastUnit.content)) { - toBeCompiled.push(this.createHarnessTestFile(lastUnit, rootDir)); - units.forEach(unit => { - if (unit.name !== lastUnit.name) { - otherFiles.push(this.createHarnessTestFile(unit, rootDir)); - } - }); - } - else { - toBeCompiled = units.map(unit => { - return this.createHarnessTestFile(unit, rootDir); - }); - } - - if (tsConfigOptions && tsConfigOptions.configFilePath !== undefined) { - tsConfigOptions.configFilePath = ts.combinePaths(rootDir, tsConfigOptions.configFilePath); - tsConfigOptions.configFile.fileName = tsConfigOptions.configFilePath; - } - - const output = Harness.Compiler.compileFiles( - toBeCompiled, otherFiles, harnessSettings, /*options*/ tsConfigOptions, /*currentDirectory*/ harnessSettings.currentDirectory); - - options = output.options; - result = output.result; - }); - - after(() => { - // Mocha holds onto the closure environment of the describe callback even after the test is done. - // Therefore we have to clean out large objects after the test is done. - justName = undefined; - lastUnit = undefined; - hasNonDtsFiles = undefined; - result = undefined; - options = undefined; - toBeCompiled = undefined; - otherFiles = undefined; - tsConfigFiles = undefined; - }); - - // check errors - it("Correct errors for " + fileName, () => { - Harness.Compiler.doErrorBaseline(justName, tsConfigFiles.concat(toBeCompiled, otherFiles), result.errors, !!options.pretty); - }); - - it (`Correct module resolution tracing for ${fileName}`, () => { - if (options.traceResolution) { - Harness.Baseline.runBaseline(justName.replace(/\.tsx?$/, ".trace.json"), () => { - return utils.removeTestPathPrefixes(JSON.stringify(result.traceResults || [], undefined, 4)); - }); - } - }); - - // Source maps? - it("Correct sourcemap content for " + fileName, () => { - if (options.sourceMap || options.inlineSourceMap) { - Harness.Baseline.runBaseline(justName.replace(/\.tsx?$/, ".sourcemap.txt"), () => { - const record = utils.removeTestPathPrefixes(result.getSourceMapRecord()); - if ((options.noEmitOnError && result.errors.length !== 0) || record === undefined) { - // Because of the noEmitOnError option no files are created. We need to return null because baselining isn't required. - /* tslint:disable:no-null-keyword */ - return null; - /* tslint:enable:no-null-keyword */ - } - return record; - }); - } - }); - - it("Correct JS output for " + fileName, () => { - if (hasNonDtsFiles && this.emit) { - Harness.Compiler.doJsEmitBaseline(justName, fileName, options, result, tsConfigFiles, toBeCompiled, otherFiles, harnessSettings); - } - }); - - it("Correct Sourcemap output for " + fileName, () => { - Harness.Compiler.doSourcemapBaseline(justName, options, result, harnessSettings); - }); - - it("Correct type/symbol baselines for " + fileName, () => { - if (fileName.indexOf("APISample") >= 0) { - return; - } - - Harness.Compiler.doTypeAndSymbolBaseline(justName, result.program, toBeCompiled.concat(otherFiles).filter(file => !!result.program.getSourceFile(file.unitName))); - }); - }); - } - - private createHarnessTestFile(lastUnit: Harness.TestCaseParser.TestUnitData, rootDir: string, unitName?: string): Harness.Compiler.TestFile { - return { unitName: unitName || this.makeUnitName(lastUnit.name, rootDir), content: lastUnit.content, fileOptions: lastUnit.fileOptions }; - } - public initializeTests() { describe(this.testSuiteName + " tests", () => { describe("Setup compiler for compiler baselines", () => { @@ -197,19 +52,33 @@ class CompilerBaselineRunner extends RunnerBase { }); // this will set up a series of describe/it blocks to run between the setup and cleanup phases - if (this.tests.length === 0) { - const testFiles = this.enumerateTestFiles(); - testFiles.forEach(fn => { - fn = fn.replace(/\\/g, "/"); - this.checkTestCodeOutput(fn); - }); - } - else { - this.tests.forEach(test => this.checkTestCodeOutput(test)); - } + const files = this.tests.length > 0 ? this.tests : this.enumerateTestFiles(); + files.forEach(file => { this.checkTestCodeOutput(vpath.normalizeSeparators(file)); }); }); } + public checkTestCodeOutput(fileName: string) { + for (const { name, payload } of CompilerTest.getConfigurations(fileName)) { + describe(`${this.testSuiteName} tests for ${fileName}${name ? ` (${name})` : ``}`, () => { + this.runSuite(fileName, payload); + }); + } + } + + private runSuite(fileName: string, testCaseContent: Harness.TestCaseParser.TestCaseContent) { + // Mocha holds onto the closure environment of the describe callback even after the test is done. + // Everything declared here should be cleared out in the "after" callback. + let compilerTest: CompilerTest | undefined; + before(() => { compilerTest = new CompilerTest(fileName, testCaseContent); }); + it(`Correct errors for ${fileName}`, () => { compilerTest.verifyDiagnostics(); }); + it(`Correct module resolution tracing for ${fileName}`, () => { compilerTest.verifyModuleResolution(); }); + it(`Correct sourcemap content for ${fileName}`, () => { compilerTest.verifySourceMapRecord(); }); + it(`Correct JS output for ${fileName}`, () => { if (this.emit) compilerTest.verifyJavaScriptOutput(); }); + it(`Correct Sourcemap output for ${fileName}`, () => { compilerTest.verifySourceMapOutput(); }); + it(`Correct type/symbol baselines for ${fileName}`, () => { compilerTest.verifyTypesAndSymbols(); }); + after(() => { compilerTest = undefined; }); + } + private parseOptions() { if (this.options && this.options.length > 0) { this.emit = false; @@ -226,4 +95,191 @@ class CompilerBaselineRunner extends RunnerBase { } } } +} + +interface CompilerTestConfiguration { + name: string; + payload: Harness.TestCaseParser.TestCaseContent; +} + +class CompilerTest { + private fileName: string; + private justName: string; + private lastUnit: Harness.TestCaseParser.TestUnitData; + private harnessSettings: Harness.TestCaseParser.CompilerSettings; + private hasNonDtsFiles: boolean; + private result: Harness.Compiler.CompilerResult; + private options: ts.CompilerOptions; + private tsConfigFiles: Harness.Compiler.TestFile[]; + // equivalent to the files that will be passed on the command line + private toBeCompiled: Harness.Compiler.TestFile[]; + // equivalent to other files on the file system not directly passed to the compiler (ie things that are referenced by other files) + private otherFiles: Harness.Compiler.TestFile[]; + + constructor(fileName: string, testCaseContent: Harness.TestCaseParser.TestCaseContent) { + this.fileName = fileName; + this.justName = vpath.basename(fileName); + const rootDir = fileName.indexOf("conformance") === -1 ? "tests/cases/compiler/" : ts.getDirectoryPath(fileName) + "/"; + const units = testCaseContent.testUnitData; + this.harnessSettings = testCaseContent.settings; + let tsConfigOptions: ts.CompilerOptions; + this.tsConfigFiles = []; + if (testCaseContent.tsConfig) { + assert.equal(testCaseContent.tsConfig.fileNames.length, 0, `list of files in tsconfig is not currently supported`); + + tsConfigOptions = ts.cloneCompilerOptions(testCaseContent.tsConfig.options); + this.tsConfigFiles.push(this.createHarnessTestFile(testCaseContent.tsConfigFileUnitData, rootDir, ts.combinePaths(rootDir, tsConfigOptions.configFilePath))); + } + else { + const baseUrl = this.harnessSettings.baseUrl; + if (baseUrl !== undefined && !ts.isRootedDiskPath(baseUrl)) { + this.harnessSettings.baseUrl = ts.getNormalizedAbsolutePath(baseUrl, rootDir); + } + } + + this.lastUnit = units[units.length - 1]; + this.hasNonDtsFiles = ts.forEach(units, unit => !ts.fileExtensionIs(unit.name, ts.Extension.Dts)); + // We need to assemble the list of input files for the compiler and other related files on the 'filesystem' (ie in a multi-file test) + // If the last file in a test uses require or a triple slash reference we'll assume all other files will be brought in via references, + // otherwise, assume all files are just meant to be in the same compilation session without explicit references to one another. + this.toBeCompiled = []; + this.otherFiles = []; + + if (testCaseContent.settings.noImplicitReferences || /require\(/.test(this.lastUnit.content) || /reference\spath/.test(this.lastUnit.content)) { + this.toBeCompiled.push(this.createHarnessTestFile(this.lastUnit, rootDir)); + units.forEach(unit => { + if (unit.name !== this.lastUnit.name) { + this.otherFiles.push(this.createHarnessTestFile(unit, rootDir)); + } + }); + } + else { + this.toBeCompiled = units.map(unit => { + return this.createHarnessTestFile(unit, rootDir); + }); + } + + if (tsConfigOptions && tsConfigOptions.configFilePath !== undefined) { + tsConfigOptions.configFilePath = ts.combinePaths(rootDir, tsConfigOptions.configFilePath); + tsConfigOptions.configFile.fileName = tsConfigOptions.configFilePath; + } + + const output = Harness.Compiler.compileFiles( + this.toBeCompiled, + this.otherFiles, + this.harnessSettings, + /*options*/ tsConfigOptions, + /*currentDirectory*/ this.harnessSettings.currentDirectory); + + this.options = output.options; + this.result = output.result; + } + + public static getConfigurations(fileName: string) { + const content = Harness.IO.readFile(fileName); + const rootDir = fileName.indexOf("conformance") === -1 ? "tests/cases/compiler/" : ts.getDirectoryPath(fileName) + "/"; + const testCaseContent = Harness.TestCaseParser.makeUnitsFromTest(content, fileName, rootDir); + const configurations: CompilerTestConfiguration[] = []; + const scriptTargets = this._split(testCaseContent.settings.target); + const moduleKinds = this._split(testCaseContent.settings.module); + for (const scriptTarget of scriptTargets) { + for (const moduleKind of moduleKinds) { + let name = ""; + if (moduleKinds.length > 1) { + name += `@module: ${moduleKind || "none"}`; + } + if (scriptTargets.length > 1) { + if (name) name += ", "; + name += `@target: ${scriptTarget || "none"}`; + } + + const settings = { ...testCaseContent.settings }; + if (scriptTarget) settings.target = scriptTarget; + if (moduleKind) settings.module = moduleKind; + configurations.push({ name, payload: { ...testCaseContent, settings } }); + } + } + + return configurations; + } + + public verifyDiagnostics() { + // check errors + Harness.Compiler.doErrorBaseline( + this.justName, + this.tsConfigFiles.concat(this.toBeCompiled, this.otherFiles), + this.result.errors, + !!this.options.pretty); + } + + public verifyModuleResolution() { + if (this.options.traceResolution) { + Harness.Baseline.runBaseline(this.justName.replace(/\.tsx?$/, ".trace.json"), () => { + return utils.removeTestPathPrefixes(JSON.stringify(this.result.traceResults || [], undefined, 4)); + }); + } + } + + public verifySourceMapRecord() { + if (this.options.sourceMap || this.options.inlineSourceMap) { + Harness.Baseline.runBaseline(this.justName.replace(/\.tsx?$/, ".sourcemap.txt"), () => { + const record = utils.removeTestPathPrefixes(this.result.getSourceMapRecord()); + if ((this.options.noEmitOnError && this.result.errors.length !== 0) || record === undefined) { + // Because of the noEmitOnError option no files are created. We need to return null because baselining isn't required. + /* tslint:disable:no-null-keyword */ + return null; + /* tslint:enable:no-null-keyword */ + } + return record; + }); + } + } + + public verifyJavaScriptOutput() { + if (this.hasNonDtsFiles) { + Harness.Compiler.doJsEmitBaseline( + this.justName, + this.fileName, + this.options, + this.result, + this.tsConfigFiles, + this.toBeCompiled, + this.otherFiles, + this.harnessSettings); + } + } + + public verifySourceMapOutput() { + Harness.Compiler.doSourcemapBaseline( + this.justName, + this.options, + this.result, + this.harnessSettings); + } + + public verifyTypesAndSymbols() { + if (this.fileName.indexOf("APISample") >= 0) { + return; + } + + Harness.Compiler.doTypeAndSymbolBaseline( + this.justName, + this.result.program, + this.toBeCompiled.concat(this.otherFiles).filter(file => !!this.result.program.getSourceFile(file.unitName))); + } + + private static _split(text: string) { + const entries = text && text.split(",").map(s => s.toLowerCase().trim()).filter(s => s.length > 0); + return entries && entries.length > 0 ? entries : [""]; + } + + private makeUnitName(name: string, root: string) { + const path = ts.toPath(name, root, core.identity); + const pathStart = ts.toPath(Harness.IO.getCurrentDirectory(), "", core.identity); + return pathStart ? path.replace(pathStart, "/") : path; + } + + private createHarnessTestFile(lastUnit: Harness.TestCaseParser.TestUnitData, rootDir: string, unitName?: string): Harness.Compiler.TestFile { + return { unitName: unitName || this.makeUnitName(lastUnit.name, rootDir), content: lastUnit.content, fileOptions: lastUnit.fileOptions }; + } } \ No newline at end of file diff --git a/src/harness/core.ts b/src/harness/core.ts new file mode 100644 index 00000000000..9efe53c4d8a --- /dev/null +++ b/src/harness/core.ts @@ -0,0 +1,421 @@ +/// + +// NOTE: The contents of this file are all exported from the namespace 'core'. This is to +// support the eventual conversion of harness into a modular system. + +// NOTE: Some of the functions here duplicate functionality from compiler/core.ts. They have been added +// to reduce the number of direct dependencies on compiler and services to eventually break away +// from depending directly on the compiler to speed up compilation time. + +namespace core { + export function identity(v: T): T { return v; } + + // + // Comparers + // + + export type Comparer = (x: T, y: T) => number; + export type EqualityComparer = (x: T, y: T) => boolean; + + export function compareNumbers(a: number, b: number): number { + if (a === b) return 0; + if (a === undefined) return -1; + if (b === undefined) return +1; + return a < b ? -1 : +1; + } + + export function compareStrings(a: string, b: string, ignoreCase: boolean): number { + return ignoreCase + ? compareStringsCaseInsensitive(a, b) + : compareStringsCaseSensitive(a, b); + } + + // NOTE: This is a duplicate of `compareNumbers` above, but is intended to be used only with + // strings to reduce polymorphism. + export function compareStringsCaseSensitive(a: string, b: string): number { + if (a === b) return 0; + if (a === undefined) return -1; + if (b === undefined) return +1; + return a < b ? -1 : +1; + } + + export function compareStringsCaseInsensitive(a: string, b: string): number { + if (a === b) return 0; + if (a === undefined) return -1; + if (b === undefined) return +1; + a = a.toUpperCase(); + b = b.toUpperCase(); + return a < b ? -1 : a > b ? +1 : 0; + } + + export function equateStringsCaseSensitive(a: string, b: string): boolean { + return a === b; + } + + export function equateStringsCaseInsensitive(a: string, b: string): boolean { + return a === b + || a !== undefined + && b !== undefined + && a.toUpperCase() === b.toUpperCase(); + } + + // + // Collections + // + + /** + * A collection of key/value pairs internally sorted by key. + */ + export class KeyedCollection { + private _comparer: (a: K, b: K) => number; + private _keys: K[] = []; + private _values: V[] = []; + private _order: number[] = []; + private _version = 0; + private _copyOnWrite = false; + + constructor(comparer: (a: K, b: K) => number) { + this._comparer = comparer; + } + + public get size() { + return this._keys.length; + } + + public has(key: K) { + return binarySearch(this._keys, key, identity, this._comparer) >= 0; + } + + public get(key: K) { + const index = binarySearch(this._keys, key, identity, this._comparer); + return index >= 0 ? this._values[index] : undefined; + } + + public set(key: K, value: V) { + const index = binarySearch(this._keys, key, identity, this._comparer); + if (index >= 0) { + this._values[index] = value; + } + else { + this.writePreamble(); + insertAt(this._keys, ~index, key); + insertAt(this._values, ~index, value); + insertAt(this._order, ~index, this._version); + this._version++; + } + return this; + } + + public delete(key: K) { + const index = binarySearch(this._keys, key, identity, this._comparer); + if (index >= 0) { + this.writePreamble(); + removeAt(this._keys, index); + removeAt(this._values, index); + removeAt(this._order, index); + this._version++; + return true; + } + return false; + } + + public clear() { + if (this.size > 0) { + this.writePreamble(); + this._keys.length = 0; + this._values.length = 0; + this._order.length = 0; + this._version = 0; + } + } + + public forEach(callback: (value: V, key: K, collection: this) => void) { + const keys = this._keys; + const values = this._values; + const order = this.getInsertionOrder(); + const version = this._version; + this._copyOnWrite = true; + for (const index of order) { + callback(values[index], keys[index], this); + } + if (version === this._version) { + this._copyOnWrite = false; + } + } + + private writePreamble() { + if (this._copyOnWrite) { + this._keys = this._keys.slice(); + this._values = this._values.slice(); + this._order = this._order.slice(); + this._copyOnWrite = false; + } + } + + private getInsertionOrder() { + return this._order + .map((_, i) => i) + .sort((x, y) => compareNumbers(this._order[x], this._order[y])); + } + } + + /** + * A collection of metadata that supports inheritance. + */ + export class Metadata { + private static readonly _undefinedValue = {}; + private _parent: Metadata | undefined; + private _map: { [key: string]: any }; + private _version = 0; + private _size = -1; + private _parentVersion: number | undefined; + + constructor(parent?: Metadata) { + this._parent = parent; + this._map = Object.create(parent ? parent._map : null); // tslint:disable-line:no-null-keyword + } + + public get size(): number { + if (this._size === -1 || (this._parent && this._parent._version !== this._parentVersion)) { + let size = 0; + for (const _ in this._map) size++; + this._size = size; + if (this._parent) { + this._parentVersion = this._parent._version; + } + } + return this._size; + } + + public has(key: string): boolean { + return this._map[Metadata._escapeKey(key)] !== undefined; + } + + public get(key: string): any { + const value = this._map[Metadata._escapeKey(key)]; + return value === Metadata._undefinedValue ? undefined : value; + } + + public set(key: string, value: any): this { + this._map[Metadata._escapeKey(key)] = value === undefined ? Metadata._undefinedValue : value; + this._size = -1; + this._version++; + return this; + } + + public delete(key: string): boolean { + const escapedKey = Metadata._escapeKey(key); + if (this._map[escapedKey] !== undefined) { + delete this._map[escapedKey]; + this._size = -1; + this._version++; + return true; + } + return false; + } + + public clear(): void { + this._map = Object.create(this._parent ? this._parent._map : null); // tslint:disable-line:no-null-keyword + this._size = -1; + this._version++; + } + + public forEach(callback: (value: any, key: string, map: this) => void) { + for (const key in this._map) { + callback(this._map[key], Metadata._unescapeKey(key), this); + } + } + + private static _escapeKey(text: string) { + return (text.length >= 2 && text.charAt(0) === "_" && text.charAt(1) === "_" ? "_" + text : text); + } + + private static _unescapeKey(text: string) { + return (text.length >= 3 && text.charAt(0) === "_" && text.charAt(1) === "_" && text.charAt(2) === "_" ? text.slice(1) : text); + } + } + + export function binarySearch(array: ReadonlyArray, value: T, keySelector: (v: T) => U, keyComparer: Comparer, offset?: number): number { + if (!array || array.length === 0) { + return -1; + } + + let low = offset || 0; + let high = array.length - 1; + const key = keySelector(value); + while (low <= high) { + const middle = low + ((high - low) >> 1); + const midKey = keySelector(array[middle]); + const result = keyComparer(midKey, key); + if (result < 0) { + low = middle + 1; + } + else if (result > 0) { + high = middle - 1; + } + else { + return middle; + } + } + + return ~low; + } + + export function removeAt(array: T[], index: number): void { + for (let i = index; i < array.length - 1; i++) { + array[i] = array[i + 1]; + } + + array.length--; + } + + export function insertAt(array: T[], index: number, value: T): void { + if (index === 0) { + array.unshift(value); + } + else if (index === array.length) { + array.push(value); + } + else { + for (let i = array.length; i > index; i--) { + array[i] = array[i - 1]; + } + array[index] = value; + } + } + + export function stableSort(array: T[], comparer: (x: T, y: T) => number): T[] { + return array + .map((_, i) => i) // create array of indices + .sort((x, y) => comparer(array[x], array[y]) || x - y) // sort indices by value then position + .map(i => array[i]); // get sorted array + } + + // + // Strings + // + + export function padLeft(text: string, size: number, ch = " "): string { + while (text.length < size) text = ch + text; + return text; + } + + export function padRight(text: string, size: number, ch = " "): string { + while (text.length < size) text += ch; + return text; + } + + export function getByteOrderMarkLength(text: string): number { + if (text.length >= 2) { + const ch0 = text.charCodeAt(0); + const ch1 = text.charCodeAt(1); + if ((ch0 === 0xff && ch1 === 0xfe) || + (ch0 === 0xfe && ch1 === 0xff)) { + return 2; + } + if (text.length >= 3 && ch0 === 0xef && ch1 === 0xbb && text.charCodeAt(2) === 0xbf) { + return 3; + } + } + return 0; + } + + export function removeByteOrderMark(text: string): string { + const length = getByteOrderMarkLength(text); + return length ? text.slice(length) : text; + } + + function splitLinesWorker(text: string, lineStarts: number[] | undefined, lines: string[] | undefined, removeEmptyElements: boolean) { + let pos = 0; + let end = 0; + let lineStart = 0; + let nonWhiteSpace = false; + while (pos < text.length) { + const ch = text.charCodeAt(pos); + end = pos; + pos++; + switch (ch) { + // LineTerminator + case 0x000d: // carriage return + if (pos < text.length && text.charCodeAt(pos) === 0x000a) { + pos++; + } + // falls through + + case 0x000a: // line feed + case 0x2028: // line separator + case 0x2029: // paragraph separator + if (lineStarts) { + lineStarts.push(lineStart); + } + if (lines && (!removeEmptyElements || nonWhiteSpace)) { + lines.push(text.slice(lineStart, end)); + } + lineStart = pos; + nonWhiteSpace = false; + break; + + // WhiteSpace + case 0x0009: // tab + case 0x000b: // vertical tab + case 0x000c: // form feed + case 0x0020: // space + case 0x00a0: // no-break space + case 0xfeff: // zero width no-break space + case 0x1680: // ogham space mark + case 0x2000: // en quad + case 0x2001: // em quad + case 0x2002: // en space + case 0x2003: // em space + case 0x2004: // three-per-em space + case 0x2005: // four-per-em space + case 0x2006: // six-per-em space + case 0x2007: // figure space + case 0x2008: // punctuation space + case 0x2009: // thin space + case 0x200a: // hair space + case 0x202f: // narrow no-break space + case 0x205f: // medium mathematical space + case 0x3000: // ideographic space + case 0x0085: // next-line (not strictly per spec, but used by the compiler) + break; + + default: + nonWhiteSpace = true; + break; + } + } + if (lineStarts) { + lineStarts.push(lineStart); + } + if (lines && (!removeEmptyElements || nonWhiteSpace)) { + lines.push(text.slice(lineStart, text.length)); + } + } + + export type LineStarts = ReadonlyArray; + + export interface LinesAndLineStarts { + readonly lines: ReadonlyArray; + readonly lineStarts: LineStarts; + } + + export function getLinesAndLineStarts(text: string): LinesAndLineStarts { + const lines: string[] = []; + const lineStarts: number[] = []; + splitLinesWorker(text, lineStarts, lines, /*removeEmptyElements*/ false); + return { lines, lineStarts }; + } + + export function splitLines(text: string, removeEmptyElements = false): string[] { + const lines: string[] = []; + splitLinesWorker(text, /*lineStarts*/ undefined, lines, removeEmptyElements); + return lines; + } + + export function computeLineStarts(text: string): LineStarts { + const lineStarts: number[] = []; + splitLinesWorker(text, lineStarts, /*lines*/ undefined, /*removeEmptyElements*/ false); + return lineStarts; + } +} \ No newline at end of file diff --git a/src/harness/documents.ts b/src/harness/documents.ts index b460cc8393f..a5df3a6af52 100644 --- a/src/harness/documents.ts +++ b/src/harness/documents.ts @@ -1,4 +1,7 @@ -/// +/// + +// NOTE: The contents of this file are all exported from the namespace 'documents'. This is to +// support the eventual conversion of harness into a modular system. namespace documents { export class TextDocument { @@ -6,16 +9,16 @@ namespace documents { public readonly file: string; public readonly text: string; - private _lineStarts: number[] | undefined; + private _lineStarts: core.LineStarts | undefined; - constructor(file: string, content: string, meta?: Map) { + constructor(file: string, text: string, meta?: Map) { this.file = file; - this.text = content; + this.text = text; this.meta = meta || new Map(); } - public get lineStarts(): number[] { - return this._lineStarts || (this._lineStarts = ts.computeLineStarts(this.text)); + public get lineStarts(): core.LineStarts { + return this._lineStarts || (this._lineStarts = core.computeLineStarts(this.text)); } } @@ -39,10 +42,6 @@ namespace documents { nameIndex?: number; } - const mappingRegExp = /([A-Za-z0-9+/]+),?|(;)|./g; - const sourceMappingURLRegExp = /^\/\/[#@]\s*sourceMappingURL\s*=\s*(.*?)\s*$/mig; - const dataURLRegExp = /^data:application\/json;base64,([a-z0-9+/=]+)$/i; - export class SourceMap { public readonly raw: RawSourceMap; public readonly mapFile: string | undefined; @@ -54,6 +53,11 @@ namespace documents { public readonly mappings: ReadonlyArray = []; public readonly names: ReadonlyArray | undefined; + private static readonly _mappingRegExp = /([A-Za-z0-9+/]+),?|(;)|./g; + private static readonly _sourceMappingURLRegExp = /^\/\/[#@]\s*sourceMappingURL\s*=\s*(.*?)\s*$/mig; + private static readonly _dataURLRegExp = /^data:application\/json;base64,([a-z0-9+/=]+)$/i; + private static readonly _base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + private _emittedLineMappings: Mapping[][] = []; private _sourceLineMappings: Mapping[][][] = []; @@ -76,9 +80,9 @@ namespace documents { let sourceColumn = 0; let nameIndex = 0; let match: RegExpExecArray | null; - while (match = mappingRegExp.exec(this.raw.mappings)) { + while (match = SourceMap._mappingRegExp.exec(this.raw.mappings)) { if (match[1]) { - const segment = decodeVLQ(match[1]); + const segment = SourceMap._decodeVLQ(match[1]); if (segment.length !== 1 && segment.length !== 4 && segment.length !== 5) { throw new Error("Invalid VLQ"); } @@ -120,14 +124,14 @@ namespace documents { public static getUrl(text: string) { let match: RegExpExecArray | null; let lastMatch: RegExpExecArray | undefined; - while (match = sourceMappingURLRegExp.exec(text)) { + while (match = SourceMap._sourceMappingURLRegExp.exec(text)) { lastMatch = match; } return lastMatch ? lastMatch[1] : undefined; } public static fromUrl(url: string) { - const match = dataURLRegExp.exec(url); + const match = SourceMap._dataURLRegExp.exec(url); return match ? new SourceMap(/*mapFile*/ undefined, new Buffer(match[1], "base64").toString("utf8")) : undefined; } @@ -144,26 +148,24 @@ namespace documents { const mappingsForSource = this._sourceLineMappings[sourceIndex]; return mappingsForSource && mappingsForSource[sourceLine]; } - } - const base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - - export function decodeVLQ(text: string) { - const vlq: number[] = []; - let shift = 0; - let value = 0; - for (let i = 0; i < text.length; i++) { - const currentByte = base64Chars.indexOf(text.charAt(i)); - value += (currentByte & 31) << shift; - if ((currentByte & 32) === 0) { - vlq.push(value & 1 ? -(value >>> 1) : value >>> 1); - shift = 0; - value = 0; - } - else { - shift += 5; + private static _decodeVLQ(text: string): number[] { + const vlq: number[] = []; + let shift = 0; + let value = 0; + for (let i = 0; i < text.length; i++) { + const currentByte = SourceMap._base64Chars.indexOf(text.charAt(i)); + value += (currentByte & 31) << shift; + if ((currentByte & 32) === 0) { + vlq.push(value & 1 ? -(value >>> 1) : value >>> 1); + shift = 0; + value = 0; + } + else { + shift += 5; + } } + return vlq; } - return vlq; } } \ No newline at end of file diff --git a/src/harness/events.ts b/src/harness/events.ts index 684995d4250..f882f461729 100644 --- a/src/harness/events.ts +++ b/src/harness/events.ts @@ -1,4 +1,8 @@ /// + +// NOTE: The contents of this file are all exported from the namespace 'events'. This is to +// support the eventual conversion of harness into a modular system. + namespace events { const _events = require("events"); diff --git a/src/harness/harness.ts b/src/harness/harness.ts index 2ab0b8c7a35..1d17c784783 100644 --- a/src/harness/harness.ts +++ b/src/harness/harness.ts @@ -1222,8 +1222,8 @@ namespace Harness { const fileOutputs = compilation.outputs ? compilation.outputs.map(output => ({ fileName: output.file, - code: utils.removeByteOrderMark(output.text), - writeByteOrderMark: utils.getByteOrderMarkLength(output.text) > 0 + code: core.removeByteOrderMark(output.text), + writeByteOrderMark: core.getByteOrderMarkLength(output.text) > 0 })) : []; const traceResults = compilation.traces && compilation.traces.slice(); @@ -1856,13 +1856,15 @@ namespace Harness { return opts; } - /** Given a test file containing // @FileName directives, return an array of named units of code to be added to an existing compiler instance */ - export function makeUnitsFromTest(code: string, fileName: string, rootDir?: string): { + export interface TestCaseContent { settings: CompilerSettings; testUnitData: TestUnitData[]; tsConfig: ts.ParsedCommandLine; tsConfigFileUnitData: TestUnitData; - } { + } + + /** Given a test file containing // @FileName directives, return an array of named units of code to be added to an existing compiler instance */ + export function makeUnitsFromTest(code: string, fileName: string, rootDir?: string): TestCaseContent { const settings = extractCompilerSettings(code); // List of all the subfiles we've parsed out diff --git a/src/harness/tsconfig.json b/src/harness/tsconfig.json index 26f60116009..8e853bbf3b9 100644 --- a/src/harness/tsconfig.json +++ b/src/harness/tsconfig.json @@ -83,9 +83,9 @@ "harness.ts", + "core.ts", "utils.ts", "events.ts", - "collections.ts", "documents.ts", "vpath.ts", "vfs.ts", diff --git a/src/harness/unittests/programMissingFiles.ts b/src/harness/unittests/programMissingFiles.ts index 6e0602998f0..8c042f2afbc 100644 --- a/src/harness/unittests/programMissingFiles.ts +++ b/src/harness/unittests/programMissingFiles.ts @@ -42,7 +42,7 @@ namespace ts { const testCompilerHost = new compiler.CompilerHost( vfs.VirtualFileSystem.createFromTestFiles( - { useCaseSensitiveFileNames: false, currentDirectory: "d:\\pretend\\" }, + { useCaseSensitiveFileNames: false, currentDirectory: "d:\\pretend\\" }, [emptyFile, referenceFile]), { newLine: NewLineKind.LineFeed }); diff --git a/src/harness/utils.ts b/src/harness/utils.ts index 518a1be993f..4d0d43d7fba 100644 --- a/src/harness/utils.ts +++ b/src/harness/utils.ts @@ -1,26 +1,9 @@ +/// + +// NOTE: The contents of this file are all exported from the namespace 'core'. This is to +// support the eventual conversion of harness into a modular system. + namespace utils { - export function identity(v: T) { return v; } - - export function getByteOrderMarkLength(text: string) { - if (text.length >= 2) { - const ch0 = text.charCodeAt(0); - const ch1 = text.charCodeAt(1); - if ((ch0 === 0xff && ch1 === 0xfe) || - (ch0 === 0xfe && ch1 === 0xff)) { - return 2; - } - if (text.length >= 3 && ch0 === 0xef && ch1 === 0xbb && text.charCodeAt(2) === 0xbf) { - return 3; - } - } - return 0; - } - - export function removeByteOrderMark(text: string) { - const length = getByteOrderMarkLength(text); - return length ? text.slice(length) : text; - } - const leadingCommentRegExp = /^(\s*\/\*[^]*?\*\/\s*|\s*\/\/[^\r\n\u2028\u2029]*[\r\n\u2028\u2029]*)+/; const trailingCommentRegExp = /(\s*\/\*[^]*?\*\/\s*|\s*\/\/[^\r\n\u2028\u2029]*[\r\n\u2028\u2029]*)+$/; const leadingAndTrailingCommentRegExp = /^(\s*\/\*[^]*?\*\/\s*|\s*\/\/[^\r\n\u2028\u2029]*[\r\n\u2028\u2029]*)+|(\s*\/\*[^]*?\*\/\s*|\s*\/\/[^\r\n\u2028\u2029]*[\r\n\u2028\u2029]*)+$/g; diff --git a/src/harness/vfs.ts b/src/harness/vfs.ts index 643b805a926..da4f4ae190c 100644 --- a/src/harness/vfs.ts +++ b/src/harness/vfs.ts @@ -1,13 +1,13 @@ +/// /// -/// +/// /// /// -/// -namespace vfs { - import KeyedCollection = collections.KeyedCollection; - import Metadata = collections.Metadata; - import EventEmitter = events.EventEmitter; +// NOTE: The contents of this file are all exported from the namespace 'vfs'. This is to +// support the eventual conversion of harness into a modular system. + +namespace vfs { export interface PathMappings { [path: string]: string; } @@ -89,7 +89,7 @@ namespace vfs { recursive: boolean; } - export abstract class VirtualFileSystemObject extends EventEmitter { + export abstract class VirtualFileSystemObject extends events.EventEmitter { private _readonly = false; /** @@ -121,11 +121,11 @@ namespace vfs { private _currentDirectory: string; private _currentDirectoryStack: string[] | undefined; private _shadowRoot: VirtualFileSystem | undefined; - private _watchedFiles: KeyedCollection | undefined; - private _watchedDirectories: KeyedCollection | undefined; + private _watchedFiles: core.KeyedCollection | undefined; + private _watchedDirectories: core.KeyedCollection | undefined; private _stringComparer: ts.Comparer | undefined; private _pathComparer: ts.Comparer | undefined; - private _metadata: Metadata; + private _metadata: core.Metadata; private _onRootFileSystemChange: (path: string, change: FileSystemChange) => void; constructor(currentDirectory: string, useCaseSensitiveFileNames: boolean) { @@ -137,8 +137,8 @@ namespace vfs { public get stringComparer() { return this._stringComparer || (this._stringComparer = this.useCaseSensitiveFileNames - ? collections.compareStringsCaseSensitive - : collections.compareStringsCaseInsensitive); + ? core.compareStringsCaseSensitive + : core.compareStringsCaseInsensitive); } public get pathComparer() { @@ -157,8 +157,8 @@ namespace vfs { /** * Gets metadata about this file system. */ - public get metadata(): Metadata { - return this._metadata || (this._metadata = new Metadata(this.shadowRoot ? this.shadowRoot.metadata : undefined)); + public get metadata(): core.Metadata { + return this._metadata || (this._metadata = new core.Metadata(this.shadowRoot ? this.shadowRoot.metadata : undefined)); } /** @@ -447,7 +447,7 @@ namespace vfs { public watchFile(path: string, watcher: (path: string, change: FileSystemChange) => void): ts.FileWatcher { if (!this._watchedFiles) { const pathComparer = this.useCaseSensitiveFileNames ? vpath.compareCaseSensitive : vpath.compareCaseInsensitive; - this._watchedFiles = new KeyedCollection(pathComparer); + this._watchedFiles = new core.KeyedCollection(pathComparer); } path = vpath.resolve(this.currentDirectory, path); @@ -476,7 +476,7 @@ namespace vfs { public watchDirectory(path: string, watcher: (path: string) => void, recursive?: boolean) { if (!this._watchedDirectories) { const pathComparer = this.useCaseSensitiveFileNames ? vpath.compareCaseSensitive : vpath.compareCaseInsensitive; - this._watchedDirectories = new KeyedCollection(pathComparer); + this._watchedDirectories = new core.KeyedCollection(pathComparer); } path = vpath.resolve(this.currentDirectory, path); @@ -590,7 +590,7 @@ namespace vfs { export abstract class VirtualFileSystemEntry extends VirtualFileSystemObject { private _path: string; - private _metadata: Metadata; + private _metadata: core.Metadata; /** * Gets the name of this entry. @@ -623,8 +623,8 @@ namespace vfs { /** * Gets metadata about this entry. */ - public get metadata(): Metadata { - return this._metadata || (this._metadata = new Metadata(this.shadowRoot ? this.shadowRoot.metadata : undefined)); + public get metadata(): core.Metadata { + return this._metadata || (this._metadata = new core.Metadata(this.shadowRoot ? this.shadowRoot.metadata : undefined)); } /** @@ -724,7 +724,7 @@ namespace vfs { export class VirtualDirectory extends VirtualFileSystemEntry { protected _shadowRoot: VirtualDirectory | undefined; private _parent: VirtualDirectory; - private _entries: KeyedCollection | undefined; + private _entries: core.KeyedCollection | undefined; private _resolver: FileSystemResolver | undefined; private _onChildFileSystemChange: (path: string, change: FileSystemChange) => void; @@ -990,7 +990,7 @@ namespace vfs { protected getOwnEntries() { if (!this._entries) { - const entries = new KeyedCollection(this.fileSystem.stringComparer); + const entries = new core.KeyedCollection(this.fileSystem.stringComparer); const resolver = this._resolver; const shadowRoot = this._shadowRoot; if (resolver) { @@ -1136,8 +1136,8 @@ namespace vfs { export class VirtualDirectorySymlink extends VirtualDirectory { private _targetPath: string; private _target: VirtualDirectory | undefined; - private _views: KeyedCollection | undefined; - private _allViews: KeyedCollection | undefined; + private _views: core.KeyedCollection | undefined; + private _allViews: core.KeyedCollection | undefined; private _onTargetParentChildRemoved: (entry: VirtualEntry) => void; private _onTargetChildRemoved: (entry: VirtualEntry) => void; private _onTargetChildAdded: (entry: VirtualEntry) => void; @@ -1145,7 +1145,7 @@ namespace vfs { constructor(parent: VirtualDirectory, name: string, target: string) { super(parent, name); - this._views = new KeyedCollection(this.fileSystem.stringComparer); + this._views = new core.KeyedCollection(this.fileSystem.stringComparer); this._targetPath = target; this._onTargetParentChildRemoved = entry => this.onTargetParentChildRemoved(entry); this._onTargetChildAdded = entry => this.onTargetChildAdded(entry); @@ -1225,9 +1225,9 @@ namespace vfs { return target && target.removeFile(name) || false; } - protected getOwnEntries(): KeyedCollection { + protected getOwnEntries(): core.KeyedCollection { if (!this._allViews) { - this._allViews = new KeyedCollection(this.fileSystem.stringComparer); + this._allViews = new core.KeyedCollection(this.fileSystem.stringComparer); const target = this.target; if (target) { for (const entry of target.getEntries()) { diff --git a/src/harness/vpath.ts b/src/harness/vpath.ts index 7a7669a4e10..9ad081ac1f5 100644 --- a/src/harness/vpath.ts +++ b/src/harness/vpath.ts @@ -1,11 +1,13 @@ /// -namespace vpath { - // NOTE: Some of the functions here duplicate functionality from compiler/core.ts. They have been added - // to reduce the number of direct dependencies on compiler and services to eventually break away - // from depending directly on the compiler to speed up compilation time. - import compareValues = collections.compareNumbers; - import compareStrings = collections.compareStrings; +// NOTE: The contents of this file are all exported from the namespace 'vpath'. This is to +// support the eventual conversion of harness into a modular system. + +// NOTE: Some of the functions here duplicate functionality from compiler/core.ts. They have been +// added to reduce the number of direct dependencies on compiler and services to eventually +// break away from depending directly on the compiler to speed up compilation time. + +namespace vpath { /** * Virtual path separator. @@ -101,10 +103,7 @@ namespace vpath { return normalize(combine(path, ...paths)); } - /** - * Gets a relative path that can be used to traverse between `from` and `to`. - */ - export function relative(from: string, to: string, ignoreCase: boolean) { + function relativeWorker(from: string, to: string, stringEqualityComparer: core.EqualityComparer) { if (!isAbsolute(from)) throw new Error("Path not absolute"); if (!isAbsolute(to)) throw new Error("Path not absolute"); @@ -113,7 +112,7 @@ namespace vpath { let start: number; for (start = 0; start < fromComponents.length && start < toComponents.length; start++) { - if (compareStrings(fromComponents[start], toComponents[start], ignoreCase)) { + if (stringEqualityComparer(fromComponents[start], toComponents[start])) { break; } } @@ -130,10 +129,22 @@ namespace vpath { return format(["", ...components]); } + function relativeCaseSensitive(from: string, to: string) { + return relativeWorker(from, to, core.equateStringsCaseSensitive); + } + + function relativeCaseInsensitive(from: string, to: string) { + return relativeWorker(from, to, core.equateStringsCaseInsensitive); + } + /** - * Compare two paths. + * Gets a relative path that can be used to traverse between `from` and `to`. */ - export function compare(a: string, b: string, ignoreCase: boolean) { + export function relative(from: string, to: string, ignoreCase: boolean) { + return ignoreCase ? relativeCaseInsensitive(from, to) : relativeCaseSensitive(from, to); + } + + function compareWorker(a: string, b: string, stringComparer: core.Comparer) { if (a === b) return 0; a = removeTrailingSeparator(a); b = removeTrailingSeparator(b); @@ -142,21 +153,32 @@ namespace vpath { const bComponents = reduce(parse(b)); const len = Math.min(aComponents.length, bComponents.length); for (let i = 0; i < len; i++) { - const result = compareStrings(aComponents[i], bComponents[i], ignoreCase); + const result = stringComparer(aComponents[i], bComponents[i]); if (result !== 0) return result; } - return compareValues(aComponents.length, bComponents.length); + return core.compareNumbers(aComponents.length, bComponents.length); } /** * Performs a case-sensitive comparison of two paths. */ - export function compareCaseSensitive(a: string, b: string) { return compare(a, b, /*ignoreCase*/ false); } + export function compareCaseSensitive(a: string, b: string) { + return compareWorker(a, b, core.compareStringsCaseSensitive); + } /** * Performs a case-insensitive comparison of two paths. */ - export function compareCaseInsensitive(a: string, b: string) { return compare(a, b, /*ignoreCase*/ true); } + export function compareCaseInsensitive(a: string, b: string) { + return compareWorker(a, b, core.compareStringsCaseInsensitive); + } + + /** + * Compare two paths. + */ + export function compare(a: string, b: string, ignoreCase: boolean) { + return ignoreCase ? compareCaseInsensitive(a, b) : compareCaseSensitive(a, b); + } /** * Determines whether two strings are equal. @@ -174,24 +196,35 @@ namespace vpath { return ignoreCase && a.toUpperCase() === b.toUpperCase(); } - /** - * Determines whether the path `descendant` is beneath the path `ancestor`. - */ - export function beneath(ancestor: string, descendant: string, ignoreCase: boolean) { + function beneathWorker(ancestor: string, descendant: string, stringEqualityComparer: ts.EqualityComparer) { if (!isAbsolute(ancestor)) throw new Error("Path not absolute"); if (!isAbsolute(descendant)) throw new Error("Path not absolute"); const ancestorComponents = reduce(parse(ancestor)); const descendantComponents = reduce(parse(descendant)); if (descendantComponents.length < ancestorComponents.length) return false; - const equalityComparer = ignoreCase ? collections.equateStringsCaseInsensitive : collections.equateStringsCaseSensitive; for (let i = 0; i < ancestorComponents.length; i++) { - if (!equalityComparer(ancestorComponents[i], descendantComponents[i])) { + if (!stringEqualityComparer(ancestorComponents[i], descendantComponents[i])) { return false; } } return true; } + function beneathCaseSensitive(ancestor: string, descendant: string) { + return beneathWorker(ancestor, descendant, core.equateStringsCaseSensitive); + } + + function beneathCaseInsensitive(ancestor: string, descendant: string) { + return beneathWorker(ancestor, descendant, core.equateStringsCaseInsensitive); + } + + /** + * Determines whether the path `descendant` is beneath the path `ancestor`. + */ + export function beneath(ancestor: string, descendant: string, ignoreCase: boolean) { + return ignoreCase ? beneathCaseInsensitive(ancestor, descendant) : beneathCaseSensitive(ancestor, descendant); + } + /** * Parse a path into a root component and zero or more path segments. * If the path is relative, the root component is `""`. @@ -239,6 +272,21 @@ namespace vpath { const extRegExp = /\.\w+$/; + function extnameWorker(path: string, extensions: string | string[], stringEqualityComparer: core.EqualityComparer) { + const manyExtensions = Array.isArray(extensions) ? extensions : undefined; + const singleExtension = Array.isArray(extensions) ? undefined : extensions; + const length = manyExtensions ? manyExtensions.length : 1; + for (let i = 0; i < length; i++) { + let extension = manyExtensions ? manyExtensions[i] : singleExtension; + if (!extension.startsWith(".")) extension = "." + extension; + if (path.length >= extension.length && + stringEqualityComparer(path.slice(path.length - extension.length), extension)) { + return extension; + } + } + return ""; + } + /** * Gets the file extension for a path. */ @@ -249,19 +297,7 @@ namespace vpath { export function extname(path: string, extensions: string | string[], ignoreCase: boolean): string; export function extname(path: string, extensions?: string | string[], ignoreCase?: boolean) { if (extensions) { - const manyExtensions = Array.isArray(extensions) ? extensions : undefined; - const singleExtension = Array.isArray(extensions) ? undefined : extensions; - const length = manyExtensions ? manyExtensions.length : 1; - const comparer = ignoreCase ? collections.compareStringsCaseInsensitive : collections.compareStringsCaseSensitive; - for (let i = 0; i < length; i++) { - let extension = manyExtensions ? manyExtensions[i] : singleExtension; - if (!extension.startsWith(".")) extension = "." + extension; - if (path.length >= extension.length && - comparer(path.slice(path.length - extension.length), extension) === 0) { - return extension; - } - } - return ""; + return extnameWorker(path, extensions, ignoreCase ? core.equateStringsCaseInsensitive : core.equateStringsCaseSensitive); } const match = extRegExp.exec(path);