diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index 31928f73ce217..2f6f528677583 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -1,30 +1,34 @@
-// Available variables which can be used inside of strings.
-// ${workspaceRoot}: the root folder of the team
-// ${file}: the current opened file
-// ${fileBasename}: the current opened file's basename
-// ${fileDirname}: the current opened file's dirname
-// ${fileExtname}: the current opened file's extension
-// ${cwd}: the current working directory of the spawned process
{
- "version": "0.1.0",
- "command": "gulp",
- "isShellCommand": true,
- "showOutput": "silent",
+ // See https://go.microsoft.com/fwlink/?LinkId=733558
+ // for the documentation about the tasks.json format
+ "version": "2.0.0",
"tasks": [
{
- "taskName": "local",
- "isBuildCommand": true,
- "showOutput": "silent",
- "problemMatcher": [
- "$tsc"
- ]
+ "type": "shell",
+ "identifier": "local",
+ "label": "gulp: local",
+ "command": "gulp",
+ "args": ["local"],
+ "group": { "kind": "build", "isDefault": true },
+ "problemMatcher": ["$gulp-tsc"]
},
{
- "taskName": "tests",
- "showOutput": "silent",
- "problemMatcher": [
- "$tsc"
- ]
+ "type": "shell",
+ "identifier": "tsc",
+ "label": "gulp: tsc",
+ "command": "gulp",
+ "args": ["tsc"],
+ "group": "build",
+ "problemMatcher": ["$gulp-tsc"]
+ },
+ {
+ "type": "shell",
+ "identifier": "tests",
+ "label": "gulp: tests",
+ "command": "gulp",
+ "args": ["tests"],
+ "group": "build",
+ "problemMatcher": ["$gulp-tsc"]
}
]
}
\ No newline at end of file
diff --git a/Gulpfile.js b/Gulpfile.js
index 3de5e5150acc9..9a082bb447932 100644
--- a/Gulpfile.js
+++ b/Gulpfile.js
@@ -761,7 +761,7 @@ const nodeServerOutFile = "tests/webTestServer.js";
const nodeServerInFile = "tests/webTestServer.ts";
gulp.task(nodeServerOutFile, /*help*/ false, [servicesFile], () => {
/** @type {tsc.Settings} */
- const settings = getCompilerSettings({ module: "commonjs" }, /*useBuiltCompiler*/ true);
+ const settings = getCompilerSettings({ module: "commonjs", target: "es2015" }, /*useBuiltCompiler*/ true);
return gulp.src(nodeServerInFile)
.pipe(newer(nodeServerOutFile))
.pipe(sourcemaps.init())
diff --git a/Jakefile.js b/Jakefile.js
index 3a50a6ee11377..aed3f99d5e8c0 100644
--- a/Jakefile.js
+++ b/Jakefile.js
@@ -1,5 +1,6 @@
// This file contains the build logic for the public repo
// @ts-check
+///
var fs = require("fs");
var os = require("os");
@@ -171,35 +172,34 @@ var compilerFilename = "tsc.js";
var LKGCompiler = path.join(LKGDirectory, compilerFilename);
var builtLocalCompiler = path.join(builtLocalDirectory, compilerFilename);
-/* Compiles a file from a list of sources
- * @param outFile: the target file name
- * @param sources: an array of the names of the source files
- * @param prereqs: prerequisite tasks to compiling the file
- * @param prefixes: a list of files to prepend to the target file
- * @param useBuiltCompiler: true to use the built compiler, false to use the LKG
- * @parap {Object} opts - property bag containing auxiliary options
- * @param {boolean} opts.noOutFile: true to compile without using --out
- * @param {boolean} opts.generateDeclarations: true to compile using --declaration
- * @param {string} opts.outDir: value for '--outDir' command line option
- * @param {boolean} opts.keepComments: false to compile using --removeComments
- * @param {boolean} opts.preserveConstEnums: true if compiler should keep const enums in code
- * @param {boolean} opts.noResolve: true if compiler should not include non-rooted files in compilation
- * @param {boolean} opts.stripInternal: true if compiler should remove declarations marked as @internal
- * @param {boolean} opts.inlineSourceMap: true if compiler should inline sourceMap
- * @param {Array} opts.types: array of types to include in compilation
- * @param callback: a function to execute after the compilation process ends
- */
+/**
+ * Compiles a file from a list of sources
+ * @param {string} outFile the target file name
+ * @param {string[]} sources an array of the names of the source files
+ * @param {string[]} prereqs prerequisite tasks to compiling the file
+ * @param {string[]} prefixes a list of files to prepend to the target file
+ * @param {boolean} useBuiltCompiler true to use the built compiler, false to use the LKG
+ * @param {object} [opts] property bag containing auxiliary options
+ * @param {boolean} [opts.noOutFile] true to compile without using --out
+ * @param {boolean} [opts.generateDeclarations] true to compile using --declaration
+ * @param {string} [opts.outDir] value for '--outDir' command line option
+ * @param {boolean} [opts.keepComments] false to compile using --removeComments
+ * @param {boolean} [opts.preserveConstEnums] true if compiler should keep const enums in code
+ * @param {boolean} [opts.noResolve] true if compiler should not include non-rooted files in compilation
+ * @param {boolean} [opts.stripInternal] true if compiler should remove declarations marked as internal
+ * @param {boolean} [opts.inlineSourceMap] true if compiler should inline sourceMap
+ * @param {string[]} [opts.types] array of types to include in compilation
+ * @param {string} [opts.lib] explicit libs to include.
+ * @param {function(): void} [callback] a function to execute after the compilation process ends
+ */
function compileFile(outFile, sources, prereqs, prefixes, useBuiltCompiler, opts, callback) {
file(outFile, prereqs, function() {
- if (process.env.USE_TRANSFORMS === "false") {
- useBuiltCompiler = false;
- }
var startCompileTime = mark();
opts = opts || {};
var compilerPath = useBuiltCompiler ? builtLocalCompiler : LKGCompiler;
- var options = "--noImplicitAny --noImplicitThis --alwaysStrict --noEmitOnError --types ";
+ var options = "--noImplicitAny --noImplicitThis --alwaysStrict --noEmitOnError";
if (opts.types) {
- options += opts.types.join(",");
+ options += " --types " + opts.types.join(",");
}
options += " --pretty";
// Keep comments when specifically requested
@@ -236,7 +236,7 @@ function compileFile(outFile, sources, prereqs, prefixes, useBuiltCompiler, opts
options += " --inlineSourceMap --inlineSources";
}
else {
- options += " -sourcemap";
+ options += " --sourcemap";
}
}
options += " --newLine LF";
@@ -244,7 +244,7 @@ function compileFile(outFile, sources, prereqs, prefixes, useBuiltCompiler, opts
if (opts.stripInternal) {
options += " --stripInternal";
}
- options += " --target es5";
+ options += " --target es5";
if (opts.lib) {
options += " --lib " + opts.lib;
}
@@ -362,7 +362,6 @@ var buildProtocolTs = path.join(scriptsDirectory, "buildProtocol.ts");
var buildProtocolJs = path.join(scriptsDirectory, "buildProtocol.js");
var buildProtocolDts = path.join(builtLocalDirectory, "protocol.d.ts");
var typescriptServicesDts = path.join(builtLocalDirectory, "typescriptServices.d.ts");
-var typesMapJson = path.join(builtLocalDirectory, "typesMap.json");
file(buildProtocolTs);
@@ -540,7 +539,7 @@ var serverFile = path.join(builtLocalDirectory, "tsserver.js");
compileFile(serverFile, serverSources, [builtLocalDirectory, copyright, cancellationTokenFile, typingsInstallerFile, watchGuardFile].concat(serverSources).concat(servicesSources), /*prefixes*/ [copyright], /*useBuiltCompiler*/ true, { types: ["node"], preserveConstEnums: true, lib: "es6" });
var tsserverLibraryFile = path.join(builtLocalDirectory, "tsserverlibrary.js");
var tsserverLibraryDefinitionFile = path.join(builtLocalDirectory, "tsserverlibrary.d.ts");
-file(typesMapOutputPath, function() {
+file(typesMapOutputPath, /** @type {*} */(function() {
var content = fs.readFileSync(path.join(serverDirectory, 'typesMap.json'));
// Validate that it's valid JSON
try {
@@ -549,7 +548,7 @@ file(typesMapOutputPath, function() {
console.log("Parse error in typesMap.json: " + e);
}
fs.writeFileSync(typesMapOutputPath, content);
-});
+}));
compileFile(
tsserverLibraryFile,
languageServiceLibrarySources,
@@ -693,7 +692,7 @@ desc("Builds the test infrastructure using the built compiler");
task("tests", ["local", run].concat(libraryTargets));
function exec(cmd, completeHandler, errorHandler) {
- var ex = jake.createExec([cmd], { windowsVerbatimArguments: true, interactive: true });
+ var ex = jake.createExec([cmd], /** @type {jake.ExecOptions} */({ windowsVerbatimArguments: true, interactive: true }));
// Add listeners for output and error
ex.addListener("stdout", function (output) {
process.stdout.write(output);
@@ -1170,7 +1169,7 @@ task("lint", ["build-rules"], () => {
function lint(project, cb) {
const cmd = `node node_modules/tslint/bin/tslint --project ${project} --formatters-dir ./built/local/tslint/formatters --format autolinkableStylish`;
console.log("Linting: " + cmd);
- jake.exec([cmd], { interactive: true, windowsVerbatimArguments: true }, cb);
+ jake.exec([cmd], cb, /** @type {jake.ExecOptions} */({ interactive: true, windowsVerbatimArguments: true }));
}
lint("scripts/tslint/tsconfig.json", () => lint("src/tsconfig-base.json", () => {
if (fold.isTravis()) console.log(fold.end("lint"));
diff --git a/package.json b/package.json
index 3e53a3c67b89e..ef4650c49b0e9 100644
--- a/package.json
+++ b/package.json
@@ -39,6 +39,7 @@
"@types/gulp-help": "latest",
"@types/gulp-newer": "latest",
"@types/gulp-sourcemaps": "latest",
+ "@types/jake": "latest",
"@types/merge2": "latest",
"@types/minimatch": "latest",
"@types/minimist": "latest",
@@ -47,6 +48,7 @@
"@types/node": "8.5.5",
"@types/q": "latest",
"@types/run-sequence": "latest",
+ "@types/source-map-support": "latest",
"@types/through2": "latest",
"@types/travis-fold": "latest",
"@types/xml2js": "^0.4.0",
diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts
index c01ad65dc8115..fc86c63a5c649 100644
--- a/src/compiler/commandLineParser.ts
+++ b/src/compiler/commandLineParser.ts
@@ -1820,7 +1820,7 @@ namespace ts {
}
function getDefaultCompilerOptions(configFileName?: string) {
- const options: CompilerOptions = getBaseFileName(configFileName) === "jsconfig.json"
+ const options: CompilerOptions = configFileName && getBaseFileName(configFileName) === "jsconfig.json"
? { allowJs: true, maxNodeModuleJsDepth: 2, allowSyntheticDefaultImports: true, skipLibCheck: true, noEmit: true }
: {};
return options;
@@ -1835,7 +1835,7 @@ namespace ts {
}
function getDefaultTypeAcquisition(configFileName?: string): TypeAcquisition {
- return { enable: getBaseFileName(configFileName) === "jsconfig.json", include: [], exclude: [] };
+ return { enable: configFileName && getBaseFileName(configFileName) === "jsconfig.json", include: [], exclude: [] };
}
function convertTypeAcquisitionFromJsonWorker(jsonOptions: any,
diff --git a/src/compiler/core.ts b/src/compiler/core.ts
index 800c26f6c01aa..72b45385c9370 100644
--- a/src/compiler/core.ts
+++ b/src/compiler/core.ts
@@ -1890,12 +1890,12 @@ namespace ts {
comparer(a[key], b[key]);
}
- function getDiagnosticFileName(diagnostic: Diagnostic): string {
- return diagnostic.file ? diagnostic.file.fileName : undefined;
+ function getDiagnosticFilePath(diagnostic: Diagnostic): string {
+ return diagnostic.file ? diagnostic.file.path : undefined;
}
export function compareDiagnostics(d1: Diagnostic, d2: Diagnostic): Comparison {
- return compareStringsCaseSensitive(getDiagnosticFileName(d1), getDiagnosticFileName(d2)) ||
+ return compareStringsCaseSensitive(getDiagnosticFilePath(d1), getDiagnosticFilePath(d2)) ||
compareValues(d1.start, d2.start) ||
compareValues(d1.length, d2.length) ||
compareValues(d1.code, d2.code) ||
@@ -1932,110 +1932,6 @@ namespace ts {
return text1 ? Comparison.GreaterThan : Comparison.LessThan;
}
- export function normalizeSlashes(path: string): string {
- return path.replace(/\\/g, "/");
- }
-
- /**
- * Returns length of path root (i.e. length of "/", "x:/", "//server/share/, file:///user/files")
- */
- export function getRootLength(path: string): number {
- if (path.charCodeAt(0) === CharacterCodes.slash) {
- if (path.charCodeAt(1) !== CharacterCodes.slash) return 1;
- const p1 = path.indexOf("/", 2);
- if (p1 < 0) return 2;
- const p2 = path.indexOf("/", p1 + 1);
- if (p2 < 0) return p1 + 1;
- return p2 + 1;
- }
- if (path.charCodeAt(1) === CharacterCodes.colon) {
- if (path.charCodeAt(2) === CharacterCodes.slash || path.charCodeAt(2) === CharacterCodes.backslash) return 3;
- }
- // Per RFC 1738 'file' URI schema has the shape file:///
- // if is omitted then it is assumed that host value is 'localhost',
- // however slash after the omitted is not removed.
- // file:///folder1/file1 - this is a correct URI
- // file://folder2/file2 - this is an incorrect URI
- if (path.lastIndexOf("file:///", 0) === 0) {
- return "file:///".length;
- }
- const idx = path.indexOf("://");
- if (idx !== -1) {
- return idx + "://".length;
- }
- return 0;
- }
-
- /**
- * Internally, we represent paths as strings with '/' as the directory separator.
- * When we make system calls (eg: LanguageServiceHost.getDirectory()),
- * we expect the host to correctly handle paths in our specified format.
- */
- export const directorySeparator = "/";
- const directorySeparatorCharCode = CharacterCodes.slash;
- function getNormalizedParts(normalizedSlashedPath: string, rootLength: number): string[] {
- const parts = normalizedSlashedPath.substr(rootLength).split(directorySeparator);
- const normalized: string[] = [];
- for (const part of parts) {
- if (part !== ".") {
- if (part === ".." && normalized.length > 0 && lastOrUndefined(normalized) !== "..") {
- normalized.pop();
- }
- else {
- // A part may be an empty string (which is 'falsy') if the path had consecutive slashes,
- // e.g. "path//file.ts". Drop these before re-joining the parts.
- if (part) {
- normalized.push(part);
- }
- }
- }
- }
-
- return normalized;
- }
-
- export function normalizePath(path: string): string {
- return normalizePathAndParts(path).path;
- }
-
- export function normalizePathAndParts(path: string): { path: string, parts: string[] } {
- path = normalizeSlashes(path);
- const rootLength = getRootLength(path);
- const root = path.substr(0, rootLength);
- const parts = getNormalizedParts(path, rootLength);
- if (parts.length) {
- const joinedParts = root + parts.join(directorySeparator);
- return { path: pathEndsWithDirectorySeparator(path) ? joinedParts + directorySeparator : joinedParts, parts };
- }
- else {
- return { path: root, parts };
- }
- }
-
- /** A path ending with '/' refers to a directory only, never a file. */
- export function pathEndsWithDirectorySeparator(path: string): boolean {
- return path.charCodeAt(path.length - 1) === directorySeparatorCharCode;
- }
-
- /**
- * Returns the path except for its basename. Eg:
- *
- * /path/to/file.ext -> /path/to
- */
- export function getDirectoryPath(path: Path): Path;
- export function getDirectoryPath(path: string): string;
- export function getDirectoryPath(path: string): string {
- return path.substr(0, Math.max(getRootLength(path), path.lastIndexOf(directorySeparator)));
- }
-
- export function isUrl(path: string) {
- return path && !isRootedDiskPath(path) && stringContains(path, "://");
- }
-
- export function pathIsRelative(path: string): boolean {
- return /^\.\.?($|[\\/])/.test(path);
- }
-
export function getEmitScriptTarget(compilerOptions: CompilerOptions) {
return compilerOptions.target || ScriptTarget.ES3;
}
@@ -2089,8 +1985,208 @@ namespace ts {
return true;
}
+ //
+ // Paths
+ //
+
+
+ /**
+ * Internally, we represent paths as strings with '/' as the directory separator.
+ * When we make system calls (eg: LanguageServiceHost.getDirectory()),
+ * we expect the host to correctly handle paths in our specified format.
+ */
+ export const directorySeparator = "/";
+ const altDirectorySeparator = "\\";
+ const urlSchemeSeparator = "://";
+
+ const backslashRegExp = /\\/g;
+
+ /**
+ * Normalize path separators.
+ */
+ export function normalizeSlashes(path: string): string {
+ return path.replace(backslashRegExp, directorySeparator);
+ }
+
+ function isVolumeCharacter(charCode: number) {
+ return (charCode >= CharacterCodes.a && charCode <= CharacterCodes.z) ||
+ (charCode >= CharacterCodes.A && charCode <= CharacterCodes.Z);
+ }
+
+ function getFileUrlVolumeSeparatorEnd(url: string, start: number) {
+ const ch0 = url.charCodeAt(start);
+ if (ch0 === CharacterCodes.colon) return start + 1;
+ if (ch0 === CharacterCodes.percent && url.charCodeAt(start + 1) === CharacterCodes._3) {
+ const ch2 = url.charCodeAt(start + 2);
+ if (ch2 === CharacterCodes.a || ch2 === CharacterCodes.A) return start + 3;
+ }
+ return -1;
+ }
+
+ /**
+ * Returns length of the root part of a path or URL (i.e. length of "/", "x:/", "//server/share/, file:///user/files").
+ * If the root is part of a URL, the twos-complement of the root length is returned.
+ */
+ function getEncodedRootLength(path: string): number {
+ if (!path) return 0;
+ const ch0 = path.charCodeAt(0);
+
+ // POSIX or UNC
+ if (ch0 === CharacterCodes.slash || ch0 === CharacterCodes.backslash) {
+ if (path.charCodeAt(1) !== ch0) return 1; // POSIX: "/" (or non-normalized "\")
+
+ const p1 = path.indexOf(ch0 === CharacterCodes.slash ? directorySeparator : altDirectorySeparator, 2);
+ if (p1 < 0) return path.length; // UNC: "//server" or "\\server"
+
+ return p1 + 1; // UNC: "//server/" or "\\server\"
+ }
+
+ // DOS
+ if (isVolumeCharacter(ch0) && path.charCodeAt(1) === CharacterCodes.colon) {
+ const ch2 = path.charCodeAt(2);
+ if (ch2 === CharacterCodes.slash || ch2 === CharacterCodes.backslash) return 3; // DOS: "c:/" or "c:\"
+ if (path.length === 2) return 2; // DOS: "c:" (but not "c:d")
+ }
+
+ // URL
+ const schemeEnd = path.indexOf(urlSchemeSeparator);
+ if (schemeEnd !== -1) {
+ const authorityStart = schemeEnd + urlSchemeSeparator.length;
+ const authorityEnd = path.indexOf(directorySeparator, authorityStart);
+ if (authorityEnd !== -1) { // URL: "file:///", "file://server/", "file://server/path"
+ // For local "file" URLs, include the leading DOS volume (if present).
+ // Per https://www.ietf.org/rfc/rfc1738.txt, a host of "" or "localhost" is a
+ // special case interpreted as "the machine from which the URL is being interpreted".
+ const scheme = path.slice(0, schemeEnd);
+ const authority = path.slice(authorityStart, authorityEnd);
+ if (scheme === "file" && (authority === "" || authority === "localhost") &&
+ isVolumeCharacter(path.charCodeAt(authorityEnd + 1))) {
+ const volumeSeparatorEnd = getFileUrlVolumeSeparatorEnd(path, authorityEnd + 2);
+ if (volumeSeparatorEnd !== -1) {
+ if (path.charCodeAt(volumeSeparatorEnd) === CharacterCodes.slash) {
+ // URL: "file:///c:/", "file://localhost/c:/", "file:///c%3a/", "file://localhost/c%3a/"
+ return ~(volumeSeparatorEnd + 1);
+ }
+ if (volumeSeparatorEnd === path.length) {
+ // URL: "file:///c:", "file://localhost/c:", "file:///c$3a", "file://localhost/c%3a"
+ // but not "file:///c:d" or "file:///c%3ad"
+ return ~volumeSeparatorEnd;
+ }
+ }
+ }
+ return ~(authorityEnd + 1); // URL: "file://server/", "http://server/"
+ }
+ return ~path.length; // URL: "file://server", "http://server"
+ }
+
+ // relative
+ return 0;
+ }
+
+ /**
+ * Returns length of the root part of a path or URL (i.e. length of "/", "x:/", "//server/share/, file:///user/files").
+ *
+ * For example:
+ * ```ts
+ * getRootLength("a") === 0 // ""
+ * getRootLength("/") === 1 // "/"
+ * getRootLength("c:") === 2 // "c:"
+ * getRootLength("c:d") === 0 // ""
+ * getRootLength("c:/") === 3 // "c:/"
+ * getRootLength("c:\\") === 3 // "c:\\"
+ * getRootLength("//server") === 7 // "//server"
+ * getRootLength("//server/share") === 8 // "//server/"
+ * getRootLength("\\\\server") === 7 // "\\\\server"
+ * getRootLength("\\\\server\\share") === 8 // "\\\\server\\"
+ * getRootLength("file:///path") === 8 // "file:///"
+ * getRootLength("file:///c:") === 10 // "file:///c:"
+ * getRootLength("file:///c:d") === 8 // "file:///"
+ * getRootLength("file:///c:/path") === 11 // "file:///c:/"
+ * getRootLength("file://server") === 13 // "file://server"
+ * getRootLength("file://server/path") === 14 // "file://server/"
+ * getRootLength("http://server") === 13 // "http://server"
+ * getRootLength("http://server/path") === 14 // "http://server/"
+ * ```
+ */
+ export function getRootLength(path: string) {
+ const rootLength = getEncodedRootLength(path);
+ return rootLength < 0 ? ~rootLength : rootLength;
+ }
+
+ // TODO(rbuckton): replace references with `resolvePath`
+ export function normalizePath(path: string): string {
+ return resolvePath(path);
+ }
+
+ export function normalizePathAndParts(path: string): { path: string, parts: string[] } {
+ path = normalizeSlashes(path);
+ const [root, ...parts] = reducePathComponents(getPathComponents(path));
+ if (parts.length) {
+ const joinedParts = root + parts.join(directorySeparator);
+ return { path: hasTrailingDirectorySeparator(path) ? ensureTrailingDirectorySeparator(joinedParts) : joinedParts, parts };
+ }
+ else {
+ return { path: root, parts };
+ }
+ }
+
+ /**
+ * Returns the path except for its basename. Semantics align with NodeJS's `path.dirname`
+ * except that we support URL's as well.
+ *
+ * ```ts
+ * getDirectoryPath("/path/to/file.ext") === "/path/to"
+ * getDirectoryPath("/path/to/") === "/path"
+ * getDirectoryPath("/") === "/"
+ * ```
+ */
+ export function getDirectoryPath(path: Path): Path;
+ /**
+ * Returns the path except for its basename. Semantics align with NodeJS's `path.dirname`
+ * except that we support URL's as well.
+ *
+ * ```ts
+ * getDirectoryPath("/path/to/file.ext") === "/path/to"
+ * getDirectoryPath("/path/to/") === "/path"
+ * getDirectoryPath("/") === "/"
+ * ```
+ */
+ export function getDirectoryPath(path: string): string;
+ export function getDirectoryPath(path: string): string {
+ path = normalizeSlashes(path);
+
+ // If the path provided is itself the root, then return it.
+ const rootLength = getRootLength(path);
+ if (rootLength === path.length) return path;
+
+ // return the leading portion of the path up to the last (non-terminal) directory separator
+ // but not including any trailing directory separator.
+ path = removeTrailingDirectorySeparator(path);
+ return path.slice(0, Math.max(rootLength, path.lastIndexOf(directorySeparator)));
+ }
+
+ export function isUrl(path: string) {
+ return getEncodedRootLength(path) < 0;
+ }
+
+ export function pathIsRelative(path: string): boolean {
+ return /^\.\.?($|[\\/])/.test(path);
+ }
+
+ /**
+ * Determines whether a path is an absolute path (e.g. starts with `/`, or a dos path
+ * like `c:`, `c:\` or `c:/`).
+ */
export function isRootedDiskPath(path: string) {
- return path && getRootLength(path) !== 0;
+ return getEncodedRootLength(path) > 0;
+ }
+
+ /**
+ * Determines whether a path consists only of a path root.
+ */
+ export function isDiskPathRoot(path: string) {
+ const rootLength = getEncodedRootLength(path);
+ return rootLength > 0 && rootLength === path.length;
}
export function convertToRelativePath(absoluteOrRelativePath: string, basePath: string, getCanonicalFileName: (path: string) => string): string {
@@ -2099,146 +2195,214 @@ namespace ts {
: getRelativePathToDirectoryOrUrl(basePath, absoluteOrRelativePath, basePath, getCanonicalFileName, /*isAbsolutePathAnUrl*/ false);
}
- function normalizedPathComponents(path: string, rootLength: number) {
- const normalizedParts = getNormalizedParts(path, rootLength);
- return [path.substr(0, rootLength)].concat(normalizedParts);
+ function pathComponents(path: string, rootLength: number) {
+ const root = path.substring(0, rootLength);
+ const rest = path.substring(rootLength).split(directorySeparator);
+ if (rest.length && !lastOrUndefined(rest)) rest.pop();
+ return [root, ...rest];
}
- export function getNormalizedPathComponents(path: string, currentDirectory: string) {
- path = normalizeSlashes(path);
- let rootLength = getRootLength(path);
- if (rootLength === 0) {
- // If the path is not rooted it is relative to current directory
- path = combinePaths(normalizeSlashes(currentDirectory), path);
- rootLength = getRootLength(path);
+ /**
+ * Parse a path into an array containing a root component (at index 0) and zero or more path
+ * components (at indices > 0). The result is not normalized.
+ * If the path is relative, the root component is `""`.
+ * If the path is absolute, the root component includes the first path separator (`/`).
+ */
+ export function getPathComponents(path: string, currentDirectory = "") {
+ path = combinePaths(currentDirectory, path);
+ const rootLength = getRootLength(path);
+ return pathComponents(path, rootLength);
+ }
+
+ /**
+ * Reduce an array of path components to a more simplified path by navigating any
+ * `"."` or `".."` entries in the path.
+ */
+ export function reducePathComponents(components: ReadonlyArray) {
+ if (!some(components)) return [];
+ const reduced = [components[0]];
+ for (let i = 1; i < components.length; i++) {
+ const component = components[i];
+ if (component === ".") continue;
+ if (component === "..") {
+ if (reduced.length > 1) {
+ if (reduced[reduced.length - 1] !== "..") {
+ reduced.pop();
+ continue;
+ }
+ }
+ else if (reduced[0]) continue;
+ }
+ reduced.push(component);
}
+ return reduced;
+ }
- return normalizedPathComponents(path, rootLength);
+ /**
+ * Parse a path into an array containing a root component (at index 0) and zero or more path
+ * components (at indices > 0). The result is normalized.
+ * If the path is relative, the root component is `""`.
+ * If the path is absolute, the root component includes the first path separator (`/`).
+ */
+ export function getNormalizedPathComponents(path: string, currentDirectory: string) {
+ return reducePathComponents(getPathComponents(path, currentDirectory));
}
export function getNormalizedAbsolutePath(fileName: string, currentDirectory: string) {
- return getNormalizedPathFromPathComponents(getNormalizedPathComponents(fileName, currentDirectory));
+ return getPathFromPathComponents(getNormalizedPathComponents(fileName, currentDirectory));
}
- export function getNormalizedPathFromPathComponents(pathComponents: ReadonlyArray) {
- if (pathComponents && pathComponents.length) {
- return pathComponents[0] + pathComponents.slice(1).join(directorySeparator);
- }
+ /**
+ * Formats a parsed path consisting of a root component (at index 0) and zero or more path
+ * segments (at indices > 0).
+ */
+ export function getPathFromPathComponents(pathComponents: ReadonlyArray) {
+ if (pathComponents.length === 0) return "";
+
+ const root = pathComponents[0] && ensureTrailingDirectorySeparator(pathComponents[0]);
+ if (pathComponents.length === 1) return root;
+
+ return root + pathComponents.slice(1).join(directorySeparator);
}
- function getNormalizedPathComponentsOfUrl(url: string) {
- // Get root length of http://www.website.com/folder1/folder2/
- // In this example the root is: http://www.website.com/
- // normalized path components should be ["http://www.website.com/", "folder1", "folder2"]
+ function getPathComponentsRelativeTo(from: string, to: string, stringEqualityComparer: (a: string, b: string) => boolean, getCanonicalFileName: GetCanonicalFileName) {
+ const fromComponents = reducePathComponents(getPathComponents(from));
+ const toComponents = reducePathComponents(getPathComponents(to));
- const urlLength = url.length;
- // Initial root length is http:// part
- let rootLength = url.indexOf("://") + "://".length;
- while (rootLength < urlLength) {
- // Consume all immediate slashes in the protocol
- // eg.initial rootlength is just file:// but it needs to consume another "/" in file:///
- if (url.charCodeAt(rootLength) === CharacterCodes.slash) {
- rootLength++;
- }
- else {
- // non slash character means we continue proceeding to next component of root search
- break;
- }
+ let start: number;
+ for (start = 0; start < fromComponents.length && start < toComponents.length; start++) {
+ const fromComponent = getCanonicalFileName(fromComponents[start]);
+ const toComponent = getCanonicalFileName(toComponents[start]);
+ const comparer = start === 0 ? equateStringsCaseInsensitive : stringEqualityComparer;
+ if (!comparer(fromComponent, toComponent)) break;
}
- // there are no parts after http:// just return current string as the pathComponent
- if (rootLength === urlLength) {
- return [url];
+ if (start === 0) {
+ return toComponents;
}
- // Find the index of "/" after website.com so the root can be http://www.website.com/ (from existing http://)
- const indexOfNextSlash = url.indexOf(directorySeparator, rootLength);
- if (indexOfNextSlash !== -1) {
- // Found the "/" after the website.com so the root is length of http://www.website.com/
- // and get components after the root normally like any other folder components
- rootLength = indexOfNextSlash + 1;
- return normalizedPathComponents(url, rootLength);
- }
- else {
- // Can't find the host assume the rest of the string as component
- // but make sure we append "/" to it as root is not joined using "/"
- // eg. if url passed in was http://website.com we want to use root as [http://website.com/]
- // so that other path manipulations will be correct and it can be merged with relative paths correctly
- return [url + directorySeparator];
+ const components = toComponents.slice(start);
+ const relative: string[] = [];
+ for (; start < fromComponents.length; start++) {
+ relative.push("..");
}
+ return ["", ...relative, ...components];
}
- function getNormalizedPathOrUrlComponents(pathOrUrl: string, currentDirectory: string) {
- if (isUrl(pathOrUrl)) {
- return getNormalizedPathComponentsOfUrl(pathOrUrl);
- }
- else {
- return getNormalizedPathComponents(pathOrUrl, currentDirectory);
- }
+ /**
+ * Gets a relative path that can be used to traverse between `from` and `to`.
+ */
+ export function getRelativePath(from: string, to: string, ignoreCase: boolean): string;
+ /**
+ * Gets a relative path that can be used to traverse between `from` and `to`.
+ */
+ // tslint:disable-next-line:unified-signatures
+ export function getRelativePath(from: string, to: string, getCanonicalFileName: GetCanonicalFileName): string;
+ export function getRelativePath(from: string, to: string, getCanonicalFileNameOrIgnoreCase: GetCanonicalFileName | boolean) {
+ Debug.assert((getRootLength(from) > 0) === (getRootLength(to) > 0), "Paths must either both be absolute or both be relative");
+ const getCanonicalFileName = typeof getCanonicalFileNameOrIgnoreCase === "function" ? getCanonicalFileNameOrIgnoreCase : identity;
+ const ignoreCase = typeof getCanonicalFileNameOrIgnoreCase === "boolean" ? getCanonicalFileNameOrIgnoreCase : false;
+ const pathComponents = getPathComponentsRelativeTo(from, to, ignoreCase ? equateStringsCaseInsensitive : equateStringsCaseSensitive, getCanonicalFileName);
+ return getPathFromPathComponents(pathComponents);
}
export function getRelativePathToDirectoryOrUrl(directoryPathOrUrl: string, relativeOrAbsolutePath: string, currentDirectory: string, getCanonicalFileName: GetCanonicalFileName, isAbsolutePathAnUrl: boolean) {
- const pathComponents = getNormalizedPathOrUrlComponents(relativeOrAbsolutePath, currentDirectory);
- const directoryComponents = getNormalizedPathOrUrlComponents(directoryPathOrUrl, currentDirectory);
- if (directoryComponents.length > 1 && lastOrUndefined(directoryComponents) === "") {
- // If the directory path given was of type test/cases/ then we really need components of directory to be only till its name
- // that is ["test", "cases", ""] needs to be actually ["test", "cases"]
- directoryComponents.pop();
- }
+ const pathComponents = getPathComponentsRelativeTo(
+ resolvePath(currentDirectory, directoryPathOrUrl),
+ resolvePath(currentDirectory, relativeOrAbsolutePath),
+ equateStringsCaseSensitive,
+ getCanonicalFileName
+ );
- // Find the component that differs
- let joinStartIndex: number;
- for (joinStartIndex = 0; joinStartIndex < pathComponents.length && joinStartIndex < directoryComponents.length; joinStartIndex++) {
- if (getCanonicalFileName(directoryComponents[joinStartIndex]) !== getCanonicalFileName(pathComponents[joinStartIndex])) {
- break;
- }
+ const firstComponent = pathComponents[0];
+ if (isAbsolutePathAnUrl && isRootedDiskPath(firstComponent)) {
+ const prefix = firstComponent.charAt(0) === directorySeparator ? "file://" : "file:///";
+ pathComponents[0] = prefix + firstComponent;
}
- // Get the relative path
- if (joinStartIndex) {
- let relativePath = "";
- const relativePathComponents = pathComponents.slice(joinStartIndex, pathComponents.length);
- for (; joinStartIndex < directoryComponents.length; joinStartIndex++) {
- if (directoryComponents[joinStartIndex] !== "") {
- relativePath = relativePath + ".." + directorySeparator;
- }
- }
+ return getPathFromPathComponents(pathComponents);
+ }
- return relativePath + relativePathComponents.join(directorySeparator);
- }
+ /**
+ * Ensures a path is either absolute (prefixed with `/` or `c:`) or dot-relative (prefixed
+ * with `./` or `../`) so as not to be confused with an unprefixed module name.
+ */
+ export function ensurePathIsNonModuleName(path: string): string {
+ return getRootLength(path) === 0 && !pathIsRelative(path) ? "./" + path : path;
+ }
- // Cant find the relative path, get the absolute path
- let absolutePath = getNormalizedPathFromPathComponents(pathComponents);
- if (isAbsolutePathAnUrl && isRootedDiskPath(absolutePath)) {
- absolutePath = "file:///" + absolutePath;
- }
+ /**
+ * Returns the path except for its containing directory name.
+ * Semantics align with NodeJS's `path.basename` except that we support URL's as well.
+ *
+ * ```ts
+ * getBaseFileName("/path/to/file.ext") === "file.ext"
+ * getBaseFileName("/path/to/") === "to"
+ * getBaseFileName("/") === ""
+ * ```
+ */
+ export function getBaseFileName(path: string): string;
+ /**
+ * Gets the portion of a path following the last (non-terminal) separator (`/`).
+ * Semantics align with NodeJS's `path.basename` except that we support URL's as well.
+ * If the base name has any one of the provided extensions, it is removed.
+ *
+ * ```ts
+ * getBaseFileName("/path/to/file.ext", ".ext", true) === "file"
+ * getBaseFileName("/path/to/file.js", ".ext", true) === "file.js"
+ * ```
+ */
+ export function getBaseFileName(path: string, extensions: string | ReadonlyArray, ignoreCase: boolean): string;
+ export function getBaseFileName(path: string, extensions?: string | ReadonlyArray, ignoreCase?: boolean) {
+ path = normalizeSlashes(path);
- return absolutePath;
- }
+ // if the path provided is itself the root, then it has not file name.
+ const rootLength = getRootLength(path);
+ if (rootLength === path.length) return "";
- export function getRelativePath(path: string, directoryPath: string, getCanonicalFileName: GetCanonicalFileName) {
- const relativePath = getRelativePathToDirectoryOrUrl(directoryPath, path, directoryPath, getCanonicalFileName, /*isAbsolutePathAnUrl*/ false);
- return ensurePathIsRelative(relativePath);
+ // return the trailing portion of the path starting after the last (non-terminal) directory
+ // separator but not including any trailing directory separator.
+ path = removeTrailingDirectorySeparator(path);
+ const name = path.slice(Math.max(getRootLength(path), path.lastIndexOf(directorySeparator) + 1));
+ const extension = extensions !== undefined && ignoreCase !== undefined ? getAnyExtensionFromPath(name, extensions, ignoreCase) : undefined;
+ return extension ? name.slice(0, name.length - extension.length) : name;
}
- export function ensurePathIsRelative(path: string): string {
- return !pathIsRelative(path) ? "./" + path : path;
+ /**
+ * Combines paths. If a path is absolute, it replaces any previous path.
+ */
+ export function combinePaths(path: string, ...paths: string[]): string {
+ if (path) path = normalizeSlashes(path);
+ for (let relativePath of paths) {
+ if (!relativePath) continue;
+ relativePath = normalizeSlashes(relativePath);
+ if (!path || getRootLength(relativePath) !== 0) {
+ path = relativePath;
+ }
+ else {
+ path = ensureTrailingDirectorySeparator(path) + relativePath;
+ }
+ }
+ return path;
}
- export function getBaseFileName(path: string) {
- if (path === undefined) {
- return undefined;
- }
- const i = path.lastIndexOf(directorySeparator);
- return i < 0 ? path : path.substring(i + 1);
+ /**
+ * Combines and resolves paths. If a path is absolute, it replaces any previous path. Any
+ * `.` and `..` path components are resolved.
+ */
+ export function resolvePath(path: string, ...paths: string[]): string {
+ const combined = some(paths) ? combinePaths(path, ...paths) : normalizeSlashes(path);
+ const normalized = getPathFromPathComponents(reducePathComponents(getPathComponents(combined)));
+ return normalized && hasTrailingDirectorySeparator(combined) ? ensureTrailingDirectorySeparator(normalized) : normalized;
}
- export function combinePaths(path1: string, path2: string): string {
- if (!(path1 && path1.length)) return path2;
- if (!(path2 && path2.length)) return path1;
- if (getRootLength(path2) !== 0) return path2;
- if (path1.charAt(path1.length - 1) === directorySeparator) return path1 + path2;
- return path1 + directorySeparator + path2;
+ /**
+ * Determines whether a path has a trailing separator (`/` or `\\`).
+ */
+ export function hasTrailingDirectorySeparator(path: string) {
+ if (path.length === 0) return false;
+ const ch = path.charCodeAt(path.length - 1);
+ return ch === CharacterCodes.slash || ch === CharacterCodes.backslash;
}
/**
@@ -2248,7 +2412,7 @@ namespace ts {
export function removeTrailingDirectorySeparator(path: Path): Path;
export function removeTrailingDirectorySeparator(path: string): string;
export function removeTrailingDirectorySeparator(path: string) {
- if (path.charAt(path.length - 1) === directorySeparator) {
+ if (hasTrailingDirectorySeparator(path)) {
return path.substr(0, path.length - 1);
}
@@ -2262,47 +2426,78 @@ namespace ts {
export function ensureTrailingDirectorySeparator(path: Path): Path;
export function ensureTrailingDirectorySeparator(path: string): string;
export function ensureTrailingDirectorySeparator(path: string) {
- if (path.charAt(path.length - 1) !== directorySeparator) {
+ if (!hasTrailingDirectorySeparator(path)) {
return path + directorySeparator;
}
return path;
}
- export function comparePaths(a: string, b: string, currentDirectory: string, ignoreCase?: boolean) {
+ function comparePathsWorker(a: string, b: string, componentComparer: (a: string, b: string) => Comparison) {
if (a === b) return Comparison.EqualTo;
if (a === undefined) return Comparison.LessThan;
if (b === undefined) return Comparison.GreaterThan;
- a = removeTrailingDirectorySeparator(a);
- b = removeTrailingDirectorySeparator(b);
- const aComponents = getNormalizedPathComponents(a, currentDirectory);
- const bComponents = getNormalizedPathComponents(b, currentDirectory);
+ const aComponents = reducePathComponents(getPathComponents(a));
+ const bComponents = reducePathComponents(getPathComponents(b));
const sharedLength = Math.min(aComponents.length, bComponents.length);
- const comparer = getStringComparer(ignoreCase);
for (let i = 0; i < sharedLength; i++) {
- const result = comparer(aComponents[i], bComponents[i]);
+ const stringComparer = i === 0 ? compareStringsCaseInsensitive : componentComparer;
+ const result = stringComparer(aComponents[i], bComponents[i]);
if (result !== Comparison.EqualTo) {
return result;
}
}
-
return compareValues(aComponents.length, bComponents.length);
}
- export function containsPath(parent: string, child: string, currentDirectory: string, ignoreCase?: boolean) {
+ /**
+ * Performs a case-sensitive comparison of two paths.
+ */
+ export function comparePathsCaseSensitive(a: string, b: string) {
+ return comparePathsWorker(a, b, compareStringsCaseSensitive);
+ }
+
+ /**
+ * Performs a case-insensitive comparison of two paths.
+ */
+ export function comparePathsCaseInsensitive(a: string, b: string) {
+ return comparePathsWorker(a, b, compareStringsCaseInsensitive);
+ }
+
+ export function comparePaths(a: string, b: string, ignoreCase?: boolean): Comparison;
+ export function comparePaths(a: string, b: string, currentDirectory: string, ignoreCase?: boolean): Comparison;
+ export function comparePaths(a: string, b: string, currentDirectory?: string | boolean, ignoreCase?: boolean) {
+ if (typeof currentDirectory === "string") {
+ a = combinePaths(currentDirectory, a);
+ b = combinePaths(currentDirectory, b);
+ }
+ else if (typeof currentDirectory === "boolean") {
+ ignoreCase = currentDirectory;
+ }
+ return comparePathsWorker(a, b, getStringComparer(ignoreCase));
+ }
+
+ export function containsPath(parent: string, child: string, ignoreCase?: boolean): boolean;
+ export function containsPath(parent: string, child: string, currentDirectory: string, ignoreCase?: boolean): boolean;
+ export function containsPath(parent: string, child: string, currentDirectory?: string | boolean, ignoreCase?: boolean) {
+ if (typeof currentDirectory === "string") {
+ parent = combinePaths(currentDirectory, parent);
+ child = combinePaths(currentDirectory, child);
+ }
+ else if (typeof currentDirectory === "boolean") {
+ ignoreCase = currentDirectory;
+ }
if (parent === undefined || child === undefined) return false;
if (parent === child) return true;
- parent = removeTrailingDirectorySeparator(parent);
- child = removeTrailingDirectorySeparator(child);
- if (parent === child) return true;
- const parentComponents = getNormalizedPathComponents(parent, currentDirectory);
- const childComponents = getNormalizedPathComponents(child, currentDirectory);
+ const parentComponents = reducePathComponents(getPathComponents(parent));
+ const childComponents = reducePathComponents(getPathComponents(child));
if (childComponents.length < parentComponents.length) {
return false;
}
- const equalityComparer = ignoreCase ? equateStringsCaseInsensitive : equateStringsCaseSensitive;
+ const componentEqualityComparer = ignoreCase ? equateStringsCaseInsensitive : equateStringsCaseSensitive;
for (let i = 0; i < parentComponents.length; i++) {
+ const equalityComparer = i === 0 ? equateStringsCaseInsensitive : componentEqualityComparer;
if (!equalityComparer(parentComponents[i], childComponents[i])) {
return false;
}
@@ -2791,7 +2986,14 @@ namespace ts {
}
export function changeExtension(path: T, newExtension: string): T {
- return (removeFileExtension(path) + newExtension);
+ return changeAnyExtension(path, newExtension, extensionsToRemove, /*ignoreCase*/ false);
+ }
+
+ export function changeAnyExtension(path: string, ext: string): string;
+ export function changeAnyExtension(path: string, ext: string, extensions: string | ReadonlyArray, ignoreCase: boolean): string;
+ export function changeAnyExtension(path: string, ext: string, extensions?: string | ReadonlyArray, ignoreCase?: boolean) {
+ const pathext = extensions !== undefined && ignoreCase !== undefined ? getAnyExtensionFromPath(path, extensions, ignoreCase) : getAnyExtensionFromPath(path);
+ return pathext ? path.slice(0, path.length - pathext.length) + (startsWith(ext, ".") ? ext : "." + ext) : path;
}
/**
@@ -3125,14 +3327,40 @@ namespace ts {
return find(supportedTypescriptExtensionsForExtractExtension, e => fileExtensionIs(path, e)) || find(supportedJavascriptExtensions, e => fileExtensionIs(path, e));
}
- // Retrieves any string from the final "." onwards from a base file name.
- // Unlike extensionFromPath, which throws an exception on unrecognized extensions.
- export function getAnyExtensionFromPath(path: string): string | undefined {
+ function getAnyExtensionFromPathWorker(path: string, extensions: string | ReadonlyArray, stringEqualityComparer: (a: string, b: string) => boolean) {
+ if (typeof extensions === "string") extensions = [extensions];
+ for (let extension of extensions) {
+ if (!startsWith(extension, ".")) extension = "." + extension;
+ if (path.length >= extension.length && path.charAt(path.length - extension.length) === ".") {
+ const pathExtension = path.slice(path.length - extension.length);
+ if (stringEqualityComparer(pathExtension, extension)) {
+ return pathExtension;
+ }
+ }
+ }
+ return "";
+ }
+
+ /**
+ * Gets the file extension for a path.
+ */
+ export function getAnyExtensionFromPath(path: string): string;
+ /**
+ * Gets the file extension for a path, provided it is one of the provided extensions.
+ */
+ export function getAnyExtensionFromPath(path: string, extensions: string | ReadonlyArray, ignoreCase: boolean): string;
+ export function getAnyExtensionFromPath(path: string, extensions?: string | ReadonlyArray, ignoreCase?: boolean): string {
+ // Retrieves any string from the final "." onwards from a base file name.
+ // Unlike extensionFromPath, which throws an exception on unrecognized extensions.
+ if (extensions) {
+ return getAnyExtensionFromPathWorker(path, extensions, ignoreCase ? equateStringsCaseInsensitive : equateStringsCaseSensitive);
+ }
const baseFileName = getBaseFileName(path);
const extensionIndex = baseFileName.lastIndexOf(".");
if (extensionIndex >= 0) {
return baseFileName.substring(extensionIndex);
}
+ return "";
}
export function isCheckJsEnabledForFile(sourceFile: SourceFile, compilerOptions: CompilerOptions) {
diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts
index 1c0bf6eff1349..ed61d20610c1b 100644
--- a/src/compiler/emitter.ts
+++ b/src/compiler/emitter.ts
@@ -65,7 +65,8 @@ namespace ts {
// JavaScript files are always LanguageVariant.JSX, as JSX syntax is allowed in .js files also.
// So for JavaScript files, '.jsx' is only emitted if the input was '.jsx', and JsxEmit.Preserve.
// For TypeScript, the only time to emit with a '.jsx' extension, is on JSX input, and JsxEmit.Preserve
- function getOutputExtension(sourceFile: SourceFile, options: CompilerOptions): Extension {
+ /* @internal */
+ export function getOutputExtension(sourceFile: SourceFile, options: CompilerOptions): Extension {
if (options.jsx === JsxEmit.Preserve) {
if (isSourceFileJavaScript(sourceFile)) {
if (fileExtensionIs(sourceFile.fileName, Extension.Jsx)) {
diff --git a/src/compiler/moduleNameResolver.ts b/src/compiler/moduleNameResolver.ts
index 81aed5224902f..9ede13888e8b7 100644
--- a/src/compiler/moduleNameResolver.ts
+++ b/src/compiler/moduleNameResolver.ts
@@ -806,7 +806,7 @@ namespace ts {
if (state.traceEnabled) {
trace(state.host, Diagnostics.Loading_module_as_file_Slash_folder_candidate_module_location_0_target_file_type_1, candidate, Extensions[extensions]);
}
- if (!pathEndsWithDirectorySeparator(candidate)) {
+ if (!hasTrailingDirectorySeparator(candidate)) {
if (!onlyRecordFailures) {
const parentOfCandidate = getDirectoryPath(candidate);
if (!directoryProbablyExists(parentOfCandidate, state.host)) {
diff --git a/src/compiler/program.ts b/src/compiler/program.ts
index b924944fdb5ea..db630d8ca22cf 100755
--- a/src/compiler/program.ts
+++ b/src/compiler/program.ts
@@ -57,7 +57,7 @@ namespace ts {
return currentDirectory;
}
- return getNormalizedPathFromPathComponents(commonPathComponents);
+ return getPathFromPathComponents(commonPathComponents);
}
interface OutputFingerprint {
diff --git a/src/compiler/watchUtilities.ts b/src/compiler/watchUtilities.ts
index fca4f858e1660..b7b8e57e8726c 100644
--- a/src/compiler/watchUtilities.ts
+++ b/src/compiler/watchUtilities.ts
@@ -63,7 +63,7 @@ namespace ts {
}
function getCachedFileSystemEntries(rootDirPath: Path): MutableFileSystemEntries | undefined {
- return cachedReadDirectoryResult.get(rootDirPath);
+ return cachedReadDirectoryResult.get(ensureTrailingDirectorySeparator(rootDirPath));
}
function getCachedFileSystemEntriesForBaseDir(path: Path): MutableFileSystemEntries | undefined {
@@ -80,7 +80,7 @@ namespace ts {
directories: host.getDirectories(rootDir) || []
};
- cachedReadDirectoryResult.set(rootDirPath, resultFromHost);
+ cachedReadDirectoryResult.set(ensureTrailingDirectorySeparator(rootDirPath), resultFromHost);
return resultFromHost;
}
@@ -90,6 +90,7 @@ namespace ts {
* The host request is done under try catch block to avoid caching incorrect result
*/
function tryReadDirectory(rootDir: string, rootDirPath: Path): MutableFileSystemEntries | undefined {
+ rootDirPath = ensureTrailingDirectorySeparator(rootDirPath);
const cachedResult = getCachedFileSystemEntries(rootDirPath);
if (cachedResult) {
return cachedResult;
@@ -100,7 +101,7 @@ namespace ts {
}
catch (_e) {
// If there is exception to read directories, dont cache the result and direct the calls to host
- Debug.assert(!cachedReadDirectoryResult.has(rootDirPath));
+ Debug.assert(!cachedReadDirectoryResult.has(ensureTrailingDirectorySeparator(rootDirPath)));
return undefined;
}
}
@@ -142,7 +143,7 @@ namespace ts {
function directoryExists(dirPath: string): boolean {
const path = toPath(dirPath);
- return cachedReadDirectoryResult.has(path) || host.directoryExists(dirPath);
+ return cachedReadDirectoryResult.has(ensureTrailingDirectorySeparator(path)) || host.directoryExists(dirPath);
}
function createDirectory(dirPath: string) {
diff --git a/src/harness/collections.ts b/src/harness/collections.ts
new file mode 100644
index 0000000000000..7c71296409c98
--- /dev/null
+++ b/src/harness/collections.ts
@@ -0,0 +1,321 @@
+namespace collections {
+ export interface SortOptions {
+ comparer: (a: T, b: T) => number;
+ sort: "insertion" | "comparison";
+ }
+
+ export class SortedMap {
+ private _comparer: (a: K, b: K) => number;
+ private _keys: K[] = [];
+ private _values: V[] = [];
+ private _order: number[] | undefined;
+ private _version = 0;
+ private _copyOnWrite = false;
+
+ constructor(comparer: ((a: K, b: K) => number) | SortOptions, iterable?: Iterable<[K, V]>) {
+ this._comparer = typeof comparer === "object" ? comparer.comparer : comparer;
+ this._order = typeof comparer === "object" && comparer.sort === "insertion" ? [] : undefined;
+ if (iterable) {
+ const iterator = getIterator(iterable);
+ try {
+ for (let i = nextResult(iterator); i; i = nextResult(iterator)) {
+ const [key, value] = i.value;
+ this.set(key, value);
+ }
+ }
+ finally {
+ closeIterator(iterator);
+ }
+ }
+ }
+
+ public get size() {
+ return this._keys.length;
+ }
+
+ public get comparer() {
+ return this._comparer;
+ }
+
+ public get [Symbol.toStringTag]() {
+ return "SortedMap";
+ }
+
+ public has(key: K) {
+ return ts.binarySearch(this._keys, key, ts.identity, this._comparer) >= 0;
+ }
+
+ public get(key: K) {
+ const index = ts.binarySearch(this._keys, key, ts.identity, this._comparer);
+ return index >= 0 ? this._values[index] : undefined;
+ }
+
+ public set(key: K, value: V) {
+ const index = ts.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);
+ if (this._order) insertAt(this._order, ~index, this._version);
+ this.writePostScript();
+ }
+ return this;
+ }
+
+ public delete(key: K) {
+ const index = ts.binarySearch(this._keys, key, ts.identity, this._comparer);
+ if (index >= 0) {
+ this.writePreamble();
+ ts.orderedRemoveItemAt(this._keys, index);
+ ts.orderedRemoveItemAt(this._values, index);
+ if (this._order) ts.orderedRemoveItemAt(this._order, index);
+ this.writePostScript();
+ return true;
+ }
+ return false;
+ }
+
+ public clear() {
+ if (this.size > 0) {
+ this.writePreamble();
+ this._keys.length = 0;
+ this._values.length = 0;
+ if (this._order) this._order.length = 0;
+ this.writePostScript();
+ }
+ }
+
+ public forEach(callback: (value: V, key: K, collection: this) => void, thisArg?: any) {
+ const keys = this._keys;
+ const values = this._values;
+ const indices = this.getIterationOrder();
+ const version = this._version;
+ this._copyOnWrite = true;
+ try {
+ if (indices) {
+ for (const i of indices) {
+ callback.call(thisArg, values[i], keys[i], this);
+ }
+ }
+ else {
+ for (let i = 0; i < keys.length; i++) {
+ callback.call(thisArg, values[i], keys[i], this);
+ }
+ }
+ }
+ finally {
+ if (version === this._version) {
+ this._copyOnWrite = false;
+ }
+ }
+ }
+
+ public * keys() {
+ const keys = this._keys;
+ const indices = this.getIterationOrder();
+ const version = this._version;
+ this._copyOnWrite = true;
+ try {
+ if (indices) {
+ for (const i of indices) {
+ yield keys[i];
+ }
+ }
+ else {
+ yield* keys;
+ }
+ }
+ finally {
+ if (version === this._version) {
+ this._copyOnWrite = false;
+ }
+ }
+ }
+
+ public * values() {
+ const values = this._values;
+ const indices = this.getIterationOrder();
+ const version = this._version;
+ this._copyOnWrite = true;
+ try {
+ if (indices) {
+ for (const i of indices) {
+ yield values[i];
+ }
+ }
+ else {
+ yield* values;
+ }
+ }
+ finally {
+ if (version === this._version) {
+ this._copyOnWrite = false;
+ }
+ }
+ }
+
+ public * entries() {
+ const keys = this._keys;
+ const values = this._values;
+ const indices = this.getIterationOrder();
+ const version = this._version;
+ this._copyOnWrite = true;
+ try {
+ if (indices) {
+ for (const i of indices) {
+ yield [keys[i], values[i]] as [K, V];
+ }
+ }
+ else {
+ for (let i = 0; i < keys.length; i++) {
+ yield [keys[i], values[i]] as [K, V];
+ }
+ }
+ }
+ finally {
+ if (version === this._version) {
+ this._copyOnWrite = false;
+ }
+ }
+ }
+
+ public [Symbol.iterator]() {
+ return this.entries();
+ }
+
+ private writePreamble() {
+ if (this._copyOnWrite) {
+ this._keys = this._keys.slice();
+ this._values = this._values.slice();
+ if (this._order) this._order = this._order.slice();
+ this._copyOnWrite = false;
+ }
+ }
+
+ private writePostScript() {
+ this._version++;
+ }
+
+ private getIterationOrder() {
+ if (this._order) {
+ const order = this._order;
+ return this._order
+ .map((_, i) => i)
+ .sort((x, y) => order[x] - order[y]);
+ }
+ return undefined;
+ }
+ }
+
+ 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 getIterator(iterable: Iterable): Iterator {
+ return iterable[Symbol.iterator]();
+ }
+
+ export function nextResult(iterator: Iterator): IteratorResult | undefined {
+ const result = iterator.next();
+ return result.done ? undefined : result;
+ }
+
+ export function closeIterator(iterator: Iterator) {
+ const fn = iterator.return;
+ if (typeof fn === "function") fn.call(iterator);
+ }
+
+ /**
+ * 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 get parent() {
+ return this._parent;
+ }
+
+ 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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/harness/compiler.ts b/src/harness/compiler.ts
new file mode 100644
index 0000000000000..8308074c44466
--- /dev/null
+++ b/src/harness/compiler.ts
@@ -0,0 +1,248 @@
+/**
+ * Test harness compiler functionality.
+ */
+namespace compiler {
+ export interface Project {
+ file: string;
+ config?: ts.ParsedCommandLine;
+ errors?: ts.Diagnostic[];
+ }
+
+ export function readProject(host: fakes.ParseConfigHost, project: string | undefined, existingOptions?: ts.CompilerOptions): Project | undefined {
+ if (project) {
+ project = host.vfs.stringComparer(vpath.basename(project), "tsconfig.json") === 0 ? project :
+ vpath.combine(project, "tsconfig.json");
+ }
+ else {
+ [project] = host.vfs.scanSync(".", "ancestors-or-self", {
+ accept: (path, stats) => stats.isFile() && host.vfs.stringComparer(vpath.basename(path), "tsconfig.json") === 0
+ });
+ }
+
+ if (project) {
+ // TODO(rbuckton): Do we need to resolve this? Resolving breaks projects tests.
+ // project = vpath.resolve(host.vfs.currentDirectory, project);
+
+ // read the config file
+ const readResult = ts.readConfigFile(project, path => host.readFile(path));
+ if (readResult.error) {
+ return { file: project, errors: [readResult.error] };
+ }
+
+ // parse the config file
+ const config = ts.parseJsonConfigFileContent(readResult.config, host, vpath.dirname(project), existingOptions);
+ return { file: project, errors: config.errors, config };
+ }
+ }
+
+ /**
+ * Correlates compilation inputs and outputs
+ */
+ export interface CompilationOutput {
+ readonly inputs: ReadonlyArray;
+ readonly js: documents.TextDocument | undefined;
+ readonly dts: documents.TextDocument | undefined;
+ readonly map: documents.TextDocument | undefined;
+ }
+
+ export class CompilationResult {
+ public readonly host: fakes.CompilerHost;
+ public readonly program: ts.Program | undefined;
+ public readonly result: ts.EmitResult | undefined;
+ public readonly options: ts.CompilerOptions;
+ public readonly diagnostics: ReadonlyArray;
+ public readonly js: ReadonlyMap;
+ public readonly dts: ReadonlyMap;
+ public readonly maps: ReadonlyMap;
+
+ private _inputs: documents.TextDocument[] = [];
+ private _inputsAndOutputs: collections.SortedMap;
+
+ constructor(host: fakes.CompilerHost, options: ts.CompilerOptions, program: ts.Program | undefined, result: ts.EmitResult | undefined, diagnostics: ts.Diagnostic[]) {
+ this.host = host;
+ this.program = program;
+ this.result = result;
+ this.diagnostics = diagnostics;
+ this.options = program ? program.getCompilerOptions() : options;
+
+ // collect outputs
+ const js = this.js = new collections.SortedMap({ comparer: this.vfs.stringComparer, sort: "insertion" });
+ const dts = this.dts = new collections.SortedMap({ comparer: this.vfs.stringComparer, sort: "insertion" });
+ const maps = this.maps = new collections.SortedMap({ comparer: this.vfs.stringComparer, sort: "insertion" });
+ for (const document of this.host.outputs) {
+ if (vpath.isJavaScript(document.file)) {
+ js.set(document.file, document);
+ }
+ else if (vpath.isDeclaration(document.file)) {
+ dts.set(document.file, document);
+ }
+ else if (vpath.isSourceMap(document.file)) {
+ maps.set(document.file, document);
+ }
+ }
+
+ // correlate inputs and outputs
+ this._inputsAndOutputs = new collections.SortedMap({ comparer: this.vfs.stringComparer, sort: "insertion" });
+ if (program) {
+ if (this.options.out || this.options.outFile) {
+ const outFile = vpath.resolve(this.vfs.cwd(), this.options.outFile || this.options.out);
+ const inputs: documents.TextDocument[] = [];
+ for (const sourceFile of program.getSourceFiles()) {
+ if (sourceFile) {
+ const input = new documents.TextDocument(sourceFile.fileName, sourceFile.text);
+ this._inputs.push(input);
+ if (!vpath.isDeclaration(sourceFile.fileName)) {
+ inputs.push(input);
+ }
+ }
+ }
+
+ const outputs: CompilationOutput = {
+ inputs,
+ js: js.get(outFile),
+ dts: dts.get(vpath.changeExtension(outFile, ".d.ts")),
+ map: maps.get(outFile + ".map")
+ };
+
+ if (outputs.js) this._inputsAndOutputs.set(outputs.js.file, outputs);
+ if (outputs.dts) this._inputsAndOutputs.set(outputs.dts.file, outputs);
+ if (outputs.map) this._inputsAndOutputs.set(outputs.map.file, outputs);
+
+ for (const input of inputs) {
+ this._inputsAndOutputs.set(input.file, outputs);
+ }
+ }
+ else {
+ for (const sourceFile of program.getSourceFiles()) {
+ if (sourceFile) {
+ const input = new documents.TextDocument(sourceFile.fileName, sourceFile.text);
+ this._inputs.push(input);
+ if (!vpath.isDeclaration(sourceFile.fileName)) {
+ const extname = ts.getOutputExtension(sourceFile, this.options);
+ const outputs: CompilationOutput = {
+ inputs: [input],
+ js: js.get(this.getOutputPath(sourceFile.fileName, extname)),
+ dts: dts.get(this.getOutputPath(sourceFile.fileName, ".d.ts")),
+ map: maps.get(this.getOutputPath(sourceFile.fileName, extname + ".map"))
+ };
+
+ this._inputsAndOutputs.set(sourceFile.fileName, outputs);
+ if (outputs.js) this._inputsAndOutputs.set(outputs.js.file, outputs);
+ if (outputs.dts) this._inputsAndOutputs.set(outputs.dts.file, outputs);
+ if (outputs.map) this._inputsAndOutputs.set(outputs.map.file, outputs);
+ }
+ }
+ }
+ }
+ }
+
+ this.diagnostics = diagnostics;
+ }
+
+ public get vfs(): vfs.FileSystem {
+ return this.host.vfs;
+ }
+
+ public get inputs(): ReadonlyArray {
+ return this._inputs;
+ }
+
+ public get outputs(): ReadonlyArray {
+ return this.host.outputs;
+ }
+
+ public get traces(): ReadonlyArray {
+ return this.host.traces;
+ }
+
+ public get emitSkipped(): boolean {
+ return this.result && this.result.emitSkipped || false;
+ }
+
+ public get singleFile(): boolean {
+ return !!this.options.outFile || !!this.options.out;
+ }
+
+ public get commonSourceDirectory(): string {
+ const common = this.program && this.program.getCommonSourceDirectory() || "";
+ return common && vpath.combine(this.vfs.cwd(), common);
+ }
+
+ public getInputsAndOutputs(path: string): CompilationOutput | undefined {
+ return this._inputsAndOutputs.get(vpath.resolve(this.vfs.cwd(), path));
+ }
+
+ public getInputs(path: string): ReadonlyArray | undefined {
+ const outputs = this.getInputsAndOutputs(path);
+ return outputs && outputs.inputs;
+ }
+
+ public getOutput(path: string, kind: "js" | "dts" | "map"): documents.TextDocument | undefined {
+ const outputs = this.getInputsAndOutputs(path);
+ return outputs && outputs[kind];
+ }
+
+ public getSourceMapRecord(): string | undefined {
+ if (this.result.sourceMaps && this.result.sourceMaps.length > 0) {
+ return Harness.SourceMapRecorder.getSourceMapRecord(this.result.sourceMaps, this.program, Array.from(this.js.values()), Array.from(this.dts.values()));
+ }
+ }
+
+ 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 && documents.SourceMap.fromSource(document.text);
+ }
+ if (this.options.sourceMap) {
+ const document = this.getOutput(path, "map");
+ return document && new documents.SourceMap(document.file, document.text);
+ }
+ }
+
+ public getOutputPath(path: string, ext: string): string {
+ if (this.options.outFile || this.options.out) {
+ path = vpath.resolve(this.vfs.cwd(), this.options.outFile || this.options.out);
+ }
+ else {
+ path = vpath.resolve(this.vfs.cwd(), path);
+ const outDir = ext === ".d.ts" ? this.options.declarationDir || this.options.outDir : this.options.outDir;
+ if (outDir) {
+ const common = this.commonSourceDirectory;
+ if (common) {
+ path = vpath.relative(common, path, this.vfs.ignoreCase);
+ path = vpath.combine(vpath.resolve(this.vfs.cwd(), this.options.outDir), path);
+ }
+ }
+ }
+ return vpath.changeExtension(path, ext);
+ }
+ }
+
+ export function compileFiles(host: fakes.CompilerHost, rootFiles: string[] | undefined, compilerOptions: ts.CompilerOptions): CompilationResult {
+ if (compilerOptions.project || !rootFiles || rootFiles.length === 0) {
+ const project = readProject(host.parseConfigHost, compilerOptions.project, compilerOptions);
+ if (project) {
+ if (project.errors && project.errors.length > 0) {
+ return new CompilationResult(host, compilerOptions, /*program*/ undefined, /*result*/ undefined, project.errors);
+ }
+ if (project.config) {
+ rootFiles = project.config.fileNames;
+ compilerOptions = project.config.options;
+ }
+ }
+ delete compilerOptions.project;
+ }
+
+ // establish defaults (aligns with old harness)
+ if (compilerOptions.target === undefined) compilerOptions.target = ts.ScriptTarget.ES3;
+ if (compilerOptions.newLine === undefined) compilerOptions.newLine = ts.NewLineKind.CarriageReturnLineFeed;
+ if (compilerOptions.skipDefaultLibCheck === undefined) compilerOptions.skipDefaultLibCheck = true;
+ if (compilerOptions.noErrorTruncation === undefined) compilerOptions.noErrorTruncation = true;
+
+ const program = ts.createProgram(rootFiles || [], compilerOptions, host);
+ const emitResult = program.emit();
+ const errors = ts.getPreEmitDiagnostics(program);
+ return new CompilationResult(host, compilerOptions, program, emitResult, errors);
+ }
+}
\ No newline at end of file
diff --git a/src/harness/compilerRunner.ts b/src/harness/compilerRunner.ts
index 4edff00b2887e..1ffa2fc63a7b7 100644
--- a/src/harness/compilerRunner.ts
+++ b/src/harness/compilerRunner.ts
@@ -1,6 +1,10 @@
///
///
///
+///
+///
+///
+///
const enum CompilerTestType {
Conformance,
@@ -41,185 +45,240 @@ class CompilerBaselineRunner extends RunnerBase {
return this.enumerateFiles(this.basePath, /\.tsx?$/, { recursive: true });
}
- private makeUnitName(name: string, root: string) {
- const path = ts.toPath(name, root, (fileName) => Harness.Compiler.getCanonicalFileName(fileName));
- const pathStart = ts.toPath(Harness.IO.getCurrentDirectory(), "", (fileName) => Harness.Compiler.getCanonicalFileName(fileName));
- return pathStart ? path.replace(pathStart, "/") : path;
+ public initializeTests() {
+ describe(this.testSuiteName + " tests", () => {
+ describe("Setup compiler for compiler baselines", () => {
+ this.parseOptions();
+ });
+
+ // this will set up a series of describe/it blocks to run between the setup and cleanup phases
+ const files = this.tests.length > 0 ? this.tests : this.enumerateTestFiles();
+ files.forEach(file => { this.checkTestCodeOutput(vpath.normalizeSeparators(file)); });
+ });
}
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 = fileName.replace(/^.*[\\\/]/, ""); // strips the fileName from the path.
- 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);
- }
- }
+ for (const { name, payload } of CompilerTest.getConfigurations(fileName)) {
+ describe(`${this.testSuiteName} tests for ${fileName}${name ? ` (${name})` : ``}`, () => {
+ this.runSuite(fileName, payload);
+ });
+ }
+ }
- 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);
- });
- }
+ 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; });
+ }
- if (tsConfigOptions && tsConfigOptions.configFilePath !== undefined) {
- tsConfigOptions.configFilePath = ts.combinePaths(rootDir, tsConfigOptions.configFilePath);
- tsConfigOptions.configFile.fileName = tsConfigOptions.configFilePath;
+ private parseOptions() {
+ if (this.options && this.options.length > 0) {
+ this.emit = false;
+
+ const opts = this.options.split(",");
+ for (const opt of opts) {
+ switch (opt) {
+ case "emit":
+ this.emit = true;
+ break;
+ default:
+ throw new Error("unsupported flag");
}
+ }
+ }
+ }
+}
- const output = Harness.Compiler.compileFiles(
- toBeCompiled, otherFiles, harnessSettings, /*options*/ tsConfigOptions, /*currentDirectory*/ harnessSettings.currentDirectory);
+interface CompilerTestConfiguration {
+ name: string;
+ payload: Harness.TestCaseParser.TestCaseContent;
+}
- options = output.options;
- result = output.result;
- });
+class CompilerTest {
+ private fileName: string;
+ private justName: string;
+ private lastUnit: Harness.TestCaseParser.TestUnitData;
+ private harnessSettings: Harness.TestCaseParser.CompilerSettings;
+ private hasNonDtsFiles: boolean;
+ private result: compiler.CompilationResult;
+ 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[];
- 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;
- });
+ 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`);
- // check errors
- it("Correct errors for " + fileName, () => {
- Harness.Compiler.doErrorBaseline(justName, tsConfigFiles.concat(toBeCompiled, otherFiles), result.errors, !!options.pretty);
- });
+ 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);
+ }
+ }
- it (`Correct module resolution tracing for ${fileName}`, () => {
- if (options.traceResolution) {
- Harness.Baseline.runBaseline(justName.replace(/\.tsx?$/, ".trace.json"), () => {
- return JSON.stringify(result.traceResults || [], undefined, 4);
- });
- }
- });
+ 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 = [];
- // Source maps?
- it("Correct sourcemap content for " + fileName, () => {
- if (options.sourceMap || options.inlineSourceMap || options.declarationMap) {
- Harness.Baseline.runBaseline(justName.replace(/\.tsx?$/, ".sourcemap.txt"), () => {
- const record = 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;
- });
+ 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));
}
});
-
- it("Correct JS output for " + fileName, () => {
- if (hasNonDtsFiles && this.emit) {
- Harness.Compiler.doJsEmitBaseline(justName, fileName, options, result, tsConfigFiles, toBeCompiled, otherFiles, harnessSettings);
- }
+ }
+ else {
+ this.toBeCompiled = units.map(unit => {
+ return this.createHarnessTestFile(unit, rootDir);
});
+ }
- it("Correct Sourcemap output for " + fileName, () => {
- Harness.Compiler.doSourcemapBaseline(justName, options, result, harnessSettings);
- });
+ if (tsConfigOptions && tsConfigOptions.configFilePath !== undefined) {
+ tsConfigOptions.configFilePath = ts.combinePaths(rootDir, tsConfigOptions.configFilePath);
+ tsConfigOptions.configFile.fileName = tsConfigOptions.configFilePath;
+ }
+
+ this.result = Harness.Compiler.compileFiles(
+ this.toBeCompiled,
+ this.otherFiles,
+ this.harnessSettings,
+ /*options*/ tsConfigOptions,
+ /*currentDirectory*/ this.harnessSettings.currentDirectory);
- it("Correct type/symbol baselines for " + fileName, () => {
- if (fileName.indexOf("APISample") >= 0) {
- return;
+ this.options = this.result.options;
+ }
+
+ 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"}`;
}
- Harness.Compiler.doTypeAndSymbolBaseline(justName, result.program, toBeCompiled.concat(otherFiles).filter(file => !!result.program.getSourceFile(file.unitName)));
- });
- });
+ const settings = { ...testCaseContent.settings };
+ if (scriptTarget) settings.target = scriptTarget;
+ if (moduleKind) settings.module = moduleKind;
+ configurations.push({ name, payload: { ...testCaseContent, settings } });
+ }
+ }
+
+ return configurations;
}
- 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 verifyDiagnostics() {
+ // check errors
+ Harness.Compiler.doErrorBaseline(
+ this.justName,
+ this.tsConfigFiles.concat(this.toBeCompiled, this.otherFiles),
+ this.result.diagnostics,
+ !!this.options.pretty);
}
- public initializeTests() {
- describe(this.testSuiteName + " tests", () => {
- describe("Setup compiler for compiler baselines", () => {
- this.parseOptions();
+ public verifyModuleResolution() {
+ if (this.options.traceResolution) {
+ Harness.Baseline.runBaseline(this.justName.replace(/\.tsx?$/, ".trace.json"), () => {
+ return utils.removeTestPathPrefixes(JSON.stringify(this.result.traces, undefined, 4));
});
+ }
+ }
- // 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));
- }
- });
+ public verifySourceMapRecord() {
+ if (this.options.sourceMap || this.options.inlineSourceMap || this.options.declarationMap) {
+ Harness.Baseline.runBaseline(this.justName.replace(/\.tsx?$/, ".sourcemap.txt"), () => {
+ const record = utils.removeTestPathPrefixes(this.result.getSourceMapRecord());
+ if ((this.options.noEmitOnError && this.result.diagnostics.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;
+ });
+ }
}
- private parseOptions() {
- if (this.options && this.options.length > 0) {
- this.emit = false;
+ public verifyJavaScriptOutput() {
+ if (this.hasNonDtsFiles) {
+ Harness.Compiler.doJsEmitBaseline(
+ this.justName,
+ this.fileName,
+ this.options,
+ this.result,
+ this.tsConfigFiles,
+ this.toBeCompiled,
+ this.otherFiles,
+ this.harnessSettings);
+ }
+ }
- const opts = this.options.split(",");
- for (const opt of opts) {
- switch (opt) {
- case "emit":
- this.emit = true;
- break;
- default:
- throw new Error("unsupported flag");
- }
- }
+ 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, ts.identity);
+ const pathStart = ts.toPath(Harness.IO.getCurrentDirectory(), "", ts.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/documents.ts b/src/harness/documents.ts
new file mode 100644
index 0000000000000..f521902910984
--- /dev/null
+++ b/src/harness/documents.ts
@@ -0,0 +1,187 @@
+// 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 {
+ public readonly meta: Map;
+ public readonly file: string;
+ public readonly text: string;
+
+ private _lineStarts: ReadonlyArray | undefined;
+ private _testFile: Harness.Compiler.TestFile | undefined;
+
+ constructor(file: string, text: string, meta?: Map) {
+ this.file = file;
+ this.text = text;
+ this.meta = meta || new Map();
+ }
+
+ public get lineStarts(): ReadonlyArray {
+ return this._lineStarts || (this._lineStarts = ts.computeLineStarts(this.text));
+ }
+
+ public static fromTestFile(file: Harness.Compiler.TestFile) {
+ return new TextDocument(
+ file.unitName,
+ file.content,
+ file.fileOptions && Object.keys(file.fileOptions)
+ .reduce((meta, key) => meta.set(key, file.fileOptions[key]), new Map()));
+ }
+
+ public asTestFile() {
+ return this._testFile || (this._testFile = {
+ unitName: this.file,
+ content: this.text,
+ fileOptions: Array.from(this.meta)
+ .reduce((obj, [key, value]) => (obj[key] = value, obj), {} as Record)
+ });
+ }
+ }
+
+ export interface RawSourceMap {
+ version: number;
+ file: string;
+ sourceRoot?: string;
+ sources: string[];
+ sourcesContent?: string[];
+ names: string[];
+ mappings: string;
+ }
+
+ export interface Mapping {
+ mappingIndex: number;
+ emittedLine: number;
+ emittedColumn: number;
+ sourceIndex: number;
+ sourceLine: number;
+ sourceColumn: number;
+ nameIndex?: number;
+ }
+
+ export class SourceMap {
+ public readonly raw: RawSourceMap;
+ public readonly mapFile: string | undefined;
+ public readonly version: number;
+ public readonly file: string;
+ public readonly sourceRoot: string | undefined;
+ public readonly sources: ReadonlyArray = [];
+ public readonly sourcesContent: ReadonlyArray | undefined;
+ 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[][][] = [];
+
+ constructor(mapFile: string | undefined, data: string | RawSourceMap) {
+ this.raw = typeof data === "string" ? JSON.parse(data) as RawSourceMap : data;
+ this.mapFile = mapFile;
+ this.version = this.raw.version;
+ this.file = this.raw.file;
+ this.sourceRoot = this.raw.sourceRoot;
+ this.sources = this.raw.sources;
+ this.sourcesContent = this.raw.sourcesContent;
+ this.names = this.raw.names;
+
+ // populate mappings
+ const mappings: Mapping[] = [];
+ let emittedLine = 0;
+ let emittedColumn = 0;
+ let sourceIndex = 0;
+ let sourceLine = 0;
+ let sourceColumn = 0;
+ let nameIndex = 0;
+ let match: RegExpExecArray | null;
+ while (match = SourceMap._mappingRegExp.exec(this.raw.mappings)) {
+ if (match[1]) {
+ const segment = SourceMap._decodeVLQ(match[1]);
+ if (segment.length !== 1 && segment.length !== 4 && segment.length !== 5) {
+ throw new Error("Invalid VLQ");
+ }
+
+ emittedColumn += segment[0];
+ if (segment.length >= 4) {
+ sourceIndex += segment[1];
+ sourceLine += segment[2];
+ sourceColumn += segment[3];
+ }
+
+ const mapping: Mapping = { mappingIndex: mappings.length, emittedLine, emittedColumn, sourceIndex, sourceLine, sourceColumn };
+ if (segment.length === 5) {
+ nameIndex += segment[4];
+ mapping.nameIndex = nameIndex;
+ }
+
+ mappings.push(mapping);
+
+ const mappingsForEmittedLine = this._emittedLineMappings[mapping.emittedLine] || (this._emittedLineMappings[mapping.emittedLine] = []);
+ mappingsForEmittedLine.push(mapping);
+
+ const mappingsForSource = this._sourceLineMappings[mapping.sourceIndex] || (this._sourceLineMappings[mapping.sourceIndex] = []);
+ const mappingsForSourceLine = mappingsForSource[mapping.sourceLine] || (mappingsForSource[mapping.sourceLine] = []);
+ mappingsForSourceLine.push(mapping);
+ }
+ else if (match[2]) {
+ emittedLine++;
+ emittedColumn = 0;
+ }
+ else {
+ throw new Error(`Unrecognized character '${match[0]}'.`);
+ }
+ }
+
+ this.mappings = mappings;
+ }
+
+ public static getUrl(text: string) {
+ let match: RegExpExecArray | null;
+ let lastMatch: RegExpExecArray | undefined;
+ while (match = SourceMap._sourceMappingURLRegExp.exec(text)) {
+ lastMatch = match;
+ }
+ return lastMatch ? lastMatch[1] : undefined;
+ }
+
+ public static fromUrl(url: string) {
+ const match = SourceMap._dataURLRegExp.exec(url);
+ return match ? new SourceMap(/*mapFile*/ undefined, new Buffer(match[1], "base64").toString("utf8")) : undefined;
+ }
+
+ public static fromSource(text: string) {
+ const url = this.getUrl(text);
+ return url && this.fromUrl(url);
+ }
+
+ public getMappingsForEmittedLine(emittedLine: number): ReadonlyArray | undefined {
+ return this._emittedLineMappings[emittedLine];
+ }
+
+ public getMappingsForSourceLine(sourceIndex: number, sourceLine: number): ReadonlyArray | undefined {
+ const mappingsForSource = this._sourceLineMappings[sourceIndex];
+ return mappingsForSource && mappingsForSource[sourceLine];
+ }
+
+ 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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/harness/externalCompileRunner.ts b/src/harness/externalCompileRunner.ts
index 3d07c69af8521..1d45351114d1b 100644
--- a/src/harness/externalCompileRunner.ts
+++ b/src/harness/externalCompileRunner.ts
@@ -41,7 +41,7 @@ abstract class ExternalCompileRunnerBase extends RunnerBase {
const cp = require("child_process");
it("should build successfully", () => {
- let cwd = path.join(__dirname, "../../", cls.testDir, directoryName);
+ let cwd = path.join(Harness.IO.getWorkspaceRoot(), cls.testDir, directoryName);
const stdio = isWorker ? "pipe" : "inherit";
let types: string[];
if (fs.existsSync(path.join(cwd, "test.json"))) {
@@ -69,7 +69,7 @@ abstract class ExternalCompileRunnerBase extends RunnerBase {
const install = cp.spawnSync(`npm`, ["i", "--ignore-scripts"], { cwd, timeout: timeout / 2, shell: true, stdio }); // NPM shouldn't take the entire timeout - if it takes a long time, it should be terminated and we should log the failure
if (install.status !== 0) throw new Error(`NPM Install for ${directoryName} failed: ${install.stderr.toString()}`);
}
- const args = [path.join(__dirname, "tsc.js")];
+ const args = [path.join(Harness.IO.getWorkspaceRoot(), "built/local/tsc.js")];
if (types) {
args.push("--types", types.join(","));
}
diff --git a/src/harness/fakes.ts b/src/harness/fakes.ts
new file mode 100644
index 0000000000000..669cb33c0c4c4
--- /dev/null
+++ b/src/harness/fakes.ts
@@ -0,0 +1,376 @@
+/**
+ * Fake implementations of various compiler dependencies.
+ */
+namespace fakes {
+ const processExitSentinel = new Error("System exit");
+
+ export interface SystemOptions {
+ executingFilePath?: string;
+ newLine?: "\r\n" | "\n";
+ env?: Record;
+ }
+
+ /**
+ * A fake `ts.System` that leverages a virtual file system.
+ */
+ export class System implements ts.System {
+ public readonly vfs: vfs.FileSystem;
+ public readonly args: string[] = [];
+ public readonly output: string[] = [];
+ public readonly newLine: string;
+ public readonly useCaseSensitiveFileNames: boolean;
+ public exitCode: number;
+
+ private readonly _executingFilePath: string | undefined;
+ private readonly _env: Record | undefined;
+
+ constructor(vfs: vfs.FileSystem, { executingFilePath, newLine = "\r\n", env }: SystemOptions = {}) {
+ this.vfs = vfs.isReadonly ? vfs.shadow() : vfs;
+ this.useCaseSensitiveFileNames = !this.vfs.ignoreCase;
+ this.newLine = newLine;
+ this._executingFilePath = executingFilePath;
+ this._env = env;
+ }
+
+ public write(message: string) {
+ this.output.push(message);
+ }
+
+ public readFile(path: string) {
+ try {
+ const content = this.vfs.readFileSync(path, "utf8");
+ return content === undefined ? undefined :
+ vpath.extname(path) === ".json" ? utils.removeComments(utils.removeByteOrderMark(content), utils.CommentRemoval.leadingAndTrailing) :
+ utils.removeByteOrderMark(content);
+ }
+ catch {
+ return undefined;
+ }
+ }
+
+ public writeFile(path: string, data: string, writeByteOrderMark?: boolean): void {
+ this.vfs.mkdirpSync(vpath.dirname(path));
+ this.vfs.writeFileSync(path, writeByteOrderMark ? utils.addUTF8ByteOrderMark(data) : data);
+ }
+
+ public fileExists(path: string) {
+ const stats = this._getStats(path);
+ return stats ? stats.isFile() : false;
+ }
+
+ public directoryExists(path: string) {
+ const stats = this._getStats(path);
+ return stats ? stats.isDirectory() : false;
+ }
+
+ public createDirectory(path: string): void {
+ this.vfs.mkdirpSync(path);
+ }
+
+ public getCurrentDirectory() {
+ return this.vfs.cwd();
+ }
+
+ public getDirectories(path: string) {
+ const result: string[] = [];
+ try {
+ for (const file of this.vfs.readdirSync(path)) {
+ if (this.vfs.statSync(vpath.combine(path, file)).isDirectory()) {
+ result.push(file);
+ }
+ }
+ }
+ catch { /*ignore*/ }
+ return result;
+ }
+
+ public readDirectory(path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray, depth?: number): string[] {
+ return ts.matchFiles(path, extensions, exclude, include, this.useCaseSensitiveFileNames, this.getCurrentDirectory(), depth, path => this.getAccessibleFileSystemEntries(path));
+ }
+
+ public getAccessibleFileSystemEntries(path: string): ts.FileSystemEntries {
+ const files: string[] = [];
+ const directories: string[] = [];
+ try {
+ for (const file of this.vfs.readdirSync(path)) {
+ try {
+ const stats = this.vfs.statSync(vpath.combine(path, file));
+ if (stats.isFile()) {
+ files.push(file);
+ }
+ else if (stats.isDirectory()) {
+ directories.push(file);
+ }
+ }
+ catch { /*ignored*/ }
+ }
+ }
+ catch { /*ignored*/ }
+ return { files, directories };
+ }
+
+ public exit(exitCode?: number) {
+ this.exitCode = exitCode;
+ throw processExitSentinel;
+ }
+
+ public getFileSize(path: string) {
+ const stats = this._getStats(path);
+ return stats && stats.isFile() ? stats.size : 0;
+ }
+
+ public resolvePath(path: string) {
+ return vpath.resolve(this.vfs.cwd(), path);
+ }
+
+ public getExecutingFilePath() {
+ if (this._executingFilePath === undefined) return ts.notImplemented();
+ return this._executingFilePath;
+ }
+
+ public getModifiedTime(path: string) {
+ const stats = this._getStats(path);
+ return stats ? stats.mtime : undefined;
+ }
+
+ public createHash(data: string): string {
+ return data;
+ }
+
+ public realpath(path: string) {
+ try {
+ return this.vfs.realpathSync(path);
+ }
+ catch {
+ return path;
+ }
+ }
+
+ public getEnvironmentVariable(name: string): string | undefined {
+ return this._env && this._env[name];
+ }
+
+ private _getStats(path: string) {
+ try {
+ return this.vfs.statSync(path);
+ }
+ catch {
+ return undefined;
+ }
+ }
+ }
+
+ /**
+ * A fake `ts.ParseConfigHost` that leverages a virtual file system.
+ */
+ export class ParseConfigHost implements ts.ParseConfigHost {
+ public readonly sys: System;
+
+ constructor(sys: System | vfs.FileSystem) {
+ if (sys instanceof vfs.FileSystem) sys = new System(sys);
+ this.sys = sys;
+ }
+
+ public get vfs() {
+ return this.sys.vfs;
+ }
+
+ public get useCaseSensitiveFileNames() {
+ return this.sys.useCaseSensitiveFileNames;
+ }
+
+ public fileExists(fileName: string): boolean {
+ return this.sys.fileExists(fileName);
+ }
+
+ public directoryExists(directoryName: string): boolean {
+ return this.sys.directoryExists(directoryName);
+ }
+
+ public readFile(path: string): string | undefined {
+ return this.sys.readFile(path);
+ }
+
+ public readDirectory(path: string, extensions: string[], excludes: string[], includes: string[], depth: number): string[] {
+ return this.sys.readDirectory(path, extensions, excludes, includes, depth);
+ }
+ }
+
+ /**
+ * A fake `ts.CompilerHost` that leverages a virtual file system.
+ */
+ export class CompilerHost implements ts.CompilerHost {
+ public readonly sys: System;
+ public readonly defaultLibLocation: string;
+ public readonly outputs: documents.TextDocument[] = [];
+ public readonly traces: string[] = [];
+ public readonly shouldAssertInvariants = !Harness.lightMode;
+
+ private _setParentNodes: boolean;
+ private _sourceFiles: collections.SortedMap;
+ private _parseConfigHost: ParseConfigHost;
+ private _newLine: string;
+
+ constructor(sys: System | vfs.FileSystem, options = ts.getDefaultCompilerOptions(), setParentNodes = false) {
+ if (sys instanceof vfs.FileSystem) sys = new System(sys);
+ this.sys = sys;
+ this.defaultLibLocation = sys.vfs.meta.get("defaultLibLocation") || "";
+ this._newLine = ts.getNewLineCharacter(options, () => this.sys.newLine);
+ this._sourceFiles = new collections.SortedMap({ comparer: sys.vfs.stringComparer, sort: "insertion" });
+ this._setParentNodes = setParentNodes;
+ }
+
+ public get vfs() {
+ return this.sys.vfs;
+ }
+
+ public get parseConfigHost() {
+ return this._parseConfigHost || (this._parseConfigHost = new ParseConfigHost(this.sys));
+ }
+
+ public getCurrentDirectory(): string {
+ return this.sys.getCurrentDirectory();
+ }
+
+ public useCaseSensitiveFileNames(): boolean {
+ return this.sys.useCaseSensitiveFileNames;
+ }
+
+ public getNewLine(): string {
+ return this._newLine;
+ }
+
+ public getCanonicalFileName(fileName: string): string {
+ return this.sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase();
+ }
+
+ public fileExists(fileName: string): boolean {
+ return this.sys.fileExists(fileName);
+ }
+
+ public directoryExists(directoryName: string): boolean {
+ return this.sys.directoryExists(directoryName);
+ }
+
+ public getDirectories(path: string): string[] {
+ return this.sys.getDirectories(path);
+ }
+
+ public readFile(path: string): string | undefined {
+ return this.sys.readFile(path);
+ }
+
+ public writeFile(fileName: string, content: string, writeByteOrderMark: boolean) {
+ if (writeByteOrderMark) content = utils.addUTF8ByteOrderMark(content);
+ this.sys.writeFile(fileName, content);
+
+ const document = new documents.TextDocument(fileName, content);
+ document.meta.set("fileName", fileName);
+ this.vfs.filemeta(fileName).set("document", document);
+ const index = this.outputs.findIndex(output => this.vfs.stringComparer(document.file, output.file) === 0);
+ if (index < 0) {
+ this.outputs.push(document);
+ }
+ else {
+ this.outputs[index] = document;
+ }
+ }
+
+ public trace(s: string): void {
+ this.traces.push(s);
+ }
+
+ public realpath(path: string): string {
+ return this.sys.realpath(path);
+ }
+
+ public getDefaultLibLocation(): string {
+ return vpath.resolve(this.getCurrentDirectory(), this.defaultLibLocation);
+ }
+
+ public getDefaultLibFileName(options: ts.CompilerOptions): string {
+ // return vpath.resolve(this.getDefaultLibLocation(), ts.getDefaultLibFileName(options));
+
+ // TODO(rbuckton): This patches the baseline to replace lib.es5.d.ts with lib.d.ts.
+ // This is only to make the PR for this change easier to read. A follow-up PR will
+ // revert this change and accept the new baselines.
+ // See https://github.com/Microsoft/TypeScript/pull/20763#issuecomment-352553264
+ return vpath.resolve(this.getDefaultLibLocation(), getDefaultLibFileName(options));
+ function getDefaultLibFileName(options: ts.CompilerOptions) {
+ switch (options.target) {
+ case ts.ScriptTarget.ESNext:
+ case ts.ScriptTarget.ES2017:
+ return "lib.es2017.d.ts";
+ case ts.ScriptTarget.ES2016:
+ return "lib.es2016.d.ts";
+ case ts.ScriptTarget.ES2015:
+ return "lib.es2015.d.ts";
+
+ default:
+ return "lib.d.ts";
+ }
+ }
+ }
+
+ public getSourceFile(fileName: string, languageVersion: number): ts.SourceFile | undefined {
+ const canonicalFileName = this.getCanonicalFileName(vpath.resolve(this.getCurrentDirectory(), fileName));
+ const existing = this._sourceFiles.get(canonicalFileName);
+ if (existing) return existing;
+
+ const content = this.readFile(canonicalFileName);
+ if (content === undefined) return undefined;
+
+ // A virtual file system may shadow another existing virtual file system. This
+ // allows us to reuse a common virtual file system structure across multiple
+ // tests. If a virtual file is a shadow, it is likely that the file will be
+ // reused across multiple tests. In that case, we cache the SourceFile we parse
+ // so that it can be reused across multiple tests to avoid the cost of
+ // repeatedly parsing the same file over and over (such as lib.d.ts).
+ const cacheKey = this.vfs.shadowRoot && `SourceFile[languageVersion=${languageVersion},setParentNodes=${this._setParentNodes}]`;
+ if (cacheKey) {
+ const meta = this.vfs.filemeta(canonicalFileName);
+ const sourceFileFromMetadata = meta.get(cacheKey) as ts.SourceFile | undefined;
+ if (sourceFileFromMetadata) {
+ this._sourceFiles.set(canonicalFileName, sourceFileFromMetadata);
+ return sourceFileFromMetadata;
+ }
+ }
+
+ const parsed = ts.createSourceFile(fileName, content, languageVersion, this._setParentNodes || this.shouldAssertInvariants);
+ if (this.shouldAssertInvariants) {
+ Utils.assertInvariants(parsed, /*parent*/ undefined);
+ }
+
+ this._sourceFiles.set(canonicalFileName, parsed);
+
+ if (cacheKey) {
+ // store the cached source file on the unshadowed file with the same version.
+ const stats = this.vfs.statSync(canonicalFileName);
+
+ let fs = this.vfs;
+ while (fs.shadowRoot) {
+ try {
+ const shadowRootStats = fs.shadowRoot.statSync(canonicalFileName);
+ if (shadowRootStats.dev !== stats.dev ||
+ shadowRootStats.ino !== stats.ino ||
+ shadowRootStats.mtimeMs !== stats.mtimeMs) {
+ break;
+ }
+
+ fs = fs.shadowRoot;
+ }
+ catch {
+ break;
+ }
+ }
+
+ if (fs !== this.vfs) {
+ fs.filemeta(canonicalFileName).set(cacheKey, parsed);
+ }
+ }
+
+ return parsed;
+ }
+ }
+}
+
diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts
index 5e932050bfd7f..2ab96fe167606 100644
--- a/src/harness/fourslash.ts
+++ b/src/harness/fourslash.ts
@@ -18,6 +18,7 @@
///
///
///
+///
namespace FourSlash {
ts.disableIncrementalParsing = false;
@@ -277,7 +278,13 @@ namespace FourSlash {
if (configFileName) {
const baseDir = ts.normalizePath(ts.getDirectoryPath(configFileName));
- const host = new Utils.MockParseConfigHost(baseDir, /*ignoreCase*/ false, this.inputFiles);
+ const files: vfs.FileSet = { [baseDir]: {} };
+ this.inputFiles.forEach((data, path) => {
+ const scriptInfo = new Harness.LanguageService.ScriptInfo(path, undefined, /*isRootFile*/ false);
+ files[path] = new vfs.File(data, { meta: { scriptInfo } });
+ });
+ const fs = new vfs.FileSystem(/*ignoreCase*/ true, { cwd: baseDir, files });
+ const host = new fakes.ParseConfigHost(fs);
const jsonSourceFile = ts.parseJsonText(configFileName, this.inputFiles.get(configFileName));
compilationOptions = ts.parseJsonSourceFileConfigFileContent(jsonSourceFile, host, baseDir, compilationOptions, configFileName).options;
}
@@ -333,7 +340,10 @@ namespace FourSlash {
}
for (const file of testData.files) {
- ts.forEach(file.symlinks, link => this.languageServiceAdapterHost.addSymlink(link, file.fileName));
+ ts.forEach(file.symlinks, link => {
+ this.languageServiceAdapterHost.vfs.mkdirpSync(vpath.dirname(link));
+ this.languageServiceAdapterHost.vfs.symlinkSync(file.fileName, link);
+ });
}
this.formatCodeSettings = {
diff --git a/src/harness/harness.ts b/src/harness/harness.ts
index c955c1acb59d2..53ca42d2d70a1 100644
--- a/src/harness/harness.ts
+++ b/src/harness/harness.ts
@@ -20,7 +20,7 @@
///
///
///
-///
+///
///
///
///
@@ -56,7 +56,6 @@ var assert: typeof _chai.assert = _chai.assert;
};
}
-declare var __dirname: string; // Node-specific
var global: NodeJS.Global = Function("return this").call(undefined);
declare var window: {};
@@ -67,11 +66,15 @@ interface XMLHttpRequest {
readonly readyState: number;
readonly responseText: string;
readonly status: number;
+ readonly statusText: string;
open(method: string, url: string, async?: boolean, user?: string, password?: string): void;
send(data?: string): void;
setRequestHeader(header: string, value: string): void;
+ getAllResponseHeaders(): string;
+ getResponseHeader(header: string): string | null;
+ overrideMimeType(mime: string): void;
}
-/* tslint:enable:no-var-keyword */
+/* tslint:enable:no-var-keyword prefer-const */
namespace Utils {
// Setup some globals based on the current environment
@@ -496,11 +499,13 @@ namespace Utils {
}
namespace Harness {
- export interface Io {
+ // tslint:disable-next-line:interface-name
+ export interface IO {
newLine(): string;
getCurrentDirectory(): string;
useCaseSensitiveFileNames(): boolean;
resolvePath(path: string): string;
+ getFileSize(path: string): number;
readFile(path: string): string | undefined;
writeFile(path: string, contents: string): void;
directoryName(path: string): string;
@@ -509,17 +514,20 @@ namespace Harness {
fileExists(fileName: string): boolean;
directoryExists(path: string): boolean;
deleteFile(fileName: string): void;
- listFiles(path: string, filter: RegExp, options?: { recursive?: boolean }): string[];
+ listFiles(path: string, filter?: RegExp, options?: { recursive?: boolean }): string[];
log(text: string): void;
- getMemoryUsage?(): number;
args(): string[];
getExecutingFilePath(): string;
+ getWorkspaceRoot(): string;
exit(exitCode?: number): void;
readDirectory(path: string, extension?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray, depth?: number): string[];
+ getAccessibleFileSystemEntries(dirname: string): ts.FileSystemEntries;
tryEnableSourceMapsForHost?(): void;
getEnvironmentVariable?(name: string): string;
+ getMemoryUsage?(): number;
}
- export let IO: Io;
+
+ export let IO: IO;
// harness always uses one kind of new line
// But note that `parseTestData` in `fourslash.ts` uses "\n"
@@ -528,250 +536,437 @@ namespace Harness {
// Root for file paths that are stored in a virtual file system
export const virtualFileSystemRoot = "/";
- namespace IOImpl {
- export namespace Node {
- declare const require: any;
- let fs: any, pathModule: any;
- if (require) {
- fs = require("fs");
- pathModule = require("path");
+ function createNodeIO(): IO {
+ let fs: any, pathModule: any;
+ if (require) {
+ fs = require("fs");
+ pathModule = require("path");
+ }
+ else {
+ fs = pathModule = {};
+ }
+
+ function deleteFile(path: string) {
+ try {
+ fs.unlinkSync(path);
}
- else {
- fs = pathModule = {};
- }
-
- export const resolvePath = (path: string) => ts.sys.resolvePath(path);
- export const getCurrentDirectory = () => ts.sys.getCurrentDirectory();
- export const newLine = () => harnessNewLine;
- export const useCaseSensitiveFileNames = () => ts.sys.useCaseSensitiveFileNames;
- export const args = () => ts.sys.args;
- export const getExecutingFilePath = () => ts.sys.getExecutingFilePath();
- export const exit = (exitCode: number) => ts.sys.exit(exitCode);
- export const getDirectories: typeof IO.getDirectories = path => ts.sys.getDirectories(path);
-
- export const readFile: typeof IO.readFile = path => ts.sys.readFile(path);
- export const writeFile: typeof IO.writeFile = (path, content) => ts.sys.writeFile(path, content);
- export const fileExists: typeof IO.fileExists = fs.existsSync;
- export const log: typeof IO.log = s => console.log(s);
- export const getEnvironmentVariable: typeof IO.getEnvironmentVariable = name => ts.sys.getEnvironmentVariable(name);
-
- export function tryEnableSourceMapsForHost() {
- if (ts.sys.tryEnableSourceMapsForHost) {
- ts.sys.tryEnableSourceMapsForHost();
+ catch { /*ignore*/ }
+ }
+
+ function directoryName(path: string) {
+ const dirPath = pathModule.dirname(path);
+ // Node will just continue to repeat the root path, rather than return null
+ return dirPath === path ? undefined : dirPath;
+ }
+
+ function listFiles(path: string, spec: RegExp, options?: { recursive?: boolean }) {
+ options = options || {};
+
+ function filesInFolder(folder: string): string[] {
+ let paths: string[] = [];
+
+ for (const file of fs.readdirSync(folder)) {
+ const pathToFile = pathModule.join(folder, file);
+ const stat = fs.statSync(pathToFile);
+ if (options.recursive && stat.isDirectory()) {
+ paths = paths.concat(filesInFolder(pathToFile));
+ }
+ else if (stat.isFile() && (!spec || file.match(spec))) {
+ paths.push(pathToFile);
+ }
}
+
+ return paths;
}
- export const readDirectory: typeof IO.readDirectory = (path, extension, exclude, include, depth) => ts.sys.readDirectory(path, extension, exclude, include, depth);
- export function createDirectory(path: string) {
- if (!directoryExists(path)) {
- fs.mkdirSync(path);
+ return filesInFolder(path);
+ }
+
+ function getAccessibleFileSystemEntries(dirname: string): ts.FileSystemEntries {
+ try {
+ const entries: string[] = fs.readdirSync(dirname || ".").sort(ts.sys.useCaseSensitiveFileNames ? ts.compareStringsCaseSensitive : ts.compareStringsCaseInsensitive);
+ const files: string[] = [];
+ const directories: string[] = [];
+ for (const entry of entries) {
+ if (entry === "." || entry === "..") continue;
+ const name = vpath.combine(dirname, entry);
+ try {
+ const stat = fs.statSync(name);
+ if (!stat) continue;
+ if (stat.isFile()) {
+ files.push(entry);
+ }
+ else if (stat.isDirectory()) {
+ directories.push(entry);
+ }
+ }
+ catch { /*ignore*/ }
}
+ return { files, directories };
+ }
+ catch (e) {
+ return { files: [], directories: [] };
}
+ }
- export function deleteFile(path: string) {
- try {
- fs.unlinkSync(path);
+ function createDirectory(path: string) {
+ try {
+ fs.mkdirSync(path);
+ }
+ catch (e) {
+ if (e.code === "ENOENT") {
+ createDirectory(vpath.dirname(path));
+ createDirectory(path);
+ }
+ else if (!ts.sys.directoryExists(path)) {
+ throw e;
}
- catch { /*ignore*/ }
}
+ }
- export function directoryExists(path: string): boolean {
- return fs.existsSync(path) && fs.statSync(path).isDirectory();
- }
+ return {
+ newLine: () => harnessNewLine,
+ getCurrentDirectory: () => ts.sys.getCurrentDirectory(),
+ useCaseSensitiveFileNames: () => ts.sys.useCaseSensitiveFileNames,
+ resolvePath: (path: string) => ts.sys.resolvePath(path),
+ getFileSize: (path: string) => ts.sys.getFileSize(path),
+ readFile: path => ts.sys.readFile(path),
+ writeFile: (path, content) => ts.sys.writeFile(path, content),
+ directoryName,
+ getDirectories: path => ts.sys.getDirectories(path),
+ createDirectory,
+ fileExists: path => ts.sys.fileExists(path),
+ directoryExists: path => ts.sys.directoryExists(path),
+ deleteFile,
+ listFiles,
+ log: s => console.log(s),
+ args: () => ts.sys.args,
+ getExecutingFilePath: () => ts.sys.getExecutingFilePath(),
+ getWorkspaceRoot: () => vpath.resolve(__dirname, "../.."),
+ exit: exitCode => ts.sys.exit(exitCode),
+ readDirectory: (path, extension, exclude, include, depth) => ts.sys.readDirectory(path, extension, exclude, include, depth),
+ getAccessibleFileSystemEntries,
+ tryEnableSourceMapsForHost: () => ts.sys.tryEnableSourceMapsForHost && ts.sys.tryEnableSourceMapsForHost(),
+ getMemoryUsage: () => ts.sys.getMemoryUsage && ts.sys.getMemoryUsage(),
+ getEnvironmentVariable: name => ts.sys.getEnvironmentVariable(name),
+ };
+ }
- export function directoryName(path: string) {
- const dirPath = pathModule.dirname(path);
- // Node will just continue to repeat the root path, rather than return null
- return dirPath === path ? undefined : dirPath;
- }
+ interface URL {
+ hash: string;
+ host: string;
+ hostname: string;
+ href: string;
+ password: string;
+ pathname: string;
+ port: string;
+ protocol: string;
+ search: string;
+ username: string;
+ toString(): string;
+ }
- export let listFiles: typeof IO.listFiles = (path, spec?, options?) => {
- options = options || {};
+ declare var URL: {
+ prototype: URL;
+ new(url: string, base?: string | URL): URL;
+ };
- function filesInFolder(folder: string): string[] {
- let paths: string[] = [];
+ function createBrowserIO(): IO {
+ const serverRoot = new URL("http://localhost:8888/");
- for (const file of fs.readdirSync(folder)) {
- const pathToFile = pathModule.join(folder, file);
- const stat = fs.statSync(pathToFile);
- if (options.recursive && stat.isDirectory()) {
- paths = paths.concat(filesInFolder(pathToFile));
- }
- else if (stat.isFile() && (!spec || file.match(spec))) {
- paths.push(pathToFile);
+ class HttpHeaders extends collections.SortedMap {
+ constructor(template?: Record) {
+ super(ts.compareStringsCaseInsensitive);
+ if (template) {
+ for (const key in template) {
+ if (ts.hasProperty(template, key)) {
+ this.set(key, template[key]);
}
}
-
- return paths;
}
+ }
- return filesInFolder(path);
- };
+ public static combine(left: HttpHeaders | undefined, right: HttpHeaders | undefined): HttpHeaders {
+ if (!left && !right) return undefined;
+ const headers = new HttpHeaders();
+ if (left) left.forEach((value, key) => { headers.set(key, value); });
+ if (right) right.forEach((value, key) => { headers.set(key, value); });
+ return headers;
+ }
+
+ public has(key: string) {
+ return super.has(key.toLowerCase());
+ }
- export let getMemoryUsage: typeof IO.getMemoryUsage = () => {
- if (global.gc) {
- global.gc();
+ public get(key: string) {
+ return super.get(key.toLowerCase());
+ }
+
+ public set(key: string, value: string | string[]) {
+ return super.set(key.toLowerCase(), value);
+ }
+
+ public delete(key: string) {
+ return super.delete(key.toLowerCase());
+ }
+
+ public writeRequestHeaders(xhr: XMLHttpRequest) {
+ this.forEach((values, key) => {
+ if (key === "access-control-allow-origin" || key === "content-length") return;
+ const value = Array.isArray(values) ? values.join(",") : values;
+ if (key === "content-type") {
+ xhr.overrideMimeType(value);
+ return;
+ }
+ xhr.setRequestHeader(key, value);
+ });
+ }
+
+ public static readResponseHeaders(xhr: XMLHttpRequest): HttpHeaders {
+ const allHeaders = xhr.getAllResponseHeaders();
+ const headers = new HttpHeaders();
+ for (const header of allHeaders.split(/\r\n/g)) {
+ const colonIndex = header.indexOf(":");
+ if (colonIndex >= 0) {
+ const key = header.slice(0, colonIndex).trim();
+ const value = header.slice(colonIndex + 1).trim();
+ const values = value.split(",");
+ headers.set(key, values.length > 1 ? values : value);
+ }
}
- return process.memoryUsage().heapUsed;
- };
+ return headers;
+ }
}
- export namespace Network {
- const serverRoot = "http://localhost:8888/";
+ class HttpContent {
+ public headers: HttpHeaders;
+ public content: string;
- export const newLine = () => harnessNewLine;
- export const useCaseSensitiveFileNames = () => false;
- export const getCurrentDirectory = () => "";
- export const args = () => [];
- export const getExecutingFilePath = () => "";
- export const exit = ts.noop;
- export const getDirectories = () => [];
+ constructor(headers: HttpHeaders | Record, content: string) {
+ this.headers = headers instanceof HttpHeaders ? headers : new HttpHeaders(headers);
+ this.content = content;
+ }
- export let log = (s: string) => console.log(s);
+ public static fromMediaType(mediaType: string, content: string) {
+ return new HttpContent({ "Content-Type": mediaType }, content);
+ }
- namespace Http {
- function waitForXHR(xhr: XMLHttpRequest) {
- while (xhr.readyState !== 4) { } // tslint:disable-line no-empty
- return { status: xhr.status, responseText: xhr.responseText };
- }
+ public static text(content: string) {
+ return HttpContent.fromMediaType("text/plain", content);
+ }
- /// Ask the server to use node's path.resolve to resolve the given path
+ public static json(content: object) {
+ return HttpContent.fromMediaType("application/json", JSON.stringify(content));
+ }
- export interface XHRResponse {
- status: number;
- responseText: string;
+ public static readResponseContent(xhr: XMLHttpRequest) {
+ if (typeof xhr.responseText === "string") {
+ return new HttpContent({
+ "Content-Type": xhr.getResponseHeader("Content-Type") || undefined,
+ "Content-Length": xhr.getResponseHeader("Content-Length") || undefined
+ }, xhr.responseText);
}
+ return undefined;
+ }
- /// Ask the server for the contents of the file at the given URL via a simple GET request
- export function getFileFromServerSync(url: string): XHRResponse {
- const xhr = new XMLHttpRequest();
- try {
- xhr.open("GET", url, /*async*/ false);
- xhr.send();
- }
- catch (e) {
- return { status: 404, responseText: undefined };
- }
+ public writeRequestHeaders(xhr: XMLHttpRequest) {
+ this.headers.writeRequestHeaders(xhr);
+ }
+ }
- return waitForXHR(xhr);
- }
+ class HttpRequestMessage {
+ public method: string;
+ public url: URL;
+ public headers: HttpHeaders;
+ public content?: HttpContent;
- /// Submit a POST request to the server to do the given action (ex WRITE, DELETE) on the provided URL
- export function writeToServerSync(url: string, action: string, contents?: string): XHRResponse {
- const xhr = new XMLHttpRequest();
- try {
- const actionMsg = "?action=" + action;
- xhr.open("POST", url + actionMsg, /*async*/ false);
- xhr.setRequestHeader("Access-Control-Allow-Origin", "*");
- xhr.send(contents);
- }
- catch (e) {
- log(`XHR Error: ${e}`);
- return { status: 500, responseText: undefined };
- }
+ constructor(method: string, url: string | URL, headers?: HttpHeaders | Record, content?: HttpContent) {
+ this.method = method;
+ this.url = typeof url === "string" ? new URL(url) : url;
+ this.headers = headers instanceof HttpHeaders ? headers : new HttpHeaders(headers);
+ this.content = content;
+ }
- return waitForXHR(xhr);
- }
+ public static options(url: string | URL) {
+ return new HttpRequestMessage("OPTIONS", url);
}
- export function createDirectory() {
- // Do nothing (?)
+ public static head(url: string | URL) {
+ return new HttpRequestMessage("HEAD", url);
}
- export function deleteFile(path: string) {
- Http.writeToServerSync(serverRoot + path, "DELETE");
+ public static get(url: string | URL) {
+ return new HttpRequestMessage("GET", url);
}
- export function directoryExists(): boolean {
- return false;
+ public static delete(url: string | URL) {
+ return new HttpRequestMessage("DELETE", url);
}
- function directoryNameImpl(path: string) {
- let dirPath = path;
- // root of the server
- if (dirPath.match(/localhost:\d+$/) || dirPath.match(/localhost:\d+\/$/)) {
- dirPath = undefined;
- // path + fileName
- }
- else if (dirPath.indexOf(".") === -1) {
- dirPath = dirPath.substring(0, dirPath.lastIndexOf("/"));
- // path
- }
- else {
- // strip any trailing slash
- if (dirPath.match(/.*\/$/)) {
- dirPath = dirPath.substring(0, dirPath.length - 2);
- }
- dirPath = dirPath.substring(0, dirPath.lastIndexOf("/"));
- }
+ public static put(url: string | URL, content: HttpContent) {
+ return new HttpRequestMessage("PUT", url, /*headers*/ undefined, content);
+ }
- return dirPath;
+ public static post(url: string | URL, content: HttpContent) {
+ return new HttpRequestMessage("POST", url, /*headers*/ undefined, content);
}
- export let directoryName: typeof IO.directoryName = Utils.memoize(directoryNameImpl, path => path);
- export function resolvePath(path: string) {
- const response = Http.getFileFromServerSync(serverRoot + path + "?resolve=true");
- if (response.status === 200) {
- return response.responseText;
- }
- else {
- return undefined;
+ public writeRequestHeaders(xhr: XMLHttpRequest) {
+ this.headers.writeRequestHeaders(xhr);
+ if (this.content) {
+ this.content.writeRequestHeaders(xhr);
}
}
+ }
- export function fileExists(path: string): boolean {
- const response = Http.getFileFromServerSync(serverRoot + path);
- return response.status === 200;
+ class HttpResponseMessage {
+ public statusCode: number;
+ public statusMessage: string;
+ public headers: HttpHeaders;
+ public content?: HttpContent;
+
+ constructor(statusCode: number, statusMessage: string, headers?: HttpHeaders | Record, content?: HttpContent) {
+ this.statusCode = statusCode;
+ this.statusMessage = statusMessage;
+ this.headers = headers instanceof HttpHeaders ? headers : new HttpHeaders(headers);
+ this.content = content;
}
- export const listFiles = Utils.memoize((path: string, spec?: RegExp, options?: { recursive?: boolean }): string[] => {
- const response = Http.getFileFromServerSync(serverRoot + path);
- if (response.status === 200) {
- let results = response.responseText.split(",");
- if (spec) {
- results = results.filter(file => spec.test(file));
- }
- if (options && !options.recursive) {
- results = results.filter(file => (ts.getDirectoryPath(ts.normalizeSlashes(file)) === path));
- }
- return results;
- }
- else {
- return [""];
- }
- }, (path: string, spec?: RegExp, options?: { recursive?: boolean }) => `${path}|${spec}|${options ? options.recursive : undefined}`);
+ public static notFound(): HttpResponseMessage {
+ return new HttpResponseMessage(404, "Not Found");
+ }
- export function readFile(file: string): string | undefined {
- const response = Http.getFileFromServerSync(serverRoot + file);
- if (response.status === 200) {
- return response.responseText;
- }
- else {
- return undefined;
- }
+ public static hasSuccessStatusCode(response: HttpResponseMessage) {
+ return response.statusCode === 304 || (response.statusCode >= 200 && response.statusCode < 300);
+ }
+
+ public static readResponseMessage(xhr: XMLHttpRequest) {
+ return new HttpResponseMessage(
+ xhr.status,
+ xhr.statusText,
+ HttpHeaders.readResponseHeaders(xhr),
+ HttpContent.readResponseContent(xhr));
+ }
+ }
+
+ function send(request: HttpRequestMessage): HttpResponseMessage {
+ const xhr = new XMLHttpRequest();
+ try {
+ xhr.open(request.method, request.url.toString(), /*async*/ false);
+ request.writeRequestHeaders(xhr);
+ xhr.setRequestHeader("Access-Control-Allow-Origin", "*");
+ xhr.send(request.content && request.content.content);
+ while (xhr.readyState !== 4); // block until ready
+ return HttpResponseMessage.readResponseMessage(xhr);
+ }
+ catch (e) {
+ return HttpResponseMessage.notFound();
}
+ }
- export function writeFile(path: string, contents: string) {
- Http.writeToServerSync(serverRoot + path, "WRITE", contents);
+ let caseSensitivity: "CI" | "CS" | undefined;
+
+ function useCaseSensitiveFileNames() {
+ if (!caseSensitivity) {
+ const response = send(HttpRequestMessage.options(new URL("*", serverRoot)));
+ const xCaseSensitivity = response.headers.get("X-Case-Sensitivity");
+ caseSensitivity = xCaseSensitivity === "CS" ? "CS" : "CI";
}
+ return caseSensitivity === "CS";
+ }
- export function readDirectory(path: string, extension?: string[], exclude?: string[], include?: string[], depth?: number) {
- const fs = new Utils.VirtualFileSystem(path, useCaseSensitiveFileNames());
- for (const file of listFiles(path)) {
- fs.addFile(file);
+ function resolvePath(path: string) {
+ const response = send(HttpRequestMessage.post(new URL("/api/resolve", serverRoot), HttpContent.text(path)));
+ return HttpResponseMessage.hasSuccessStatusCode(response) && response.content ? response.content.content : undefined;
+ }
+
+ function getFileSize(path: string): number {
+ const response = send(HttpRequestMessage.head(new URL(path, serverRoot)));
+ return HttpResponseMessage.hasSuccessStatusCode(response) ? +response.headers.get("Content-Length").toString() : 0;
+ }
+
+ function readFile(path: string): string | undefined {
+ const response = send(HttpRequestMessage.get(new URL(path, serverRoot)));
+ return HttpResponseMessage.hasSuccessStatusCode(response) && response.content ? response.content.content : undefined;
+ }
+
+ function writeFile(path: string, contents: string) {
+ send(HttpRequestMessage.put(new URL(path, serverRoot), HttpContent.text(contents)));
+ }
+
+ function fileExists(path: string): boolean {
+ const response = send(HttpRequestMessage.head(new URL(path, serverRoot)));
+ return HttpResponseMessage.hasSuccessStatusCode(response);
+ }
+
+ function directoryExists(path: string): boolean {
+ const response = send(HttpRequestMessage.post(new URL("/api/directoryExists", serverRoot), HttpContent.text(path)));
+ return hasJsonContent(response) && JSON.parse(response.content.content) as boolean;
+ }
+
+ function deleteFile(path: string) {
+ send(HttpRequestMessage.delete(new URL(path, serverRoot)));
+ }
+
+ function directoryName(path: string) {
+ const url = new URL(path, serverRoot);
+ return ts.getDirectoryPath(ts.normalizeSlashes(url.pathname || "/"));
+ }
+
+ function listFiles(dirname: string, spec?: RegExp, options?: { recursive?: boolean }): string[] {
+ if (spec || (options && !options.recursive)) {
+ let results = IO.listFiles(dirname);
+ if (spec) {
+ results = results.filter(file => spec.test(file));
}
- return ts.matchFiles(path, extension, exclude, include, useCaseSensitiveFileNames(), getCurrentDirectory(), depth, path => {
- const entry = fs.traversePath(path);
- if (entry && entry.isDirectory()) {
- return {
- files: ts.map(entry.getFiles(), f => f.name),
- directories: ts.map(entry.getDirectories(), d => d.name)
- };
- }
- return { files: [], directories: [] };
- });
+ if (options && !options.recursive) {
+ results = results.filter(file => ts.getDirectoryPath(ts.normalizeSlashes(file)) === dirname);
+ }
+ return results;
}
+
+ const response = send(HttpRequestMessage.post(new URL("/api/listFiles", serverRoot), HttpContent.text(dirname)));
+ return hasJsonContent(response) ? JSON.parse(response.content.content) : [];
+ }
+
+ function readDirectory(path: string, extension?: string[], exclude?: string[], include?: string[], depth?: number) {
+ return ts.matchFiles(path, extension, exclude, include, useCaseSensitiveFileNames(), "", depth, getAccessibleFileSystemEntries);
}
+
+ function getAccessibleFileSystemEntries(dirname: string): ts.FileSystemEntries {
+ const response = send(HttpRequestMessage.post(new URL("/api/getAccessibleFileSystemEntries", serverRoot), HttpContent.text(dirname)));
+ return hasJsonContent(response) ? JSON.parse(response.content.content) : { files: [], directories: [] };
+ }
+
+ function hasJsonContent(response: HttpResponseMessage): response is HttpResponseMessage & { content: HttpContent } {
+ return HttpResponseMessage.hasSuccessStatusCode(response)
+ && !!response.content
+ && /^application\/json(;.*)$/.test("" + response.content.headers.get("Content-Type"));
+ }
+
+ return {
+ newLine: () => harnessNewLine,
+ getCurrentDirectory: () => "",
+ useCaseSensitiveFileNames,
+ resolvePath,
+ getFileSize,
+ readFile,
+ writeFile,
+ directoryName: Utils.memoize(directoryName, path => path),
+ getDirectories: () => [],
+ createDirectory: () => {}, // tslint:disable-line no-empty
+ fileExists,
+ directoryExists,
+ deleteFile,
+ listFiles: Utils.memoize(listFiles, (path, spec, options) => `${path}|${spec}|${options ? options.recursive === true : true}`),
+ log: s => console.log(s),
+ args: () => [],
+ getExecutingFilePath: () => "",
+ exit: () => {}, // tslint:disable-line no-empty
+ readDirectory,
+ getAccessibleFileSystemEntries,
+ getWorkspaceRoot: () => "/"
+ };
}
export function mockHash(s: string): string {
@@ -781,16 +976,20 @@ namespace Harness {
const environment = Utils.getExecutionEnvironment();
switch (environment) {
case Utils.ExecutionEnvironment.Node:
- IO = IOImpl.Node;
+ IO = createNodeIO();
break;
case Utils.ExecutionEnvironment.Browser:
- IO = IOImpl.Network;
+ IO = createBrowserIO();
break;
default:
throw new Error(`Unknown value '${environment}' for ExecutionEnvironment.`);
}
}
+if (Harness.IO.tryEnableSourceMapsForHost && /^development$/i.test(Harness.IO.getEnvironmentVariable("NODE_ENV"))) {
+ Harness.IO.tryEnableSourceMapsForHost();
+}
+
namespace Harness {
export const libFolder = "built/local/";
const tcServicesFileName = ts.combinePaths(libFolder, Utils.getExecutionEnvironment() === Utils.ExecutionEnvironment.Browser ? "typescriptServicesInBrowserTest.js" : "typescriptServices.js");
@@ -860,19 +1059,12 @@ namespace Harness {
return result;
}
- const carriageReturnLineFeed = "\r\n";
- const lineFeed = "\n";
-
export const defaultLibFileName = "lib.d.ts";
export const es2015DefaultLibFileName = "lib.es2015.d.ts";
// Cache of lib files from "built/local"
let libFileNameSourceFileMap: ts.Map | undefined;
- // Cache of lib files from "tests/lib/"
- const testLibFileNameSourceFileMap = ts.createMap();
- const es6TestLibFileNameSourceFileMap = ts.createMap();
-
export function getDefaultLibrarySourceFile(fileName = defaultLibFileName): ts.SourceFile {
if (!isDefaultLibraryFile(fileName)) {
return undefined;
@@ -914,155 +1106,6 @@ namespace Harness {
return fileName;
}
- export function createCompilerHost(
- inputFiles: TestFile[],
- writeFile: (fn: string, contents: string, writeByteOrderMark: boolean) => void,
- scriptTarget: ts.ScriptTarget,
- useCaseSensitiveFileNames: boolean,
- // the currentDirectory is needed for rwcRunner to passed in specified current directory to compiler host
- currentDirectory: string,
- newLineKind?: ts.NewLineKind,
- libFiles?: string): ts.CompilerHost {
-
- // Local get canonical file name function, that depends on passed in parameter for useCaseSensitiveFileNames
- const getCanonicalFileName = ts.createGetCanonicalFileName(useCaseSensitiveFileNames);
-
- /** Maps a symlink name to a realpath. Used only for exposing `realpath`. */
- const realPathMap = ts.createMap();
- /**
- * Maps a file name to a source file.
- * This will have a different SourceFile for every symlink pointing to that file;
- * if the program resolves realpaths then symlink entries will be ignored.
- */
- const fileMap = ts.createMap();
- for (const file of inputFiles) {
- if (file.content !== undefined) {
- const fileName = ts.normalizePath(file.unitName);
- const path = ts.toPath(file.unitName, currentDirectory, getCanonicalFileName);
- if (file.fileOptions && file.fileOptions.symlink) {
- const links = file.fileOptions.symlink.split(",");
- for (const link of links) {
- const linkPath = ts.toPath(link, currentDirectory, getCanonicalFileName);
- realPathMap.set(linkPath, fileName);
- // Create a different SourceFile for every symlink.
- const sourceFile = createSourceFileAndAssertInvariants(linkPath, file.content, scriptTarget);
- fileMap.set(linkPath, sourceFile);
- }
- }
- const sourceFile = createSourceFileAndAssertInvariants(fileName, file.content, scriptTarget);
- fileMap.set(path, sourceFile);
- }
- }
-
- if (libFiles) {
- // Because @libFiles don't change between execution. We would cache the result of the files and reuse it to speed help compilation
- for (const fileName of libFiles.split(",")) {
- const libFileName = "tests/lib/" + fileName;
-
- if (scriptTarget <= ts.ScriptTarget.ES5) {
- if (!testLibFileNameSourceFileMap.get(libFileName)) {
- testLibFileNameSourceFileMap.set(libFileName, createSourceFileAndAssertInvariants(libFileName, IO.readFile(libFileName), scriptTarget));
- }
- }
- else {
- if (!es6TestLibFileNameSourceFileMap.get(libFileName)) {
- es6TestLibFileNameSourceFileMap.set(libFileName, createSourceFileAndAssertInvariants(libFileName, IO.readFile(libFileName), scriptTarget));
- }
- }
- }
- }
-
- function getSourceFile(fileName: string) {
- fileName = ts.normalizePath(fileName);
- const fromFileMap = fileMap.get(toPath(fileName));
- if (fromFileMap) {
- return fromFileMap;
- }
- else if (fileName === fourslashFileName) {
- const tsFn = "tests/cases/fourslash/" + fourslashFileName;
- fourslashSourceFile = fourslashSourceFile || createSourceFileAndAssertInvariants(tsFn, IO.readFile(tsFn), scriptTarget);
- return fourslashSourceFile;
- }
- else if (ts.startsWith(fileName, "tests/lib/")) {
- return scriptTarget <= ts.ScriptTarget.ES5 ? testLibFileNameSourceFileMap.get(fileName) : es6TestLibFileNameSourceFileMap.get(fileName);
- }
- else {
- // Don't throw here -- the compiler might be looking for a test that actually doesn't exist as part of the TC
- // Return if it is other library file, otherwise return undefined
- return getDefaultLibrarySourceFile(fileName);
- }
- }
-
- const newLine =
- newLineKind === ts.NewLineKind.CarriageReturnLineFeed ? carriageReturnLineFeed :
- newLineKind === ts.NewLineKind.LineFeed ? lineFeed :
- IO.newLine();
-
- function toPath(fileName: string): ts.Path {
- return ts.toPath(fileName, currentDirectory, getCanonicalFileName);
- }
-
- return {
- getCurrentDirectory: () => currentDirectory,
- getSourceFile,
- getDefaultLibFileName,
- writeFile,
- getCanonicalFileName,
- useCaseSensitiveFileNames: () => useCaseSensitiveFileNames,
- getNewLine: () => newLine,
- fileExists: fileName => fileMap.has(toPath(fileName)),
- readFile(fileName: string): string | undefined {
- const file = fileMap.get(toPath(fileName));
- if (ts.endsWith(fileName, "json")) {
- // strip comments
- return file.getText();
- }
- return file.text;
- },
- realpath: (fileName: string): ts.Path => {
- const path = toPath(fileName);
- return (realPathMap.get(path) as ts.Path) || path;
- },
- directoryExists: dir => {
- let path = ts.toPath(dir, currentDirectory, getCanonicalFileName);
- // Strip trailing /, which may exist if the path is a drive root
- if (path[path.length - 1] === "/") {
- path = path.substr(0, path.length - 1);
- }
- return mapHasFileInDirectory(path, fileMap);
- },
- getDirectories: d => {
- const path = ts.toPath(d, currentDirectory, getCanonicalFileName);
- const result: string[] = [];
- ts.forEachKey(fileMap, key => {
- if (key.indexOf(path) === 0 && key.lastIndexOf("/") > path.length) {
- let dirName = key.substr(path.length, key.indexOf("/", path.length + 1) - path.length);
- if (dirName[0] === "/") {
- dirName = dirName.substr(1);
- }
- if (result.indexOf(dirName) < 0) {
- result.push(dirName);
- }
- }
- });
- return result;
- }
- };
- }
-
- function mapHasFileInDirectory(directoryPath: ts.Path, map: ts.Map<{}>): boolean {
- if (!map) {
- return false;
- }
- let exists = false;
- ts.forEachKey(map, fileName => {
- if (!exists && ts.startsWith(fileName, directoryPath) && fileName[directoryPath.length] === "/") {
- exists = true;
- }
- });
- return exists;
- }
-
interface HarnessOptions {
useCaseSensitiveFileNames?: boolean;
includeBuiltFile?: string;
@@ -1148,18 +1191,13 @@ namespace Harness {
fileOptions?: any;
}
- export interface CompilationOutput {
- result: CompilerResult;
- options: ts.CompilerOptions & HarnessOptions;
- }
-
export function compileFiles(
inputFiles: TestFile[],
otherFiles: TestFile[],
harnessSettings: TestCaseParser.CompilerSettings,
compilerOptions: ts.CompilerOptions,
// Current directory is needed for rwcRunner to be able to use currentDirectory defined in json file
- currentDirectory: string): CompilationOutput {
+ currentDirectory: string): compiler.CompilationResult {
const options: ts.CompilerOptions & HarnessOptions = compilerOptions ? ts.cloneCompilerOptions(compilerOptions) : { noResolve: false };
options.target = options.target || ts.ScriptTarget.ES3;
options.newLine = options.newLine || ts.NewLineKind.CarriageReturnLineFeed;
@@ -1167,7 +1205,7 @@ namespace Harness {
options.skipDefaultLibCheck = typeof options.skipDefaultLibCheck === "undefined" ? true : options.skipDefaultLibCheck;
if (typeof currentDirectory === "undefined") {
- currentDirectory = IO.getCurrentDirectory();
+ currentDirectory = vfs.srcFolder;
}
// Parse settings
@@ -1178,58 +1216,26 @@ namespace Harness {
options.rootDirs = ts.map(options.rootDirs, d => ts.getNormalizedAbsolutePath(d, currentDirectory));
}
- const useCaseSensitiveFileNames = options.useCaseSensitiveFileNames !== undefined ? options.useCaseSensitiveFileNames : IO.useCaseSensitiveFileNames();
- const programFiles: TestFile[] = inputFiles.slice();
+ const useCaseSensitiveFileNames = options.useCaseSensitiveFileNames !== undefined ? options.useCaseSensitiveFileNames : true;
+ const programFileNames = inputFiles.map(file => file.unitName);
+
// Files from built\local that are requested by test "@includeBuiltFiles" to be in the context.
// Treat them as library files, so include them in build, but not in baselines.
if (options.includeBuiltFile) {
- const builtFileName = ts.combinePaths(libFolder, options.includeBuiltFile);
- const builtFile: TestFile = {
- unitName: builtFileName,
- content: normalizeLineEndings(IO.readFile(builtFileName), IO.newLine()),
- };
- programFiles.push(builtFile);
+ programFileNames.push(vpath.combine(vfs.builtFolder, options.includeBuiltFile));
}
- const fileOutputs: GeneratedFile[] = [];
-
// Files from tests\lib that are requested by "@libFiles"
if (options.libFiles) {
for (const fileName of options.libFiles.split(",")) {
- const libFileName = "tests/lib/" + fileName;
- // Content is undefined here because in createCompilerHost we will create sourceFile for the lib file and cache the result
- programFiles.push({ unitName: libFileName, content: undefined });
+ programFileNames.push(vpath.combine(vfs.testLibFolder, fileName));
}
}
-
- const programFileNames = programFiles.map(file => file.unitName);
-
- const compilerHost = createCompilerHost(
- programFiles.concat(otherFiles),
- (fileName, code, writeByteOrderMark) => fileOutputs.push({ fileName, code, writeByteOrderMark }),
- options.target,
- useCaseSensitiveFileNames,
- currentDirectory,
- options.newLine,
- options.libFiles);
-
- let traceResults: string[];
- if (options.traceResolution) {
- traceResults = [];
- compilerHost.trace = text => traceResults.push(text);
- }
- else {
- compilerHost.directoryExists = () => true; // This only visibly affects resolution traces, so to save time we always return true where possible
- }
- const program = ts.createProgram(programFileNames, options, compilerHost);
-
- const emitResult = program.emit();
-
- const errors = ts.getPreEmitDiagnostics(program);
-
- const result = new CompilerResult(fileOutputs, errors, program, IO.getCurrentDirectory(), emitResult.sourceMaps, traceResults);
- return { result, options };
+ const docs = inputFiles.concat(otherFiles).map(documents.TextDocument.fromTestFile);
+ const fs = vfs.createFromFileSystem(IO, !useCaseSensitiveFileNames, { documents: docs, cwd: currentDirectory });
+ const host = new fakes.CompilerHost(fs, options);
+ return compiler.compileFiles(host, programFileNames, options);
}
export interface DeclarationCompilationContext {
@@ -1240,45 +1246,43 @@ namespace Harness {
currentDirectory: string;
}
- export function prepareDeclarationCompilationContext(inputFiles: TestFile[],
- otherFiles: TestFile[],
- result: CompilerResult,
+ export function prepareDeclarationCompilationContext(inputFiles: ReadonlyArray,
+ otherFiles: ReadonlyArray,
+ result: compiler.CompilationResult,
harnessSettings: TestCaseParser.CompilerSettings & HarnessOptions,
options: ts.CompilerOptions,
// Current directory is needed for rwcRunner to be able to use currentDirectory defined in json file
currentDirectory: string): DeclarationCompilationContext | undefined {
- if (result.errors.length === 0) {
- if (options.declaration) {
- if (options.emitDeclarationOnly) {
- if (result.files.length > 0 || result.declFilesCode.length === 0) {
- throw new Error("Only declaration files should be generated when emitDeclarationOnly:true");
- }
- }
- else if (result.declFilesCode.length !== result.files.length) {
- throw new Error("There were no errors and declFiles generated did not match number of js files generated");
+ if (options.declaration && result.diagnostics.length === 0) {
+ if (options.emitDeclarationOnly) {
+ if (result.js.size > 0 || result.dts.size === 0) {
+ throw new Error("Only declaration files should be generated when emitDeclarationOnly:true");
}
}
+ else if (result.dts.size !== result.js.size) {
+ throw new Error("There were no errors and declFiles generated did not match number of js files generated");
+ }
}
const declInputFiles: TestFile[] = [];
const declOtherFiles: TestFile[] = [];
// if the .d.ts is non-empty, confirm it compiles correctly as well
- if (options.declaration && result.errors.length === 0 && result.declFilesCode.length > 0) {
+ if (options.declaration && result.diagnostics.length === 0 && result.dts.size > 0) {
ts.forEach(inputFiles, file => addDtsFile(file, declInputFiles));
ts.forEach(otherFiles, file => addDtsFile(file, declOtherFiles));
return { declInputFiles, declOtherFiles, harnessSettings, options, currentDirectory: currentDirectory || harnessSettings.currentDirectory };
}
function addDtsFile(file: TestFile, dtsFiles: TestFile[]) {
- if (isDTS(file.unitName)) {
+ if (vpath.isDeclaration(file.unitName)) {
dtsFiles.push(file);
}
- else if (isTS(file.unitName)) {
+ else if (vpath.isTypeScript(file.unitName)) {
const declFile = findResultCodeFile(file.unitName);
- if (declFile && !findUnit(declFile.fileName, declInputFiles) && !findUnit(declFile.fileName, declOtherFiles)) {
- dtsFiles.push({ unitName: declFile.fileName, content: declFile.code });
+ if (declFile && !findUnit(declFile.file, declInputFiles) && !findUnit(declFile.file, declOtherFiles)) {
+ dtsFiles.push({ unitName: declFile.file, content: utils.removeByteOrderMark(declFile.text) });
}
}
}
@@ -1291,7 +1295,7 @@ namespace Harness {
const outFile = options.outFile || options.out;
if (!outFile) {
if (options.outDir) {
- let sourceFilePath = ts.getNormalizedAbsolutePath(sourceFile.fileName, result.currentDirectoryForProgram);
+ let sourceFilePath = ts.getNormalizedAbsolutePath(sourceFile.fileName, result.vfs.cwd());
sourceFilePath = sourceFilePath.replace(result.program.getCommonSourceDirectory(), "");
sourceFileName = ts.combinePaths(options.outDir, sourceFilePath);
}
@@ -1305,8 +1309,7 @@ namespace Harness {
}
const dTsFileName = ts.removeFileExtension(sourceFileName) + ts.Extension.Dts;
-
- return ts.forEach(result.declFilesCode, declFile => declFile.fileName === dTsFileName ? declFile : undefined);
+ return result.dts.get(dTsFileName);
}
function findUnit(fileName: string, units: TestFile[]) {
@@ -1320,15 +1323,7 @@ namespace Harness {
}
const { declInputFiles, declOtherFiles, harnessSettings, options, currentDirectory } = context;
const output = compileFiles(declInputFiles, declOtherFiles, harnessSettings, options, currentDirectory);
- return { declInputFiles, declOtherFiles, declResult: output.result };
- }
-
- function normalizeLineEndings(text: string, lineEnding: string): string {
- let normalized = text.replace(/\r\n?/g, "\n");
- if (lineEnding !== "\n") {
- normalized = normalized.replace(/\n/g, lineEnding);
- }
- return normalized;
+ return { declInputFiles, declOtherFiles, declResult: output };
}
export function minimalDiagnosticsToString(diagnostics: ReadonlyArray, pretty?: boolean) {
@@ -1368,7 +1363,7 @@ namespace Harness {
function outputErrorText(error: ts.Diagnostic) {
const message = ts.flattenDiagnosticMessageText(error.messageText, IO.newLine());
- const errLines = RunnerBase.removeFullPaths(message)
+ const errLines = utils.removeTestPathPrefixes(message)
.split("\n")
.map(s => s.length > 0 && s.charAt(s.length - 1) === "\r" ? s.substr(0, s.length - 1) : s)
.filter(s => s.length > 0)
@@ -1386,7 +1381,7 @@ namespace Harness {
}
}
- yield [diagnosticSummaryMarker, minimalDiagnosticsToString(diagnostics, pretty) + IO.newLine() + IO.newLine(), diagnostics.length];
+ yield [diagnosticSummaryMarker, utils.removeTestPathPrefixes(minimalDiagnosticsToString(diagnostics, pretty)) + IO.newLine() + IO.newLine(), diagnostics.length];
// Report global errors
const globalErrors = diagnostics.filter(err => !err.file);
@@ -1401,7 +1396,7 @@ namespace Harness {
// Filter down to the errors in the file
const fileErrors = diagnostics.filter(e => {
const errFn = e.file;
- return errFn && errFn.fileName === inputFile.unitName;
+ return errFn && utils.removeTestPathPrefixes(errFn.fileName) === utils.removeTestPathPrefixes(inputFile.unitName);
});
@@ -1481,7 +1476,7 @@ namespace Harness {
assert.equal(totalErrorsReportedInNonLibraryFiles + numLibraryDiagnostics + numTest262HarnessDiagnostics, diagnostics.length, "total number of errors");
}
- export function doErrorBaseline(baselinePath: string, inputFiles: TestFile[], errors: ts.Diagnostic[], pretty?: boolean) {
+ export function doErrorBaseline(baselinePath: string, inputFiles: ReadonlyArray, errors: ReadonlyArray, pretty?: boolean) {
Baseline.runBaseline(baselinePath.replace(/\.tsx?$/, ".errors.txt"), (): string => {
if (!errors || (errors.length === 0)) {
/* tslint:disable:no-null-keyword */
@@ -1618,30 +1613,26 @@ namespace Harness {
}
typeLines += "\r\n";
}
- yield [checkDuplicatedFileName(unitName, dupeCase), typeLines];
+ yield [checkDuplicatedFileName(unitName, dupeCase), utils.removeTestPathPrefixes(typeLines)];
}
}
}
- function getByteOrderMarkText(file: GeneratedFile): string {
- return file.writeByteOrderMark ? "\u00EF\u00BB\u00BF" : "";
- }
-
- export function doSourcemapBaseline(baselinePath: string, options: ts.CompilerOptions, result: CompilerResult, harnessSettings: TestCaseParser.CompilerSettings) {
+ export function doSourcemapBaseline(baselinePath: string, options: ts.CompilerOptions, result: compiler.CompilationResult, harnessSettings: TestCaseParser.CompilerSettings) {
const declMaps = ts.getAreDeclarationMapsEnabled(options);
if (options.inlineSourceMap) {
- if (result.sourceMaps.length > 0 && !declMaps) {
+ if (result.maps.size > 0 && !declMaps) {
throw new Error("No sourcemap files should be generated if inlineSourceMaps was set.");
}
return;
}
else if (options.sourceMap || declMaps) {
- if (result.sourceMaps.length !== (result.files.length * (declMaps && options.sourceMap ? 2 : 1))) {
+ if (result.maps.size !== (result.js.size * (declMaps && options.sourceMap ? 2 : 1))) {
throw new Error("Number of sourcemap files should be same as js files.");
}
Baseline.runBaseline(baselinePath.replace(/\.tsx?/, ".js.map"), () => {
- if ((options.noEmitOnError && result.errors.length !== 0) || result.sourceMaps.length === 0) {
+ if ((options.noEmitOnError && result.diagnostics.length !== 0) || result.maps.size === 0) {
// We need to return null here or the runBaseLine will actually create a empty file.
// Baselining isn't required here because there is no output.
/* tslint:disable:no-null-keyword */
@@ -1650,17 +1641,17 @@ namespace Harness {
}
let sourceMapCode = "";
- for (const sourceMap of result.sourceMaps) {
+ result.maps.forEach(sourceMap => {
sourceMapCode += fileOutput(sourceMap, harnessSettings);
- }
+ });
return sourceMapCode;
});
}
}
- export function doJsEmitBaseline(baselinePath: string, header: string, options: ts.CompilerOptions, result: CompilerResult, tsConfigFiles: TestFile[], toBeCompiled: TestFile[], otherFiles: TestFile[], harnessSettings: TestCaseParser.CompilerSettings) {
- if (!options.noEmit && !options.emitDeclarationOnly && result.files.length === 0 && result.errors.length === 0) {
+ export function doJsEmitBaseline(baselinePath: string, header: string, options: ts.CompilerOptions, result: compiler.CompilationResult, tsConfigFiles: ReadonlyArray, toBeCompiled: ReadonlyArray, otherFiles: ReadonlyArray, harnessSettings: TestCaseParser.CompilerSettings) {
+ if (!options.noEmit && !options.emitDeclarationOnly && result.js.size === 0 && result.diagnostics.length === 0) {
throw new Error("Expected at least one js file to be emitted or at least one error to be created.");
}
@@ -1677,15 +1668,15 @@ namespace Harness {
}
let jsCode = "";
- for (const file of result.files) {
+ result.js.forEach(file => {
jsCode += fileOutput(file, harnessSettings);
- }
+ });
- if (result.declFilesCode.length > 0) {
+ if (result.dts.size > 0) {
jsCode += "\r\n\r\n";
- for (const declFile of result.declFilesCode) {
+ result.dts.forEach(declFile => {
jsCode += fileOutput(declFile, harnessSettings);
- }
+ });
}
const declFileContext = prepareDeclarationCompilationContext(
@@ -1693,10 +1684,10 @@ namespace Harness {
);
const declFileCompilationResult = compileDeclarationFiles(declFileContext);
- if (declFileCompilationResult && declFileCompilationResult.declResult.errors.length) {
+ if (declFileCompilationResult && declFileCompilationResult.declResult.diagnostics.length) {
jsCode += "\r\n\r\n//// [DtsFileErrors]\r\n";
jsCode += "\r\n\r\n";
- jsCode += getErrorBaseline(tsConfigFiles.concat(declFileCompilationResult.declInputFiles, declFileCompilationResult.declOtherFiles), declFileCompilationResult.declResult.errors);
+ jsCode += getErrorBaseline(tsConfigFiles.concat(declFileCompilationResult.declInputFiles, declFileCompilationResult.declOtherFiles), declFileCompilationResult.declResult.diagnostics);
}
if (jsCode.length > 0) {
@@ -1710,12 +1701,12 @@ namespace Harness {
});
}
- function fileOutput(file: GeneratedFile, harnessSettings: TestCaseParser.CompilerSettings): string {
- const fileName = harnessSettings.fullEmitPaths ? file.fileName : ts.getBaseFileName(file.fileName);
- return "//// [" + fileName + "]\r\n" + getByteOrderMarkText(file) + file.code;
+ function fileOutput(file: documents.TextDocument, harnessSettings: TestCaseParser.CompilerSettings): string {
+ const fileName = harnessSettings.fullEmitPaths ? utils.removeTestPathPrefixes(file.file) : ts.getBaseFileName(file.file);
+ return "//// [" + fileName + "]\r\n" + utils.removeTestPathPrefixes(file.text);
}
- export function collateOutputs(outputFiles: GeneratedFile[]): string {
+ export function collateOutputs(outputFiles: ReadonlyArray): string {
const gen = iterateOutputs(outputFiles);
// Emit them
let result = "";
@@ -1731,13 +1722,14 @@ namespace Harness {
return result;
}
- export function *iterateOutputs(outputFiles: GeneratedFile[]): IterableIterator<[string, string]> {
+ export function* iterateOutputs(outputFiles: Iterable): IterableIterator<[string, string]> {
// Collect, test, and sort the fileNames
- outputFiles.sort((a, b) => ts.compareStringsCaseSensitive(cleanName(a.fileName), cleanName(b.fileName)));
+ const files = Array.from(outputFiles);
+ files.slice().sort((a, b) => ts.compareStringsCaseSensitive(cleanName(a.file), cleanName(b.file)));
const dupeCase = ts.createMap();
// Yield them
- for (const outputFile of outputFiles) {
- yield [checkDuplicatedFileName(outputFile.fileName, dupeCase), "/*====== " + outputFile.fileName + " ======*/\r\n" + outputFile.code];
+ for (const outputFile of files) {
+ yield [checkDuplicatedFileName(outputFile.file, dupeCase), "/*====== " + outputFile.file + " ======*/\r\n" + utils.removeByteOrderMark(outputFile.text)];
}
function cleanName(fn: string) {
@@ -1767,83 +1759,6 @@ namespace Harness {
}
return path;
}
-
- // This does not need to exist strictly speaking, but many tests will need to be updated if it's removed
- export function compileString(_code: string, _unitName: string, _callback: (result: CompilerResult) => void) {
- // NEWTODO: Re-implement 'compileString'
- return ts.notImplemented();
- }
-
- export interface GeneratedFile {
- fileName: string;
- code: string;
- writeByteOrderMark: boolean;
- }
-
- export function isTS(fileName: string) {
- return ts.endsWith(fileName, ts.Extension.Ts);
- }
-
- export function isTSX(fileName: string) {
- return ts.endsWith(fileName, ts.Extension.Tsx);
- }
-
- export function isDTS(fileName: string) {
- return ts.endsWith(fileName, ts.Extension.Dts);
- }
-
- export function isJS(fileName: string) {
- return ts.endsWith(fileName, ts.Extension.Js);
- }
- export function isJSX(fileName: string) {
- return ts.endsWith(fileName, ts.Extension.Jsx);
- }
-
- export function isJSMap(fileName: string) {
- return ts.endsWith(fileName, ".js.map") || ts.endsWith(fileName, ".jsx.map");
- }
-
- export function isDTSMap(fileName: string) {
- return ts.endsWith(fileName, ".d.ts.map");
- }
-
- /** Contains the code and errors of a compilation and some helper methods to check its status. */
- export class CompilerResult {
- public files: GeneratedFile[] = [];
- public errors: ts.Diagnostic[] = [];
- public declFilesCode: GeneratedFile[] = [];
- public sourceMaps: GeneratedFile[] = [];
-
- /** @param fileResults an array of strings for the fileName and an ITextWriter with its code */
- constructor(fileResults: GeneratedFile[], errors: ts.Diagnostic[], public program: ts.Program,
- public currentDirectoryForProgram: string, private sourceMapData: ts.SourceMapData[], public traceResults: string[]) {
-
- for (const emittedFile of fileResults) {
- if (isDTS(emittedFile.fileName)) {
- // .d.ts file, add to declFiles emit
- this.declFilesCode.push(emittedFile);
- }
- else if (isJS(emittedFile.fileName) || isJSX(emittedFile.fileName)) {
- // .js file, add to files
- this.files.push(emittedFile);
- }
- else if (isJSMap(emittedFile.fileName) || isDTSMap(emittedFile.fileName)) {
- this.sourceMaps.push(emittedFile);
- }
- else {
- throw new Error("Unrecognized file extension for file " + emittedFile.fileName);
- }
- }
-
- this.errors = errors;
- }
-
- public getSourceMapRecord() {
- if (this.sourceMapData && this.sourceMapData.length > 0) {
- return SourceMapRecorder.getSourceMapRecord(this.sourceMapData, this.program, this.files, this.declFilesCode);
- }
- }
- }
}
export namespace TestCaseParser {
@@ -1877,13 +1792,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
@@ -2075,7 +1992,7 @@ namespace Harness {
}
const parentDirectory = IO.directoryName(dirName);
- if (parentDirectory !== "") {
+ if (parentDirectory !== "" && parentDirectory !== dirName) {
createDirectoryStructure(parentDirectory);
}
IO.createDirectory(dirName);
@@ -2174,10 +2091,11 @@ namespace Harness {
}
export function isBuiltFile(filePath: string): boolean {
- return ts.startsWith(filePath, libFolder);
+ return filePath.indexOf(libFolder) === 0 ||
+ filePath.indexOf(vpath.addTrailingSeparator(vfs.builtFolder)) === 0;
}
- export function getDefaultLibraryFile(filePath: string, io: Io): Compiler.TestFile {
+ export function getDefaultLibraryFile(filePath: string, io: IO): Compiler.TestFile {
const libFile = userSpecifiedRoot + libFolder + ts.getBaseFileName(ts.normalizeSlashes(filePath));
return { unitName: libFile, content: io.readFile(libFile) };
}
diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts
index 0063a4730f1c0..34d6f775a74b4 100644
--- a/src/harness/harnessLanguageService.ts
+++ b/src/harness/harnessLanguageService.ts
@@ -117,11 +117,17 @@ namespace Harness.LanguageService {
}
export abstract class LanguageServiceAdapterHost {
+ public readonly sys = new fakes.System(new vfs.FileSystem(/*ignoreCase*/ true, { cwd: virtualFileSystemRoot }));
public typesRegistry: ts.Map | undefined;
- protected virtualFileSystem: Utils.VirtualFileSystem = new Utils.VirtualFileSystem(virtualFileSystemRoot, /*useCaseSensitiveFilenames*/false);
+ private scriptInfos: collections.SortedMap;
constructor(protected cancellationToken = DefaultHostCancellationToken.instance,
protected settings = ts.getDefaultCompilerOptions()) {
+ this.scriptInfos = new collections.SortedMap({ comparer: this.vfs.stringComparer, sort: "insertion" });
+ }
+
+ public get vfs() {
+ return this.sys.vfs;
}
public getNewLine(): string {
@@ -130,38 +136,38 @@ namespace Harness.LanguageService {
public getFilenames(): string[] {
const fileNames: string[] = [];
- for (const virtualEntry of this.virtualFileSystem.getAllFileEntries()) {
- const scriptInfo = virtualEntry.content;
+ this.scriptInfos.forEach(scriptInfo => {
if (scriptInfo.isRootFile) {
// only include root files here
// usually it means that we won't include lib.d.ts in the list of root files so it won't mess the computation of compilation root dir.
fileNames.push(scriptInfo.fileName);
}
- }
+ });
return fileNames;
}
public getScriptInfo(fileName: string): ScriptInfo {
- const fileEntry = this.virtualFileSystem.traversePath(fileName);
- return fileEntry && fileEntry.isFile() ? fileEntry.content : undefined;
+ return this.scriptInfos.get(vpath.resolve(this.vfs.cwd(), fileName));
}
public addScript(fileName: string, content: string, isRootFile: boolean): void {
- this.virtualFileSystem.addFile(fileName, new ScriptInfo(fileName, content, isRootFile));
+ this.vfs.mkdirpSync(vpath.dirname(fileName));
+ this.vfs.writeFileSync(fileName, content);
+ this.scriptInfos.set(vpath.resolve(this.vfs.cwd(), fileName), new ScriptInfo(fileName, content, isRootFile));
}
public editScript(fileName: string, start: number, end: number, newText: string) {
const script = this.getScriptInfo(fileName);
- if (script !== undefined) {
+ if (script) {
script.editContent(start, end, newText);
+ this.vfs.mkdirpSync(vpath.dirname(fileName));
+ this.vfs.writeFileSync(fileName, script.content);
return;
}
throw new Error("No script with name '" + fileName + "'");
}
- public abstract addSymlink(from: string, target: string): void;
-
public openFile(_fileName: string, _content?: string, _scriptKindName?: string): void { /*overridden*/ }
/**
@@ -178,62 +184,60 @@ namespace Harness.LanguageService {
/// Native adapter
class NativeLanguageServiceHost extends LanguageServiceAdapterHost implements ts.LanguageServiceHost, LanguageServiceAdapterHost {
- symlinks = ts.createMap();
-
isKnownTypesPackageName(name: string): boolean {
return this.typesRegistry && this.typesRegistry.has(name);
}
+
installPackage = ts.notImplemented;
getCompilationSettings() { return this.settings; }
+
getCancellationToken() { return this.cancellationToken; }
+
getDirectories(path: string): string[] {
- const dir = this.virtualFileSystem.traversePath(path);
- return dir && dir.isDirectory() ? dir.getDirectories().map(d => d.name) : [];
+ return this.sys.getDirectories(path);
}
+
getCurrentDirectory(): string { return virtualFileSystemRoot; }
+
getDefaultLibFileName(): string { return Compiler.defaultLibFileName; }
+
getScriptFileNames(): string[] {
return this.getFilenames().filter(ts.isAnySupportedFileExtension);
}
+
getScriptSnapshot(fileName: string): ts.IScriptSnapshot {
const script = this.getScriptInfo(fileName);
return script ? new ScriptSnapshot(script) : undefined;
}
+
getScriptKind(): ts.ScriptKind { return ts.ScriptKind.Unknown; }
+
getScriptVersion(fileName: string): string {
const script = this.getScriptInfo(fileName);
return script ? script.version.toString() : undefined;
}
directoryExists(dirName: string): boolean {
- if (ts.forEachEntry(this.symlinks, (_, key) => ts.forSomeAncestorDirectory(key, ancestor => ancestor === dirName))) {
- return true;
- }
-
- const fileEntry = this.virtualFileSystem.traversePath(dirName);
- return fileEntry && fileEntry.isDirectory();
+ return this.sys.directoryExists(dirName);
}
fileExists(fileName: string): boolean {
- return this.symlinks.has(fileName) || this.getScriptSnapshot(fileName) !== undefined;
+ return this.sys.fileExists(fileName);
}
+
readDirectory(path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray, depth?: number): string[] {
- return ts.matchFiles(path, extensions, exclude, include,
- /*useCaseSensitiveFileNames*/ false,
- this.getCurrentDirectory(),
- depth,
- (p) => this.virtualFileSystem.getAccessibleFileSystemEntries(p));
+ return this.sys.readDirectory(path, extensions, exclude, include, depth);
}
+
readFile(path: string): string | undefined {
- const target = this.symlinks.get(path);
- return target !== undefined ? this.readFile(target) : ts.getSnapshotText(this.getScriptSnapshot(path));
+ return this.sys.readFile(path);
}
- addSymlink(from: string, target: string) { this.symlinks.set(from, target); }
+
realpath(path: string): string {
- const target = this.symlinks.get(path);
- return target === undefined ? path : target;
+ return this.sys.realpath(path);
}
+
getTypeRootsVersion() {
return 0;
}
@@ -262,8 +266,6 @@ namespace Harness.LanguageService {
public getModuleResolutionsForFile: (fileName: string) => string;
public getTypeReferenceDirectiveResolutionsForFile: (fileName: string) => string;
- addSymlink() { return ts.notImplemented(); }
-
constructor(preprocessToResolve: boolean, cancellationToken?: ts.HostCancellationToken, options?: ts.CompilerOptions) {
super(cancellationToken, options);
this.nativeHost = new NativeLanguageServiceHost(cancellationToken, options);
diff --git a/src/harness/loggedIO.ts b/src/harness/loggedIO.ts
index e87402ea14008..9a5e13b107abd 100644
--- a/src/harness/loggedIO.ts
+++ b/src/harness/loggedIO.ts
@@ -110,7 +110,7 @@ namespace Playback {
return run;
}
- export interface PlaybackIO extends Harness.Io, PlaybackControl { }
+ export interface PlaybackIO extends Harness.IO, PlaybackControl { }
export interface PlaybackSystem extends ts.System, PlaybackControl { }
@@ -134,7 +134,7 @@ namespace Playback {
};
}
- export function newStyleLogIntoOldStyleLog(log: IoLog, host: ts.System | Harness.Io, baseName: string) {
+ export function newStyleLogIntoOldStyleLog(log: IoLog, host: ts.System | Harness.IO, baseName: string) {
for (const file of log.filesAppended) {
if (file.contentsPath) {
file.contents = host.readFile(ts.combinePaths(baseName, file.contentsPath));
@@ -210,8 +210,8 @@ namespace Playback {
}
function initWrapper(wrapper: PlaybackSystem, underlying: ts.System): void;
- function initWrapper(wrapper: PlaybackIO, underlying: Harness.Io): void;
- function initWrapper(wrapper: PlaybackSystem | PlaybackIO, underlying: ts.System | Harness.Io): void {
+ function initWrapper(wrapper: PlaybackIO, underlying: Harness.IO): void;
+ function initWrapper(wrapper: PlaybackSystem | PlaybackIO, underlying: ts.System | Harness.IO): void {
ts.forEach(Object.keys(underlying), prop => {
(wrapper)[prop] = (underlying)[prop];
});
@@ -427,7 +427,7 @@ namespace Playback {
// console.log("Swallowed write operation during replay: " + name);
}
- export function wrapIO(underlying: Harness.Io): PlaybackIO {
+ export function wrapIO(underlying: Harness.IO): PlaybackIO {
const wrapper: PlaybackIO = {};
initWrapper(wrapper, underlying);
diff --git a/src/harness/parallel/worker.ts b/src/harness/parallel/worker.ts
index 50c043cf71d74..4a0f297eee6b4 100644
--- a/src/harness/parallel/worker.ts
+++ b/src/harness/parallel/worker.ts
@@ -28,12 +28,14 @@ namespace Harness.Parallel.Worker {
(global as any).describe = ((name, callback) => {
testList.push({ name, callback, kind: "suite" });
}) as Mocha.IContextDefinition;
+ (global as any).describe.skip = ts.noop;
(global as any).it = ((name, callback) => {
if (!testList) {
throw new Error("Tests must occur within a describe block");
}
testList.push({ name, callback, kind: "test" });
}) as Mocha.ITestDefinition;
+ (global as any).it.skip = ts.noop;
}
function setTimeoutAndExecute(timeout: number | undefined, f: () => void) {
diff --git a/src/harness/projectsRunner.ts b/src/harness/projectsRunner.ts
index 7dbfe0c5a75f3..3a34fa4cbef12 100644
--- a/src/harness/projectsRunner.ts
+++ b/src/harness/projectsRunner.ts
@@ -1,373 +1,371 @@
-///
-///
-
-// Test case is json of below type in tests/cases/project/
-interface ProjectRunnerTestCase {
- scenario: string;
- projectRoot: string; // project where it lives - this also is the current directory when compiling
- inputFiles: ReadonlyArray; // list of input files to be given to program
- resolveMapRoot?: boolean; // should we resolve this map root and give compiler the absolute disk path as map root?
- resolveSourceRoot?: boolean; // should we resolve this source root and give compiler the absolute disk path as map root?
- baselineCheck?: boolean; // Verify the baselines of output files, if this is false, we will write to output to the disk but there is no verification of baselines
- runTest?: boolean; // Run the resulting test
- bug?: string; // If there is any bug associated with this test case
-}
-
-interface ProjectRunnerTestCaseResolutionInfo extends ProjectRunnerTestCase {
- // Apart from actual test case the results of the resolution
- resolvedInputFiles: ReadonlyArray; // List of files that were asked to read by compiler
- emittedFiles: ReadonlyArray; // List of files that were emitted by the compiler
-}
-
-interface BatchCompileProjectTestCaseEmittedFile extends Harness.Compiler.GeneratedFile {
- emittedFileName: string;
-}
-
-interface CompileProjectFilesResult {
- configFileSourceFiles: ReadonlyArray;
- moduleKind: ts.ModuleKind;
- program?: ts.Program;
- compilerOptions?: ts.CompilerOptions;
- errors: ReadonlyArray;
- sourceMapData?: ReadonlyArray;
-}
-
-interface BatchCompileProjectTestCaseResult extends CompileProjectFilesResult {
- outputFiles?: BatchCompileProjectTestCaseEmittedFile[];
-}
-
-class ProjectRunner extends RunnerBase {
-
- public enumerateTestFiles() {
- return this.enumerateFiles("tests/cases/project", /\.json$/, { recursive: true });
+///
+///
+///
+///
+///
+///
+///
+
+namespace project {
+ // Test case is json of below type in tests/cases/project/
+ interface ProjectRunnerTestCase {
+ scenario: string;
+ projectRoot: string; // project where it lives - this also is the current directory when compiling
+ inputFiles: ReadonlyArray; // list of input files to be given to program
+ resolveMapRoot?: boolean; // should we resolve this map root and give compiler the absolute disk path as map root?
+ resolveSourceRoot?: boolean; // should we resolve this source root and give compiler the absolute disk path as map root?
+ baselineCheck?: boolean; // Verify the baselines of output files, if this is false, we will write to output to the disk but there is no verification of baselines
+ runTest?: boolean; // Run the resulting test
+ bug?: string; // If there is any bug associated with this test case
}
- public kind(): TestRunnerKind {
- return "project";
+ interface ProjectRunnerTestCaseResolutionInfo extends ProjectRunnerTestCase {
+ // Apart from actual test case the results of the resolution
+ resolvedInputFiles: ReadonlyArray; // List of files that were asked to read by compiler
+ emittedFiles: ReadonlyArray; // List of files that were emitted by the compiler
}
- public initializeTests() {
- if (this.tests.length === 0) {
- const testFiles = this.enumerateTestFiles();
- testFiles.forEach(fn => {
- this.runProjectTestCase(fn);
- });
- }
- else {
- this.tests.forEach(test => this.runProjectTestCase(test));
- }
+ interface CompileProjectFilesResult {
+ configFileSourceFiles: ReadonlyArray;
+ moduleKind: ts.ModuleKind;
+ program?: ts.Program;
+ compilerOptions?: ts.CompilerOptions;
+ errors: ReadonlyArray;
+ sourceMapData?: ReadonlyArray;
}
- private runProjectTestCase(testCaseFileName: string) {
- let testCase: ProjectRunnerTestCase & ts.CompilerOptions;
+ interface BatchCompileProjectTestCaseResult extends CompileProjectFilesResult {
+ outputFiles?: ReadonlyArray;
+ }
- let testFileText: string;
- try {
- testFileText = Harness.IO.readFile(testCaseFileName);
- }
- catch (e) {
- assert(false, "Unable to open testcase file: " + testCaseFileName + ": " + e.message);
+ export class ProjectRunner extends RunnerBase {
+ public enumerateTestFiles() {
+ return this.enumerateFiles("tests/cases/project", /\.json$/, { recursive: true });
}
- try {
- testCase = JSON.parse(testFileText);
- }
- catch (e) {
- assert(false, "Testcase: " + testCaseFileName + " does not contain valid json format: " + e.message);
- }
- let testCaseJustName = testCaseFileName.replace(/^.*[\\\/]/, "").replace(/\.json/, "");
-
- function moduleNameToString(moduleKind: ts.ModuleKind) {
- return moduleKind === ts.ModuleKind.AMD
- ? "amd"
- : moduleKind === ts.ModuleKind.CommonJS
- ? "node"
- : "none";
+ public kind(): TestRunnerKind {
+ return "project";
}
- // Project baselines verified go in project/testCaseName/moduleKind/
- function getBaselineFolder(moduleKind: ts.ModuleKind) {
- return "project/" + testCaseJustName + "/" + moduleNameToString(moduleKind) + "/";
+ public initializeTests() {
+ describe("projects tests", () => {
+ const tests = this.tests.length === 0 ? this.enumerateTestFiles() : this.tests;
+ for (const test of tests) {
+ this.runProjectTestCase(test);
+ }
+ });
}
- // When test case output goes to tests/baselines/local/projectOutput/testCaseName/moduleKind/
- // We have these two separate locations because when comparing baselines the baseline verifier will delete the existing file
- // so even if it was created by compiler in that location, the file will be deleted by verified before we can read it
- // so lets keep these two locations separate
- function getProjectOutputFolder(fileName: string, moduleKind: ts.ModuleKind) {
- return Harness.Baseline.localPath("projectOutput/" + testCaseJustName + "/" + moduleNameToString(moduleKind) + "/" + fileName);
+ private runProjectTestCase(testCaseFileName: string) {
+ for (const { name, payload } of ProjectTestCase.getConfigurations(testCaseFileName)) {
+ describe("Compiling project for " + payload.testCase.scenario + ": testcase " + testCaseFileName + (name ? ` (${name})` : ``), () => {
+ let projectTestCase: ProjectTestCase | undefined;
+ before(() => { projectTestCase = new ProjectTestCase(testCaseFileName, payload); });
+ it(`Correct module resolution tracing for ${testCaseFileName}`, () => projectTestCase && projectTestCase.verifyResolution());
+ it(`Correct errors for ${testCaseFileName}`, () => projectTestCase && projectTestCase.verifyDiagnostics());
+ it(`Correct JS output for ${testCaseFileName}`, () => projectTestCase && projectTestCase.verifyJavaScriptOutput());
+ // NOTE: This check was commented out in previous code. Leaving this here to eventually be restored if needed.
+ // it(`Correct sourcemap content for ${testCaseFileName}`, () => projectTestCase && projectTestCase.verifySourceMapRecord());
+ it(`Correct declarations for ${testCaseFileName}`, () => projectTestCase && projectTestCase.verifyDeclarations());
+ after(() => { projectTestCase = undefined; });
+ });
+ }
}
+ }
- function cleanProjectUrl(url: string) {
- let diskProjectPath = ts.normalizeSlashes(Harness.IO.resolvePath(testCase.projectRoot));
- let projectRootUrl = "file:///" + diskProjectPath;
- const normalizedProjectRoot = ts.normalizeSlashes(testCase.projectRoot);
- diskProjectPath = diskProjectPath.substr(0, diskProjectPath.lastIndexOf(normalizedProjectRoot));
- projectRootUrl = projectRootUrl.substr(0, projectRootUrl.lastIndexOf(normalizedProjectRoot));
- if (url && url.length) {
- if (url.indexOf(projectRootUrl) === 0) {
- // replace the disk specific project url path into project root url
- url = "file:///" + url.substr(projectRootUrl.length);
- }
- else if (url.indexOf(diskProjectPath) === 0) {
- // Replace the disk specific path into the project root path
- url = url.substr(diskProjectPath.length);
- if (url.charCodeAt(0) !== ts.CharacterCodes.slash) {
- url = "/" + url;
- }
- }
- }
+ class ProjectCompilerHost extends fakes.CompilerHost {
+ private _testCase: ProjectRunnerTestCase & ts.CompilerOptions;
+ private _projectParseConfigHost: ProjectParseConfigHost;
- return url;
+ constructor(sys: fakes.System | vfs.FileSystem, compilerOptions: ts.CompilerOptions, _testCaseJustName: string, testCase: ProjectRunnerTestCase & ts.CompilerOptions, _moduleKind: ts.ModuleKind) {
+ super(sys, compilerOptions);
+ this._testCase = testCase;
}
- function getCurrentDirectory() {
- return Harness.IO.resolvePath(testCase.projectRoot);
+ public get parseConfigHost(): fakes.ParseConfigHost {
+ return this._projectParseConfigHost || (this._projectParseConfigHost = new ProjectParseConfigHost(this.sys, this._testCase));
}
- function compileProjectFiles(moduleKind: ts.ModuleKind, configFileSourceFiles: ReadonlyArray,
- getInputFiles: () => ReadonlyArray,
- getSourceFileTextImpl: (fileName: string) => string,
- writeFile: (fileName: string, data: string, writeByteOrderMark: boolean) => void,
- compilerOptions: ts.CompilerOptions): CompileProjectFilesResult {
-
- const program = ts.createProgram(getInputFiles(), compilerOptions, createCompilerHost());
- const errors = ts.getPreEmitDiagnostics(program);
-
- const emitResult = program.emit();
- ts.addRange(errors, emitResult.diagnostics);
- const sourceMapData = emitResult.sourceMaps;
-
- // Clean up source map data that will be used in baselining
- if (sourceMapData) {
- for (const data of sourceMapData) {
- for (let j = 0; j < data.sourceMapSources.length; j++) {
- data.sourceMapSources[j] = cleanProjectUrl(data.sourceMapSources[j]);
- }
- data.jsSourceMappingURL = cleanProjectUrl(data.jsSourceMappingURL);
- data.sourceMapSourceRoot = cleanProjectUrl(data.sourceMapSourceRoot);
- }
- }
+ public getDefaultLibFileName(_options: ts.CompilerOptions) {
+ return vpath.resolve(this.getDefaultLibLocation(), "lib.es5.d.ts");
+ }
+ }
- return {
- configFileSourceFiles,
- moduleKind,
- program,
- errors,
- sourceMapData
- };
+ class ProjectParseConfigHost extends fakes.ParseConfigHost {
+ private _testCase: ProjectRunnerTestCase & ts.CompilerOptions;
- function getSourceFileText(fileName: string): string {
- const text = getSourceFileTextImpl(fileName);
- return text !== undefined ? text : getSourceFileTextImpl(ts.getNormalizedAbsolutePath(fileName, getCurrentDirectory()));
- }
+ constructor(sys: fakes.System, testCase: ProjectRunnerTestCase & ts.CompilerOptions) {
+ super(sys);
+ this._testCase = testCase;
+ }
- function getSourceFile(fileName: string, languageVersion: ts.ScriptTarget): ts.SourceFile {
- let sourceFile: ts.SourceFile;
- if (fileName === Harness.Compiler.defaultLibFileName) {
- sourceFile = Harness.Compiler.getDefaultLibrarySourceFile(Harness.Compiler.getDefaultLibFileName(compilerOptions));
- }
- else {
- const text = getSourceFileText(fileName);
- if (text !== undefined) {
- sourceFile = Harness.Compiler.createSourceFileAndAssertInvariants(fileName, text, languageVersion);
- }
- }
+ public readDirectory(path: string, extensions: string[], excludes: string[], includes: string[], depth: number): string[] {
+ const result = super.readDirectory(path, extensions, excludes, includes, depth);
+ const projectRoot = vpath.resolve(vfs.srcFolder, this._testCase.projectRoot);
+ return result.map(item => vpath.relative(
+ projectRoot,
+ vpath.resolve(projectRoot, item),
+ this.vfs.ignoreCase
+ ));
+ }
+ }
- return sourceFile;
- }
+ interface ProjectTestConfiguration {
+ name: string;
+ payload: ProjectTestPayload;
+ }
- function createCompilerHost(): ts.CompilerHost {
- return {
- getSourceFile,
- getDefaultLibFileName: () => Harness.Compiler.defaultLibFileName,
- writeFile,
- getCurrentDirectory,
- getCanonicalFileName: Harness.Compiler.getCanonicalFileName,
- useCaseSensitiveFileNames: () => Harness.IO.useCaseSensitiveFileNames(),
- getNewLine: () => Harness.IO.newLine(),
- fileExists: fileName => fileName === Harness.Compiler.defaultLibFileName || getSourceFileText(fileName) !== undefined,
- readFile: fileName => Harness.IO.readFile(fileName),
- getDirectories: path => Harness.IO.getDirectories(path)
- };
- }
- }
+ interface ProjectTestPayload {
+ testCase: ProjectRunnerTestCase & ts.CompilerOptions;
+ moduleKind: ts.ModuleKind;
+ vfs: vfs.FileSystem;
+ }
- function batchCompilerProjectTestCase(moduleKind: ts.ModuleKind): BatchCompileProjectTestCaseResult {
- let nonSubfolderDiskFiles = 0;
+ class ProjectTestCase {
+ private testCase: ProjectRunnerTestCase & ts.CompilerOptions;
+ private testCaseJustName: string;
+ private sys: fakes.System;
+ private compilerOptions: ts.CompilerOptions;
+ private compilerResult: BatchCompileProjectTestCaseResult;
- const outputFiles: BatchCompileProjectTestCaseEmittedFile[] = [];
- let inputFiles = testCase.inputFiles;
- let compilerOptions = createCompilerOptions();
- const configFileSourceFiles: ts.SourceFile[] = [];
+ constructor(testCaseFileName: string, { testCase, moduleKind, vfs }: ProjectTestPayload) {
+ this.testCase = testCase;
+ this.testCaseJustName = testCaseFileName.replace(/^.*[\\\/]/, "").replace(/\.json/, "");
+ this.compilerOptions = createCompilerOptions(testCase, moduleKind);
+ this.sys = new fakes.System(vfs);
let configFileName: string;
- if (compilerOptions.project) {
+ let inputFiles = testCase.inputFiles;
+ if (this.compilerOptions.project) {
// Parse project
- configFileName = ts.normalizePath(ts.combinePaths(compilerOptions.project, "tsconfig.json"));
+ configFileName = ts.normalizePath(ts.combinePaths(this.compilerOptions.project, "tsconfig.json"));
assert(!inputFiles || inputFiles.length === 0, "cannot specify input files and project option together");
}
else if (!inputFiles || inputFiles.length === 0) {
- configFileName = ts.findConfigFile("", fileExists);
+ configFileName = ts.findConfigFile("", path => this.sys.fileExists(path));
}
let errors: ts.Diagnostic[];
+ const configFileSourceFiles: ts.SourceFile[] = [];
if (configFileName) {
- const result = ts.readJsonConfigFile(configFileName, getSourceFileText);
+ const result = ts.readJsonConfigFile(configFileName, path => this.sys.readFile(path));
configFileSourceFiles.push(result);
- const configParseHost: ts.ParseConfigHost = {
- useCaseSensitiveFileNames: Harness.IO.useCaseSensitiveFileNames(),
- fileExists,
- readDirectory,
- readFile
- };
- const configParseResult = ts.parseJsonSourceFileConfigFileContent(result, configParseHost, ts.getDirectoryPath(configFileName), compilerOptions);
+ const configParseHost = new ProjectParseConfigHost(this.sys, this.testCase);
+ const configParseResult = ts.parseJsonSourceFileConfigFileContent(result, configParseHost, ts.getDirectoryPath(configFileName), this.compilerOptions);
inputFiles = configParseResult.fileNames;
- compilerOptions = configParseResult.options;
+ this.compilerOptions = configParseResult.options;
errors = result.parseDiagnostics.concat(configParseResult.errors);
}
- const projectCompilerResult = compileProjectFiles(moduleKind, configFileSourceFiles, () => inputFiles, getSourceFileText, writeFile, compilerOptions);
- return {
+ const compilerHost = new ProjectCompilerHost(this.sys, this.compilerOptions, this.testCaseJustName, this.testCase, moduleKind);
+ const projectCompilerResult = this.compileProjectFiles(moduleKind, configFileSourceFiles, () => inputFiles, compilerHost, this.compilerOptions);
+
+ this.compilerResult = {
configFileSourceFiles,
moduleKind,
program: projectCompilerResult.program,
- compilerOptions,
+ compilerOptions: this.compilerOptions,
sourceMapData: projectCompilerResult.sourceMapData,
- outputFiles,
+ outputFiles: compilerHost.outputs,
errors: errors ? ts.concatenate(errors, projectCompilerResult.errors) : projectCompilerResult.errors,
};
+ }
- function createCompilerOptions() {
- // Set the special options that depend on other testcase options
- const compilerOptions: ts.CompilerOptions = {
- mapRoot: testCase.resolveMapRoot && testCase.mapRoot ? Harness.IO.resolvePath(testCase.mapRoot) : testCase.mapRoot,
- sourceRoot: testCase.resolveSourceRoot && testCase.sourceRoot ? Harness.IO.resolvePath(testCase.sourceRoot) : testCase.sourceRoot,
- module: moduleKind,
- moduleResolution: ts.ModuleResolutionKind.Classic, // currently all tests use classic module resolution kind, this will change in the future
- };
- // Set the values specified using json
- const optionNameMap = ts.arrayToMap(ts.optionDeclarations, option => option.name);
- for (const name in testCase) {
- if (name !== "mapRoot" && name !== "sourceRoot") {
- const option = optionNameMap.get(name);
- if (option) {
- const optType = option.type;
- let value = testCase[name];
- if (!ts.isString(optType)) {
- const key = value.toLowerCase();
- const optTypeValue = optType.get(key);
- if (optTypeValue) {
- value = optTypeValue;
- }
- }
- compilerOptions[option.name] = value;
- }
- }
- }
+ private get vfs() {
+ return this.sys.vfs;
+ }
- return compilerOptions;
- }
+ public static getConfigurations(testCaseFileName: string): ProjectTestConfiguration[] {
+ let testCase: ProjectRunnerTestCase & ts.CompilerOptions;
- function getFileNameInTheProjectTest(fileName: string): string {
- return ts.isRootedDiskPath(fileName)
- ? fileName
- : ts.normalizeSlashes(testCase.projectRoot) + "/" + ts.normalizeSlashes(fileName);
+ let testFileText: string;
+ try {
+ testFileText = Harness.IO.readFile(testCaseFileName);
}
-
- function readDirectory(rootDir: string, extension: string[], exclude: string[], include: string[], depth: number): string[] {
- const harnessReadDirectoryResult = Harness.IO.readDirectory(getFileNameInTheProjectTest(rootDir), extension, exclude, include, depth);
- const result: string[] = [];
- for (let i = 0; i < harnessReadDirectoryResult.length; i++) {
- result[i] = ts.getRelativePathToDirectoryOrUrl(testCase.projectRoot, harnessReadDirectoryResult[i],
- getCurrentDirectory(), Harness.Compiler.getCanonicalFileName, /*isAbsolutePathAnUrl*/ false);
- }
- return result;
+ catch (e) {
+ assert(false, "Unable to open testcase file: " + testCaseFileName + ": " + e.message);
}
- function fileExists(fileName: string): boolean {
- return Harness.IO.fileExists(getFileNameInTheProjectTest(fileName));
+ try {
+ testCase = JSON.parse(testFileText);
}
+ catch (e) {
+ assert(false, "Testcase: " + testCaseFileName + " does not contain valid json format: " + e.message);
+ }
+
+ const fs = vfs.createFromFileSystem(Harness.IO, /*ignoreCase*/ false);
+ fs.mountSync(vpath.resolve(Harness.IO.getWorkspaceRoot(), "tests"), vpath.combine(vfs.srcFolder, "tests"), vfs.createResolver(Harness.IO));
+ fs.mkdirpSync(vpath.combine(vfs.srcFolder, testCase.projectRoot));
+ fs.chdir(vpath.combine(vfs.srcFolder, testCase.projectRoot));
+ fs.makeReadonly();
- function readFile(fileName: string): string | undefined {
- return Harness.IO.readFile(getFileNameInTheProjectTest(fileName));
+ return [
+ { name: `@module: commonjs`, payload: { testCase, moduleKind: ts.ModuleKind.CommonJS, vfs: fs } },
+ { name: `@module: amd`, payload: { testCase, moduleKind: ts.ModuleKind.AMD, vfs: fs } }
+ ];
+ }
+
+ public verifyResolution() {
+ const cwd = this.vfs.cwd();
+ const ignoreCase = this.vfs.ignoreCase;
+ const resolutionInfo: ProjectRunnerTestCaseResolutionInfo & ts.CompilerOptions = JSON.parse(JSON.stringify(this.testCase));
+ resolutionInfo.resolvedInputFiles = this.compilerResult.program.getSourceFiles()
+ .map(({ fileName: input }) => vpath.beneath(vfs.builtFolder, input, this.vfs.ignoreCase) || vpath.beneath(vfs.testLibFolder, input, this.vfs.ignoreCase) ? utils.removeTestPathPrefixes(input) :
+ vpath.isAbsolute(input) ? vpath.relative(cwd, input, ignoreCase) :
+ input);
+
+ resolutionInfo.emittedFiles = this.compilerResult.outputFiles
+ .map(output => output.meta.get("fileName") || output.file)
+ .map(output => utils.removeTestPathPrefixes(vpath.isAbsolute(output) ? vpath.relative(cwd, output, ignoreCase) : output));
+
+ const content = JSON.stringify(resolutionInfo, undefined, " ");
+
+ // TODO(rbuckton): This patches the baseline to replace lib.es5.d.ts with lib.d.ts.
+ // This is only to make the PR for this change easier to read. A follow-up PR will
+ // revert this change and accept the new baselines.
+ // See https://github.com/Microsoft/TypeScript/pull/20763#issuecomment-352553264
+ const patchedContent = content.replace(/lib\.es5\.d\.ts/g, "lib.d.ts");
+ Harness.Baseline.runBaseline(this.getBaselineFolder(this.compilerResult.moduleKind) + this.testCaseJustName + ".json", () => patchedContent);
+ }
+
+ public verifyDiagnostics() {
+ if (this.compilerResult.errors.length) {
+ Harness.Baseline.runBaseline(this.getBaselineFolder(this.compilerResult.moduleKind) + this.testCaseJustName + ".errors.txt", () => {
+ return getErrorsBaseline(this.compilerResult);
+ });
}
+ }
- function getSourceFileText(fileName: string): string {
- let text: string;
- try {
- text = Harness.IO.readFile(getFileNameInTheProjectTest(fileName));
+ public verifyJavaScriptOutput() {
+ if (this.testCase.baselineCheck) {
+ const errs: Error[] = [];
+ let nonSubfolderDiskFiles = 0;
+ for (const output of this.compilerResult.outputFiles) {
+ try {
+ // convert file name to rooted name
+ // if filename is not rooted - concat it with project root and then expand project root relative to current directory
+ const fileName = output.meta.get("fileName") || output.file;
+ const diskFileName = vpath.isAbsolute(fileName) ? fileName : vpath.resolve(this.vfs.cwd(), fileName);
+
+ // compute file name relative to current directory (expanded project root)
+ let diskRelativeName = vpath.relative(this.vfs.cwd(), diskFileName, this.vfs.ignoreCase);
+ if (vpath.isAbsolute(diskRelativeName) || diskRelativeName.startsWith("../")) {
+ // If the generated output file resides in the parent folder or is rooted path,
+ // we need to instead create files that can live in the project reference folder
+ // but make sure extension of these files matches with the fileName the compiler asked to write
+ diskRelativeName = `diskFile${nonSubfolderDiskFiles}${vpath.extname(fileName, [".js.map", ".js", ".d.ts"], this.vfs.ignoreCase)}`;
+ nonSubfolderDiskFiles++;
+ }
+
+ const content = utils.removeTestPathPrefixes(output.text, /*retainTrailingDirectorySeparator*/ true);
+ Harness.Baseline.runBaseline(this.getBaselineFolder(this.compilerResult.moduleKind) + diskRelativeName, () => content);
+ }
+ catch (e) {
+ errs.push(e);
+ }
}
- catch (e) {
- // text doesn't get defined.
+
+ if (errs.length) {
+ throw Error(errs.join("\n "));
}
- return text;
}
+ }
- function writeFile(fileName: string, data: string, writeByteOrderMark: boolean) {
- // convert file name to rooted name
- // if filename is not rooted - concat it with project root and then expand project root relative to current directory
- const diskFileName = ts.isRootedDiskPath(fileName)
- ? fileName
- : Harness.IO.resolvePath(ts.normalizeSlashes(testCase.projectRoot) + "/" + ts.normalizeSlashes(fileName));
-
- const currentDirectory = getCurrentDirectory();
- // compute file name relative to current directory (expanded project root)
- let diskRelativeName = ts.getRelativePathToDirectoryOrUrl(currentDirectory, diskFileName, currentDirectory, Harness.Compiler.getCanonicalFileName, /*isAbsolutePathAnUrl*/ false);
- if (ts.isRootedDiskPath(diskRelativeName) || diskRelativeName.substr(0, 3) === "../") {
- // If the generated output file resides in the parent folder or is rooted path,
- // we need to instead create files that can live in the project reference folder
- // but make sure extension of these files matches with the fileName the compiler asked to write
- diskRelativeName = "diskFile" + nonSubfolderDiskFiles +
- (Harness.Compiler.isDTS(fileName) ? ts.Extension.Dts :
- Harness.Compiler.isJS(fileName) ? ts.Extension.Js : ".js.map");
- nonSubfolderDiskFiles++;
+ public verifySourceMapRecord() {
+ // NOTE: This check was commented out in previous code. Leaving this here to eventually be restored if needed.
+ // if (compilerResult.sourceMapData) {
+ // Harness.Baseline.runBaseline(getBaselineFolder(compilerResult.moduleKind) + testCaseJustName + ".sourcemap.txt", () => {
+ // return Harness.SourceMapRecorder.getSourceMapRecord(compilerResult.sourceMapData, compilerResult.program,
+ // ts.filter(compilerResult.outputFiles, outputFile => Harness.Compiler.isJS(outputFile.emittedFileName)));
+ // });
+ // }
+ }
+
+ public verifyDeclarations() {
+ if (!this.compilerResult.errors.length && this.testCase.declaration) {
+ const dTsCompileResult = this.compileDeclarations(this.compilerResult);
+ if (dTsCompileResult && dTsCompileResult.errors.length) {
+ Harness.Baseline.runBaseline(this.getBaselineFolder(this.compilerResult.moduleKind) + this.testCaseJustName + ".dts.errors.txt", () => {
+ return getErrorsBaseline(dTsCompileResult);
+ });
}
+ }
+ }
- if (Harness.Compiler.isJS(fileName)) {
- // Make sure if there is URl we have it cleaned up
- const indexOfSourceMapUrl = data.lastIndexOf(`//# ${"sourceMappingURL"}=`); // This line can be seen as a sourceMappingURL comment
- if (indexOfSourceMapUrl !== -1) {
- data = data.substring(0, indexOfSourceMapUrl + 21) + cleanProjectUrl(data.substring(indexOfSourceMapUrl + 21));
- }
+ // Project baselines verified go in project/testCaseName/moduleKind/
+ private getBaselineFolder(moduleKind: ts.ModuleKind) {
+ return "project/" + this.testCaseJustName + "/" + moduleNameToString(moduleKind) + "/";
+ }
+
+ private cleanProjectUrl(url: string) {
+ let diskProjectPath = ts.normalizeSlashes(Harness.IO.resolvePath(this.testCase.projectRoot));
+ let projectRootUrl = "file:///" + diskProjectPath;
+ const normalizedProjectRoot = ts.normalizeSlashes(this.testCase.projectRoot);
+ diskProjectPath = diskProjectPath.substr(0, diskProjectPath.lastIndexOf(normalizedProjectRoot));
+ projectRootUrl = projectRootUrl.substr(0, projectRootUrl.lastIndexOf(normalizedProjectRoot));
+ if (url && url.length) {
+ if (url.indexOf(projectRootUrl) === 0) {
+ // replace the disk specific project url path into project root url
+ url = "file:///" + url.substr(projectRootUrl.length);
}
- else if (Harness.Compiler.isJSMap(fileName)) {
- // Make sure sources list is cleaned
- const sourceMapData = JSON.parse(data);
- for (let i = 0; i < sourceMapData.sources.length; i++) {
- sourceMapData.sources[i] = cleanProjectUrl(sourceMapData.sources[i]);
+ else if (url.indexOf(diskProjectPath) === 0) {
+ // Replace the disk specific path into the project root path
+ url = url.substr(diskProjectPath.length);
+ if (url.charCodeAt(0) !== ts.CharacterCodes.slash) {
+ url = "/" + url;
}
- sourceMapData.sourceRoot = cleanProjectUrl(sourceMapData.sourceRoot);
- data = JSON.stringify(sourceMapData);
}
+ }
- const outputFilePath = getProjectOutputFolder(diskRelativeName, moduleKind);
- // Actual writing of file as in tc.ts
- function ensureDirectoryStructure(directoryname: string) {
- if (directoryname) {
- if (!Harness.IO.directoryExists(directoryname)) {
- ensureDirectoryStructure(ts.getDirectoryPath(directoryname));
- Harness.IO.createDirectory(directoryname);
- }
+ return url;
+ }
+
+ private compileProjectFiles(moduleKind: ts.ModuleKind, configFileSourceFiles: ReadonlyArray,
+ getInputFiles: () => ReadonlyArray,
+ compilerHost: ts.CompilerHost,
+ compilerOptions: ts.CompilerOptions): CompileProjectFilesResult {
+
+ const program = ts.createProgram(getInputFiles(), compilerOptions, compilerHost);
+ const errors = ts.getPreEmitDiagnostics(program);
+
+ const emitResult = program.emit();
+ ts.addRange(errors, emitResult.diagnostics);
+ const sourceMapData = emitResult.sourceMaps;
+
+ // Clean up source map data that will be used in baselining
+ if (sourceMapData) {
+ for (const data of sourceMapData) {
+ for (let j = 0; j < data.sourceMapSources.length; j++) {
+ data.sourceMapSources[j] = this.cleanProjectUrl(data.sourceMapSources[j]);
}
+ data.jsSourceMappingURL = this.cleanProjectUrl(data.jsSourceMappingURL);
+ data.sourceMapSourceRoot = this.cleanProjectUrl(data.sourceMapSourceRoot);
}
- ensureDirectoryStructure(ts.getDirectoryPath(ts.normalizePath(outputFilePath)));
- Harness.IO.writeFile(outputFilePath, data);
-
- outputFiles.push({ emittedFileName: fileName, code: data, fileName: diskRelativeName, writeByteOrderMark });
}
+
+ return {
+ configFileSourceFiles,
+ moduleKind,
+ program,
+ errors,
+ sourceMapData
+ };
}
- function compileCompileDTsFiles(compilerResult: BatchCompileProjectTestCaseResult) {
- const allInputFiles: { emittedFileName: string; code: string; }[] = [];
+ private compileDeclarations(compilerResult: BatchCompileProjectTestCaseResult) {
if (!compilerResult.program) {
return;
}
- const compilerOptions = compilerResult.program.getCompilerOptions();
+ const compilerOptions = compilerResult.program.getCompilerOptions();
+ const allInputFiles: documents.TextDocument[] = [];
+ const rootFiles: string[] = [];
ts.forEach(compilerResult.program.getSourceFiles(), sourceFile => {
if (sourceFile.isDeclarationFile) {
- allInputFiles.unshift({ emittedFileName: sourceFile.fileName, code: sourceFile.text });
+ if (!vpath.isDefaultLibrary(sourceFile.fileName)) {
+ allInputFiles.unshift(new documents.TextDocument(sourceFile.fileName, sourceFile.text));
+ }
+ rootFiles.unshift(sourceFile.fileName);
}
else if (!(compilerOptions.outFile || compilerOptions.out)) {
let emitOutputFilePathWithoutExtension: string;
@@ -384,6 +382,7 @@ class ProjectRunner extends RunnerBase {
const file = findOutputDtsFile(outputDtsFileName);
if (file) {
allInputFiles.unshift(file);
+ rootFiles.unshift(file.meta.get("fileName") || file.file);
}
}
else {
@@ -391,153 +390,90 @@ class ProjectRunner extends RunnerBase {
const outputDtsFile = findOutputDtsFile(outputDtsFileName);
if (!ts.contains(allInputFiles, outputDtsFile)) {
allInputFiles.unshift(outputDtsFile);
+ rootFiles.unshift(outputDtsFile.meta.get("fileName") || outputDtsFile.file);
}
}
});
+ const _vfs = vfs.createFromFileSystem(Harness.IO, /*ignoreCase*/ false, {
+ documents: allInputFiles,
+ cwd: vpath.combine(vfs.srcFolder, this.testCase.projectRoot)
+ });
+
// Dont allow config files since we are compiling existing source options
- return compileProjectFiles(compilerResult.moduleKind, compilerResult.configFileSourceFiles, getInputFiles, getSourceFileText, /*writeFile*/ ts.noop, compilerResult.compilerOptions);
+ const compilerHost = new ProjectCompilerHost(_vfs, compilerResult.compilerOptions, this.testCaseJustName, this.testCase, compilerResult.moduleKind);
+ return this.compileProjectFiles(compilerResult.moduleKind, compilerResult.configFileSourceFiles, () => rootFiles, compilerHost, compilerResult.compilerOptions);
function findOutputDtsFile(fileName: string) {
- return ts.forEach(compilerResult.outputFiles, outputFile => outputFile.emittedFileName === fileName ? outputFile : undefined);
- }
- function getInputFiles() {
- return ts.map(allInputFiles, outputFile => outputFile.emittedFileName);
- }
- function getSourceFileText(fileName: string): string {
- for (const inputFile of allInputFiles) {
- const isMatchingFile = ts.isRootedDiskPath(fileName)
- ? ts.getNormalizedAbsolutePath(inputFile.emittedFileName, getCurrentDirectory()) === fileName
- : inputFile.emittedFileName === fileName;
-
- if (isMatchingFile) {
- return inputFile.code;
- }
- }
- return undefined;
+ return ts.forEach(compilerResult.outputFiles, outputFile => outputFile.meta.get("fileName") === fileName ? outputFile : undefined);
}
}
+ }
- function getErrorsBaseline(compilerResult: CompileProjectFilesResult) {
- const inputSourceFiles = compilerResult.configFileSourceFiles.slice();
- if (compilerResult.program) {
- for (const sourceFile of compilerResult.program.getSourceFiles()) {
- if (!Harness.isDefaultLibraryFile(sourceFile.fileName)) {
- inputSourceFiles.push(sourceFile);
- }
+ function moduleNameToString(moduleKind: ts.ModuleKind) {
+ return moduleKind === ts.ModuleKind.AMD
+ ? "amd"
+ : moduleKind === ts.ModuleKind.CommonJS
+ ? "node"
+ : "none";
+ }
+
+ function getErrorsBaseline(compilerResult: CompileProjectFilesResult) {
+ const inputSourceFiles = compilerResult.configFileSourceFiles.slice();
+ if (compilerResult.program) {
+ for (const sourceFile of compilerResult.program.getSourceFiles()) {
+ if (!Harness.isDefaultLibraryFile(sourceFile.fileName)) {
+ inputSourceFiles.push(sourceFile);
}
}
-
- const inputFiles = inputSourceFiles.map(sourceFile => ({
- unitName: ts.isRootedDiskPath(sourceFile.fileName) ?
- RunnerBase.removeFullPaths(sourceFile.fileName) :
- sourceFile.fileName,
- content: sourceFile.text
- }));
-
- return Harness.Compiler.getErrorBaseline(inputFiles, compilerResult.errors);
}
- const name = "Compiling project for " + testCase.scenario + ": testcase " + testCaseFileName;
-
- describe("projects tests", () => {
- describe(name, () => {
- function verifyCompilerResults(moduleKind: ts.ModuleKind) {
- let compilerResult: BatchCompileProjectTestCaseResult;
-
- function getCompilerResolutionInfo() {
- const resolutionInfo: ProjectRunnerTestCaseResolutionInfo & ts.CompilerOptions = JSON.parse(JSON.stringify(testCase));
- resolutionInfo.resolvedInputFiles = ts.map(compilerResult.program.getSourceFiles(), inputFile => {
- return ts.convertToRelativePath(inputFile.fileName, getCurrentDirectory(), path => Harness.Compiler.getCanonicalFileName(path));
- });
- resolutionInfo.emittedFiles = ts.map(compilerResult.outputFiles, outputFile => {
- return ts.convertToRelativePath(outputFile.emittedFileName, getCurrentDirectory(), path => Harness.Compiler.getCanonicalFileName(path));
- });
- return resolutionInfo;
- }
-
- it(name + ": " + moduleNameToString(moduleKind), () => {
- // Compile using node
- compilerResult = batchCompilerProjectTestCase(moduleKind);
- });
-
- it("Resolution information of (" + moduleNameToString(moduleKind) + "): " + testCaseFileName, () => {
- Harness.Baseline.runBaseline(getBaselineFolder(compilerResult.moduleKind) + testCaseJustName + ".json", () => {
- return JSON.stringify(getCompilerResolutionInfo(), undefined, " ");
- });
- });
-
-
- it("Errors for (" + moduleNameToString(moduleKind) + "): " + testCaseFileName, () => {
- if (compilerResult.errors.length) {
- Harness.Baseline.runBaseline(getBaselineFolder(compilerResult.moduleKind) + testCaseJustName + ".errors.txt", () => {
- return getErrorsBaseline(compilerResult);
- });
- }
- });
+ const inputFiles = inputSourceFiles.map(sourceFile => ({
+ unitName: ts.isRootedDiskPath(sourceFile.fileName) ?
+ RunnerBase.removeFullPaths(sourceFile.fileName) :
+ sourceFile.fileName,
+ content: sourceFile.text
+ }));
- it("Baseline of emitted result (" + moduleNameToString(moduleKind) + "): " + testCaseFileName, () => {
- if (testCase.baselineCheck) {
- const errs: Error[] = [];
- ts.forEach(compilerResult.outputFiles, outputFile => {
- // There may be multiple files with different baselines. Run all and report at the end, else
- // it stops copying the remaining emitted files from 'local/projectOutput' to 'local/project'.
- try {
- Harness.Baseline.runBaseline(getBaselineFolder(compilerResult.moduleKind) + outputFile.fileName, () => {
- try {
- return Harness.IO.readFile(getProjectOutputFolder(outputFile.fileName, compilerResult.moduleKind));
- }
- catch (e) {
- return undefined;
- }
- });
- }
- catch (e) {
- errs.push(e);
- }
- });
- if (errs.length) {
- throw Error(errs.join("\n "));
- }
- }
- });
+ return Harness.Compiler.getErrorBaseline(inputFiles, compilerResult.errors);
+ }
- // it("SourceMapRecord for (" + moduleNameToString(moduleKind) + "): " + testCaseFileName, () => {
- // if (compilerResult.sourceMapData) {
- // Harness.Baseline.runBaseline(getBaselineFolder(compilerResult.moduleKind) + testCaseJustName + ".sourcemap.txt", () => {
- // return Harness.SourceMapRecorder.getSourceMapRecord(compilerResult.sourceMapData, compilerResult.program,
- // ts.filter(compilerResult.outputFiles, outputFile => Harness.Compiler.isJS(outputFile.emittedFileName)));
- // });
- // }
- // });
-
- // Verify that all the generated .d.ts files compile
- it("Errors in generated Dts files for (" + moduleNameToString(moduleKind) + "): " + testCaseFileName, () => {
- if (!compilerResult.errors.length && testCase.declaration) {
- const dTsCompileResult = compileCompileDTsFiles(compilerResult);
- if (dTsCompileResult && dTsCompileResult.errors.length) {
- Harness.Baseline.runBaseline(getBaselineFolder(compilerResult.moduleKind) + testCaseJustName + ".dts.errors.txt", () => {
- return getErrorsBaseline(dTsCompileResult);
- });
- }
+ function createCompilerOptions(testCase: ProjectRunnerTestCase & ts.CompilerOptions, moduleKind: ts.ModuleKind) {
+ // Set the special options that depend on other testcase options
+ const compilerOptions: ts.CompilerOptions = {
+ noErrorTruncation: false,
+ skipDefaultLibCheck: false,
+ moduleResolution: ts.ModuleResolutionKind.Classic,
+ module: moduleKind,
+ mapRoot: testCase.resolveMapRoot && testCase.mapRoot
+ ? vpath.resolve(vfs.srcFolder, testCase.mapRoot)
+ : testCase.mapRoot,
+
+ sourceRoot: testCase.resolveSourceRoot && testCase.sourceRoot
+ ? vpath.resolve(vfs.srcFolder, testCase.sourceRoot)
+ : testCase.sourceRoot
+ };
+
+ // Set the values specified using json
+ const optionNameMap = ts.arrayToMap(ts.optionDeclarations, option => option.name);
+ for (const name in testCase) {
+ if (name !== "mapRoot" && name !== "sourceRoot") {
+ const option = optionNameMap.get(name);
+ if (option) {
+ const optType = option.type;
+ let value = testCase[name];
+ if (!ts.isString(optType)) {
+ const key = value.toLowerCase();
+ const optTypeValue = optType.get(key);
+ if (optTypeValue) {
+ value = optTypeValue;
}
- });
- after(() => {
- compilerResult = undefined;
- });
+ }
+ compilerOptions[option.name] = value;
}
+ }
+ }
- verifyCompilerResults(ts.ModuleKind.CommonJS);
- verifyCompilerResults(ts.ModuleKind.AMD);
-
- 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.
- testCase = undefined;
- testFileText = undefined;
- testCaseJustName = undefined;
- });
- });
- });
+ return compilerOptions;
}
-}
+}
\ No newline at end of file
diff --git a/src/harness/runner.ts b/src/harness/runner.ts
index 7341ecee7ef5f..8bcc7605048f9 100644
--- a/src/harness/runner.ts
+++ b/src/harness/runner.ts
@@ -55,7 +55,7 @@ function createRunner(kind: TestRunnerKind): RunnerBase {
case "fourslash-server":
return new FourSlashRunner(FourSlashTestType.Server);
case "project":
- return new ProjectRunner();
+ return new project.ProjectRunner();
case "rwc":
return new RWCRunner();
case "test262":
@@ -68,10 +68,6 @@ function createRunner(kind: TestRunnerKind): RunnerBase {
ts.Debug.fail(`Unknown runner kind ${kind}`);
}
-if (Harness.IO.tryEnableSourceMapsForHost && /^development$/i.test(Harness.IO.getEnvironmentVariable("NODE_ENV"))) {
- Harness.IO.tryEnableSourceMapsForHost();
-}
-
// users can define tests to run in mytest.config that will override cmd line args, otherwise use cmd line args (test.config), otherwise no options
const mytestconfigFileName = "mytest.config";
@@ -161,13 +157,13 @@ function handleTestConfig() {
case "compiler":
runners.push(new CompilerBaselineRunner(CompilerTestType.Conformance));
runners.push(new CompilerBaselineRunner(CompilerTestType.Regressions));
- runners.push(new ProjectRunner());
+ runners.push(new project.ProjectRunner());
break;
case "conformance":
runners.push(new CompilerBaselineRunner(CompilerTestType.Conformance));
break;
case "project":
- runners.push(new ProjectRunner());
+ runners.push(new project.ProjectRunner());
break;
case "fourslash":
runners.push(new FourSlashRunner(FourSlashTestType.Native));
@@ -208,7 +204,7 @@ function handleTestConfig() {
// TODO: project tests don"t work in the browser yet
if (Utils.getExecutionEnvironment() !== Utils.ExecutionEnvironment.Browser) {
- runners.push(new ProjectRunner());
+ runners.push(new project.ProjectRunner());
}
// language services
diff --git a/src/harness/rwcRunner.ts b/src/harness/rwcRunner.ts
index 54138c2880acd..bfd0153466cea 100644
--- a/src/harness/rwcRunner.ts
+++ b/src/harness/rwcRunner.ts
@@ -6,7 +6,7 @@
/* tslint:disable:no-null-keyword */
namespace RWC {
- function runWithIOLog(ioLog: IoLog, fn: (oldIO: Harness.Io) => void) {
+ function runWithIOLog(ioLog: IoLog, fn: (oldIO: Harness.IO) => void) {
const oldIO = Harness.IO;
const wrappedIO = Playback.wrapIO(oldIO);
@@ -30,7 +30,7 @@ namespace RWC {
let inputFiles: Harness.Compiler.TestFile[] = [];
let otherFiles: Harness.Compiler.TestFile[] = [];
let tsconfigFiles: Harness.Compiler.TestFile[] = [];
- let compilerResult: Harness.Compiler.CompilerResult;
+ let compilerResult: compiler.CompilationResult;
let compilerOptions: ts.CompilerOptions;
const baselineOpts: Harness.Baseline.BaselineOptions = {
Subfolder: "rwc",
@@ -142,8 +142,7 @@ namespace RWC {
opts.options.noLib = true;
// Emit the results
- compilerOptions = undefined;
- const output = Harness.Compiler.compileFiles(
+ compilerResult = Harness.Compiler.compileFiles(
inputFiles,
otherFiles,
/* harnessOptions */ undefined,
@@ -151,9 +150,7 @@ namespace RWC {
// Since each RWC json file specifies its current directory in its json file, we need
// to pass this information in explicitly instead of acquiring it from the process.
currentDirectory);
-
- compilerOptions = output.options;
- compilerResult = output.result;
+ compilerOptions = compilerResult.options;
});
function getHarnessCompilerInputUnit(fileName: string): Harness.Compiler.TestFile {
@@ -173,38 +170,38 @@ namespace RWC {
it("has the expected emitted code", function(this: Mocha.ITestCallbackContext) {
this.timeout(100_000); // Allow longer timeouts for RWC js verification
Harness.Baseline.runMultifileBaseline(baseName, "", () => {
- return Harness.Compiler.iterateOutputs(compilerResult.files);
+ return Harness.Compiler.iterateOutputs(compilerResult.js.values());
}, baselineOpts, [".js", ".jsx"]);
});
it("has the expected declaration file content", () => {
Harness.Baseline.runMultifileBaseline(baseName, "", () => {
- if (!compilerResult.declFilesCode.length) {
+ if (!compilerResult.dts.size) {
return null;
}
- return Harness.Compiler.iterateOutputs(compilerResult.declFilesCode);
+ return Harness.Compiler.iterateOutputs(compilerResult.dts.values());
}, baselineOpts, [".d.ts"]);
});
it("has the expected source maps", () => {
Harness.Baseline.runMultifileBaseline(baseName, "", () => {
- if (!compilerResult.sourceMaps.length) {
+ if (!compilerResult.maps.size) {
return null;
}
- return Harness.Compiler.iterateOutputs(compilerResult.sourceMaps);
+ return Harness.Compiler.iterateOutputs(compilerResult.maps.values());
}, baselineOpts, [".map"]);
});
it("has the expected errors", () => {
Harness.Baseline.runMultifileBaseline(baseName, ".errors.txt", () => {
- if (compilerResult.errors.length === 0) {
+ if (compilerResult.diagnostics.length === 0) {
return null;
}
// Do not include the library in the baselines to avoid noise
const baselineFiles = tsconfigFiles.concat(inputFiles, otherFiles).filter(f => !Harness.isDefaultLibraryFile(f.unitName));
- const errors = compilerResult.errors.filter(e => !e.file || !Harness.isDefaultLibraryFile(e.file.fileName));
+ const errors = compilerResult.diagnostics.filter(e => !e.file || !Harness.isDefaultLibraryFile(e.file.fileName));
return Harness.Compiler.iterateErrorBaseline(baselineFiles, errors);
}, baselineOpts);
});
@@ -212,9 +209,9 @@ namespace RWC {
// Ideally, a generated declaration file will have no errors. But we allow generated
// declaration file errors as part of the baseline.
it("has the expected errors in generated declaration files", () => {
- if (compilerOptions.declaration && !compilerResult.errors.length) {
+ if (compilerOptions.declaration && !compilerResult.diagnostics.length) {
Harness.Baseline.runMultifileBaseline(baseName, ".dts.errors.txt", () => {
- if (compilerResult.errors.length === 0) {
+ if (compilerResult.diagnostics.length === 0) {
return null;
}
@@ -225,7 +222,7 @@ namespace RWC {
compilerResult = undefined;
const declFileCompilationResult = Harness.Compiler.compileDeclarationFiles(declContext);
- return Harness.Compiler.iterateErrorBaseline(tsconfigFiles.concat(declFileCompilationResult.declInputFiles, declFileCompilationResult.declOtherFiles), declFileCompilationResult.declResult.errors);
+ return Harness.Compiler.iterateErrorBaseline(tsconfigFiles.concat(declFileCompilationResult.declInputFiles, declFileCompilationResult.declOtherFiles), declFileCompilationResult.declResult.diagnostics);
}, baselineOpts);
}
});
diff --git a/src/harness/sourceMapRecorder.ts b/src/harness/sourceMapRecorder.ts
index 78a0fe7969df1..1e13e3833a4bb 100644
--- a/src/harness/sourceMapRecorder.ts
+++ b/src/harness/sourceMapRecorder.ts
@@ -206,8 +206,8 @@ namespace Harness.SourceMapRecorder {
let sourceMapSources: string[];
let sourceMapNames: string[];
- let jsFile: Compiler.GeneratedFile;
- let jsLineMap: number[];
+ let jsFile: documents.TextDocument;
+ let jsLineMap: ReadonlyArray;
let tsCode: string;
let tsLineMap: number[];
@@ -216,13 +216,13 @@ namespace Harness.SourceMapRecorder {
let prevWrittenJsLine: number;
let spanMarkerContinues: boolean;
- export function initializeSourceMapSpanWriter(sourceMapRecordWriter: Compiler.WriterAggregator, sourceMapData: ts.SourceMapData, currentJsFile: Compiler.GeneratedFile) {
+ export function initializeSourceMapSpanWriter(sourceMapRecordWriter: Compiler.WriterAggregator, sourceMapData: ts.SourceMapData, currentJsFile: documents.TextDocument) {
sourceMapRecorder = sourceMapRecordWriter;
sourceMapSources = sourceMapData.sourceMapSources;
sourceMapNames = sourceMapData.sourceMapNames;
jsFile = currentJsFile;
- jsLineMap = ts.computeLineStarts(jsFile.code);
+ jsLineMap = jsFile.lineStarts;
spansOnSingleLine = [];
prevWrittenSourcePos = 0;
@@ -290,7 +290,7 @@ namespace Harness.SourceMapRecorder {
assert.isTrue(spansOnSingleLine.length === 1);
sourceMapRecorder.WriteLine("-------------------------------------------------------------------");
- sourceMapRecorder.WriteLine("emittedFile:" + jsFile.fileName);
+ sourceMapRecorder.WriteLine("emittedFile:" + jsFile.file);
sourceMapRecorder.WriteLine("sourceFile:" + sourceMapSources[spansOnSingleLine[0].sourceMapSpan.sourceIndex]);
sourceMapRecorder.WriteLine("-------------------------------------------------------------------");
@@ -313,15 +313,16 @@ namespace Harness.SourceMapRecorder {
writeJsFileLines(jsLineMap.length);
}
- function getTextOfLine(line: number, lineMap: number[], code: string) {
+ function getTextOfLine(line: number, lineMap: ReadonlyArray, code: string) {
const startPos = lineMap[line];
const endPos = lineMap[line + 1];
- return code.substring(startPos, endPos);
+ const text = code.substring(startPos, endPos);
+ return line === 0 ? utils.removeByteOrderMark(text) : text;
}
function writeJsFileLines(endJsLine: number) {
for (; prevWrittenJsLine < endJsLine; prevWrittenJsLine++) {
- sourceMapRecorder.Write(">>>" + getTextOfLine(prevWrittenJsLine, jsLineMap, jsFile.code));
+ sourceMapRecorder.Write(">>>" + getTextOfLine(prevWrittenJsLine, jsLineMap, jsFile.text));
}
}
@@ -417,7 +418,7 @@ namespace Harness.SourceMapRecorder {
// Emit markers
iterateSpans(writeSourceMapMarker);
- const jsFileText = getTextOfLine(currentJsLine, jsLineMap, jsFile.code);
+ const jsFileText = getTextOfLine(currentJsLine, jsLineMap, jsFile.text);
if (prevEmittedCol < jsFileText.length) {
// There is remaining text on this line that will be part of next source span so write marker that continues
writeSourceMapMarker(/*currentSpan*/ undefined, spansOnSingleLine.length, /*endColumn*/ jsFileText.length, /*endContinues*/ true);
@@ -434,13 +435,13 @@ namespace Harness.SourceMapRecorder {
}
}
- export function getSourceMapRecord(sourceMapDataList: ts.SourceMapData[], program: ts.Program, jsFiles: Compiler.GeneratedFile[], declarationFiles: Compiler.GeneratedFile[]) {
+ export function getSourceMapRecord(sourceMapDataList: ReadonlyArray, program: ts.Program, jsFiles: ReadonlyArray, declarationFiles: ReadonlyArray) {
const sourceMapRecorder = new Compiler.WriterAggregator();
for (let i = 0; i < sourceMapDataList.length; i++) {
const sourceMapData = sourceMapDataList[i];
let prevSourceFile: ts.SourceFile;
- let currentFile: Compiler.GeneratedFile;
+ let currentFile: documents.TextDocument;
if (ts.endsWith(sourceMapData.sourceMapFile, ts.Extension.Dts)) {
if (sourceMapDataList.length > jsFiles.length) {
currentFile = declarationFiles[Math.floor(i / 2)]; // When both kinds of source map are present, they alternate js/dts
diff --git a/src/harness/test262Runner.ts b/src/harness/test262Runner.ts
index 6c5b186f2b89e..08a42ed2ff739 100644
--- a/src/harness/test262Runner.ts
+++ b/src/harness/test262Runner.ts
@@ -31,7 +31,7 @@ class Test262BaselineRunner extends RunnerBase {
// Everything declared here should be cleared out in the "after" callback.
let testState: {
filename: string;
- compilerResult: Harness.Compiler.CompilerResult;
+ compilerResult: compiler.CompilationResult;
inputFiles: Harness.Compiler.TestFile[];
};
@@ -52,14 +52,12 @@ class Test262BaselineRunner extends RunnerBase {
compilerResult: undefined,
};
- const output = Harness.Compiler.compileFiles(
+ testState.compilerResult = Harness.Compiler.compileFiles(
[Test262BaselineRunner.helperFile].concat(inputFiles),
/*otherFiles*/ [],
/* harnessOptions */ undefined,
Test262BaselineRunner.options,
- /* currentDirectory */ undefined
- );
- testState.compilerResult = output.result;
+ /* currentDirectory */ undefined);
});
after(() => {
@@ -68,14 +66,14 @@ class Test262BaselineRunner extends RunnerBase {
it("has the expected emitted code", () => {
Harness.Baseline.runBaseline(testState.filename + ".output.js", () => {
- const files = testState.compilerResult.files.filter(f => f.fileName !== Test262BaselineRunner.helpersFilePath);
+ const files = Array.from(testState.compilerResult.js.values()).filter(f => f.file !== Test262BaselineRunner.helpersFilePath);
return Harness.Compiler.collateOutputs(files);
}, Test262BaselineRunner.baselineOptions);
});
it("has the expected errors", () => {
Harness.Baseline.runBaseline(testState.filename + ".errors.txt", () => {
- const errors = testState.compilerResult.errors;
+ const errors = testState.compilerResult.diagnostics;
if (errors.length === 0) {
return null;
}
diff --git a/src/harness/tsconfig.json b/src/harness/tsconfig.json
index faeff1d4bcf1c..fd1d66f394dcf 100644
--- a/src/harness/tsconfig.json
+++ b/src/harness/tsconfig.json
@@ -135,12 +135,19 @@
"../server/session.ts",
"../server/scriptVersionCache.ts",
+ "collections.ts",
+ "utils.ts",
+ "documents.ts",
+ "vpath.ts",
+ "vfs.ts",
+ "compiler.ts",
+ "fakes.ts",
+
"sourceMapRecorder.ts",
"runnerbase.ts",
- "virtualFileSystem.ts",
"harness.ts",
- "virtualFileSystemWithWatch.ts",
"harnessLanguageService.ts",
+ "virtualFileSystemWithWatch.ts",
"fourslashRunner.ts",
"fourslash.ts",
"typeWriter.ts",
diff --git a/src/harness/unittests/configurationExtension.ts b/src/harness/unittests/configurationExtension.ts
index 7e0eb5bad7e30..ca7b06c495d0b 100644
--- a/src/harness/unittests/configurationExtension.ts
+++ b/src/harness/unittests/configurationExtension.ts
@@ -1,116 +1,123 @@
-///
-///
+///
+///
+///
namespace ts {
- const testContentsJson = createMapFromTemplate({
- "/dev/tsconfig.json": {
- extends: "./configs/base",
- files: [
- "main.ts",
- "supplemental.ts"
- ]
- },
- "/dev/tsconfig.nostrictnull.json": {
- extends: "./tsconfig",
- compilerOptions: {
- strictNullChecks: false
+ function createFileSystem(ignoreCase: boolean, cwd: string, root: string) {
+ return new vfs.FileSystem(ignoreCase, {
+ cwd,
+ files: {
+ [root]: {
+ "dev/tsconfig.json": JSON.stringify({
+ extends: "./configs/base",
+ files: [
+ "main.ts",
+ "supplemental.ts"
+ ]
+ }),
+ "dev/tsconfig.nostrictnull.json": JSON.stringify({
+ extends: "./tsconfig",
+ compilerOptions: {
+ strictNullChecks: false
+ }
+ }),
+ "dev/configs/base.json": JSON.stringify({
+ compilerOptions: {
+ allowJs: true,
+ noImplicitAny: true,
+ strictNullChecks: true
+ }
+ }),
+ "dev/configs/tests.json": JSON.stringify({
+ compilerOptions: {
+ preserveConstEnums: true,
+ removeComments: false,
+ sourceMap: true
+ },
+ exclude: [
+ "../tests/baselines",
+ "../tests/scenarios"
+ ],
+ include: [
+ "../tests/**/*.ts"
+ ]
+ }),
+ "dev/circular.json": JSON.stringify({
+ extends: "./circular2",
+ compilerOptions: {
+ module: "amd"
+ }
+ }),
+ "dev/circular2.json": JSON.stringify({
+ extends: "./circular",
+ compilerOptions: {
+ module: "commonjs"
+ }
+ }),
+ "dev/missing.json": JSON.stringify({
+ extends: "./missing2",
+ compilerOptions: {
+ types: []
+ }
+ }),
+ "dev/failure.json": JSON.stringify({
+ extends: "./failure2.json",
+ compilerOptions: {
+ typeRoots: []
+ }
+ }),
+ "dev/failure2.json": JSON.stringify({
+ excludes: ["*.js"]
+ }),
+ "dev/configs/first.json": JSON.stringify({
+ extends: "./base",
+ compilerOptions: {
+ module: "commonjs"
+ },
+ files: ["../main.ts"]
+ }),
+ "dev/configs/second.json": JSON.stringify({
+ extends: "./base",
+ compilerOptions: {
+ module: "amd"
+ },
+ include: ["../supplemental.*"]
+ }),
+ "dev/configs/third.json": JSON.stringify({
+ extends: "./second",
+ compilerOptions: {
+ // tslint:disable-next-line:no-null-keyword
+ module: null
+ },
+ include: ["../supplemental.*"]
+ }),
+ "dev/configs/fourth.json": JSON.stringify({
+ extends: "./third",
+ compilerOptions: {
+ module: "system"
+ },
+ // tslint:disable-next-line:no-null-keyword
+ include: null,
+ files: ["../main.ts"]
+ }),
+ "dev/extends.json": JSON.stringify({ extends: 42 }),
+ "dev/extends2.json": JSON.stringify({ extends: "configs/base" }),
+ "dev/main.ts": "",
+ "dev/supplemental.ts": "",
+ "dev/tests/unit/spec.ts": "",
+ "dev/tests/utils.ts": "",
+ "dev/tests/scenarios/first.json": "",
+ "dev/tests/baselines/first/output.ts": ""
+ }
}
- },
- "/dev/configs/base.json": {
- compilerOptions: {
- allowJs: true,
- noImplicitAny: true,
- strictNullChecks: true
- }
- },
- "/dev/configs/tests.json": {
- compilerOptions: {
- preserveConstEnums: true,
- removeComments: false,
- sourceMap: true
- },
- exclude: [
- "../tests/baselines",
- "../tests/scenarios"
- ],
- include: [
- "../tests/**/*.ts"
- ]
- },
- "/dev/circular.json": {
- extends: "./circular2",
- compilerOptions: {
- module: "amd"
- }
- },
- "/dev/circular2.json": {
- extends: "./circular",
- compilerOptions: {
- module: "commonjs"
- }
- },
- "/dev/missing.json": {
- extends: "./missing2",
- compilerOptions: {
- types: []
- }
- },
- "/dev/failure.json": {
- extends: "./failure2.json",
- compilerOptions: {
- typeRoots: []
- }
- },
- "/dev/failure2.json": {
- excludes: ["*.js"]
- },
- "/dev/configs/first.json": {
- extends: "./base",
- compilerOptions: {
- module: "commonjs"
- },
- files: ["../main.ts"]
- },
- "/dev/configs/second.json": {
- extends: "./base",
- compilerOptions: {
- module: "amd"
- },
- include: ["../supplemental.*"]
- },
- "/dev/configs/third.json": {
- extends: "./second",
- compilerOptions: {
- // tslint:disable-next-line:no-null-keyword
- module: null
- },
- include: ["../supplemental.*"]
- },
- "/dev/configs/fourth.json": {
- extends: "./third",
- compilerOptions: {
- module: "system"
- },
- // tslint:disable-next-line:no-null-keyword
- include: null,
- files: ["../main.ts"]
- },
- "/dev/extends.json": { extends: 42 },
- "/dev/extends2.json": { extends: "configs/base" },
- "/dev/main.ts": "",
- "/dev/supplemental.ts": "",
- "/dev/tests/unit/spec.ts": "",
- "/dev/tests/utils.ts": "",
- "/dev/tests/scenarios/first.json": "",
- "/dev/tests/baselines/first/output.ts": ""
- });
- const testContents = mapEntries(testContentsJson, (k, v) => [k, isString(v) ? v : JSON.stringify(v)]);
+ });
+ }
const caseInsensitiveBasePath = "c:/dev/";
- const caseInsensitiveHost = new Utils.MockParseConfigHost(caseInsensitiveBasePath, /*useCaseSensitiveFileNames*/ false, mapEntries(testContents, (key, content) => [`c:${key}`, content]));
+ const caseInsensitiveHost = new fakes.ParseConfigHost(createFileSystem(/*ignoreCase*/ true, caseInsensitiveBasePath, "c:/"));
const caseSensitiveBasePath = "/dev/";
- const caseSensitiveHost = new Utils.MockParseConfigHost(caseSensitiveBasePath, /*useCaseSensitiveFileNames*/ true, testContents);
+ const caseSensitiveHost = new fakes.ParseConfigHost(createFileSystem(/*ignoreCase*/ false, caseSensitiveBasePath, "/"));
function verifyDiagnostics(actual: Diagnostic[], expected: {code: number, category: DiagnosticCategory, messageText: string}[]) {
assert.isTrue(expected.length === actual.length, `Expected error: ${JSON.stringify(expected)}. Actual error: ${JSON.stringify(actual)}.`);
@@ -124,7 +131,7 @@ namespace ts {
}
describe("configurationExtension", () => {
- forEach<[string, string, Utils.MockParseConfigHost], void>([
+ forEach<[string, string, fakes.ParseConfigHost], void>([
["under a case insensitive host", caseInsensitiveBasePath, caseInsensitiveHost],
["under a case sensitive host", caseSensitiveBasePath, caseSensitiveHost]
], ([testName, basePath, host]) => {
diff --git a/src/harness/unittests/convertCompilerOptionsFromJson.ts b/src/harness/unittests/convertCompilerOptionsFromJson.ts
index f429ce17b30d9..84225a364cb4e 100644
--- a/src/harness/unittests/convertCompilerOptionsFromJson.ts
+++ b/src/harness/unittests/convertCompilerOptionsFromJson.ts
@@ -1,5 +1,7 @@
///
///
+///
+///
namespace ts {
describe("convertCompilerOptionsFromJson", () => {
@@ -31,7 +33,7 @@ namespace ts {
const result = parseJsonText(configFileName, fileText);
assert(!result.parseDiagnostics.length);
assert(!!result.endOfFileToken);
- const host: ParseConfigHost = new Utils.MockParseConfigHost("/apath/", true, []);
+ const host: ParseConfigHost = new fakes.ParseConfigHost(new vfs.FileSystem(/*ignoreCase*/ false, { cwd: "/apath/" }));
const { options: actualCompilerOptions, errors: actualParseErrors } = parseJsonSourceFileConfigFileContent(result, host, "/apath/", /*existingOptions*/ undefined, configFileName);
expectedResult.compilerOptions.configFilePath = configFileName;
diff --git a/src/harness/unittests/convertTypeAcquisitionFromJson.ts b/src/harness/unittests/convertTypeAcquisitionFromJson.ts
index be1ada10a972c..4b43d10275665 100644
--- a/src/harness/unittests/convertTypeAcquisitionFromJson.ts
+++ b/src/harness/unittests/convertTypeAcquisitionFromJson.ts
@@ -1,5 +1,7 @@
///
///
+///
+///
namespace ts {
interface ExpectedResult { typeAcquisition: TypeAcquisition; errors: Diagnostic[]; }
@@ -43,7 +45,7 @@ namespace ts {
const result = parseJsonText(configFileName, fileText);
assert(!result.parseDiagnostics.length);
assert(!!result.endOfFileToken);
- const host: ParseConfigHost = new Utils.MockParseConfigHost("/apath/", true, []);
+ const host: ParseConfigHost = new fakes.ParseConfigHost(new vfs.FileSystem(/*ignoreCase*/ false, { cwd: "/apath/" }));
const { typeAcquisition: actualTypeAcquisition, errors: actualParseErrors } = parseJsonSourceFileConfigFileContent(result, host, "/apath/", /*existingOptions*/ undefined, configFileName);
verifyAcquisition(actualTypeAcquisition, expectedResult);
diff --git a/src/harness/unittests/matchFiles.ts b/src/harness/unittests/matchFiles.ts
index 458dddb22526e..4662dd9f536ee 100644
--- a/src/harness/unittests/matchFiles.ts
+++ b/src/harness/unittests/matchFiles.ts
@@ -1,107 +1,108 @@
-///
-///
+///
+///
+///
namespace ts {
const caseInsensitiveBasePath = "c:/dev/";
const caseInsensitiveTsconfigPath = "c:/dev/tsconfig.json";
- const caseInsensitiveHost = new Utils.MockParseConfigHost(caseInsensitiveBasePath, /*useCaseSensitiveFileNames*/ false, [
- "c:/dev/a.ts",
- "c:/dev/a.d.ts",
- "c:/dev/a.js",
- "c:/dev/b.ts",
- "c:/dev/b.js",
- "c:/dev/c.d.ts",
- "c:/dev/z/a.ts",
- "c:/dev/z/abz.ts",
- "c:/dev/z/aba.ts",
- "c:/dev/z/b.ts",
- "c:/dev/z/bbz.ts",
- "c:/dev/z/bba.ts",
- "c:/dev/x/a.ts",
- "c:/dev/x/aa.ts",
- "c:/dev/x/b.ts",
- "c:/dev/x/y/a.ts",
- "c:/dev/x/y/b.ts",
- "c:/dev/js/a.js",
- "c:/dev/js/b.js",
- "c:/dev/js/d.min.js",
- "c:/dev/js/ab.min.js",
- "c:/ext/ext.ts",
- "c:/ext/b/a..b.ts"
- ]);
+ const caseInsensitiveHost = new fakes.ParseConfigHost(new vfs.FileSystem(/*ignoreCase*/ true, { cwd: caseInsensitiveBasePath, files: {
+ "c:/dev/a.ts": "",
+ "c:/dev/a.d.ts": "",
+ "c:/dev/a.js": "",
+ "c:/dev/b.ts": "",
+ "c:/dev/b.js": "",
+ "c:/dev/c.d.ts": "",
+ "c:/dev/z/a.ts": "",
+ "c:/dev/z/abz.ts": "",
+ "c:/dev/z/aba.ts": "",
+ "c:/dev/z/b.ts": "",
+ "c:/dev/z/bbz.ts": "",
+ "c:/dev/z/bba.ts": "",
+ "c:/dev/x/a.ts": "",
+ "c:/dev/x/aa.ts": "",
+ "c:/dev/x/b.ts": "",
+ "c:/dev/x/y/a.ts": "",
+ "c:/dev/x/y/b.ts": "",
+ "c:/dev/js/a.js": "",
+ "c:/dev/js/b.js": "",
+ "c:/dev/js/d.min.js": "",
+ "c:/dev/js/ab.min.js": "",
+ "c:/ext/ext.ts": "",
+ "c:/ext/b/a..b.ts": "",
+ }}));
const caseSensitiveBasePath = "/dev/";
- const caseSensitiveHost = new Utils.MockParseConfigHost(caseSensitiveBasePath, /*useCaseSensitiveFileNames*/ true, [
- "/dev/a.ts",
- "/dev/a.d.ts",
- "/dev/a.js",
- "/dev/b.ts",
- "/dev/b.js",
- "/dev/A.ts",
- "/dev/B.ts",
- "/dev/c.d.ts",
- "/dev/z/a.ts",
- "/dev/z/abz.ts",
- "/dev/z/aba.ts",
- "/dev/z/b.ts",
- "/dev/z/bbz.ts",
- "/dev/z/bba.ts",
- "/dev/x/a.ts",
- "/dev/x/b.ts",
- "/dev/x/y/a.ts",
- "/dev/x/y/b.ts",
- "/dev/q/a/c/b/d.ts",
- "/dev/js/a.js",
- "/dev/js/b.js",
- ]);
+ const caseSensitiveHost = new fakes.ParseConfigHost(new vfs.FileSystem(/*ignoreCase*/ false, { cwd: caseSensitiveBasePath, files: {
+ "/dev/a.ts": "",
+ "/dev/a.d.ts": "",
+ "/dev/a.js": "",
+ "/dev/b.ts": "",
+ "/dev/b.js": "",
+ "/dev/A.ts": "",
+ "/dev/B.ts": "",
+ "/dev/c.d.ts": "",
+ "/dev/z/a.ts": "",
+ "/dev/z/abz.ts": "",
+ "/dev/z/aba.ts": "",
+ "/dev/z/b.ts": "",
+ "/dev/z/bbz.ts": "",
+ "/dev/z/bba.ts": "",
+ "/dev/x/a.ts": "",
+ "/dev/x/b.ts": "",
+ "/dev/x/y/a.ts": "",
+ "/dev/x/y/b.ts": "",
+ "/dev/q/a/c/b/d.ts": "",
+ "/dev/js/a.js": "",
+ "/dev/js/b.js": "",
+ }}));
- const caseInsensitiveMixedExtensionHost = new Utils.MockParseConfigHost(caseInsensitiveBasePath, /*useCaseSensitiveFileNames*/ false, [
- "c:/dev/a.ts",
- "c:/dev/a.d.ts",
- "c:/dev/a.js",
- "c:/dev/b.tsx",
- "c:/dev/b.d.ts",
- "c:/dev/b.jsx",
- "c:/dev/c.tsx",
- "c:/dev/c.js",
- "c:/dev/d.js",
- "c:/dev/e.jsx",
- "c:/dev/f.other"
- ]);
+ const caseInsensitiveMixedExtensionHost = new fakes.ParseConfigHost(new vfs.FileSystem(/*ignoreCase*/ true, { cwd: caseInsensitiveBasePath, files: {
+ "c:/dev/a.ts": "",
+ "c:/dev/a.d.ts": "",
+ "c:/dev/a.js": "",
+ "c:/dev/b.tsx": "",
+ "c:/dev/b.d.ts": "",
+ "c:/dev/b.jsx": "",
+ "c:/dev/c.tsx": "",
+ "c:/dev/c.js": "",
+ "c:/dev/d.js": "",
+ "c:/dev/e.jsx": "",
+ "c:/dev/f.other": "",
+ }}));
- const caseInsensitiveCommonFoldersHost = new Utils.MockParseConfigHost(caseInsensitiveBasePath, /*useCaseSensitiveFileNames*/ false, [
- "c:/dev/a.ts",
- "c:/dev/a.d.ts",
- "c:/dev/a.js",
- "c:/dev/b.ts",
- "c:/dev/x/a.ts",
- "c:/dev/node_modules/a.ts",
- "c:/dev/bower_components/a.ts",
- "c:/dev/jspm_packages/a.ts"
- ]);
+ const caseInsensitiveCommonFoldersHost = new fakes.ParseConfigHost(new vfs.FileSystem(/*ignoreCase*/ true, { cwd: caseInsensitiveBasePath, files: {
+ "c:/dev/a.ts": "",
+ "c:/dev/a.d.ts": "",
+ "c:/dev/a.js": "",
+ "c:/dev/b.ts": "",
+ "c:/dev/x/a.ts": "",
+ "c:/dev/node_modules/a.ts": "",
+ "c:/dev/bower_components/a.ts": "",
+ "c:/dev/jspm_packages/a.ts": "",
+ }}));
- const caseInsensitiveDottedFoldersHost = new Utils.MockParseConfigHost(caseInsensitiveBasePath, /*useCaseSensitiveFileNames*/ false, [
- "c:/dev/x/d.ts",
- "c:/dev/x/y/d.ts",
- "c:/dev/x/y/.e.ts",
- "c:/dev/x/.y/a.ts",
- "c:/dev/.z/.b.ts",
- "c:/dev/.z/c.ts",
- "c:/dev/w/.u/e.ts",
- "c:/dev/g.min.js/.g/g.ts"
- ]);
+ const caseInsensitiveDottedFoldersHost = new fakes.ParseConfigHost(new vfs.FileSystem(/*ignoreCase*/ true, { cwd: caseInsensitiveBasePath, files: {
+ "c:/dev/x/d.ts": "",
+ "c:/dev/x/y/d.ts": "",
+ "c:/dev/x/y/.e.ts": "",
+ "c:/dev/x/.y/a.ts": "",
+ "c:/dev/.z/.b.ts": "",
+ "c:/dev/.z/c.ts": "",
+ "c:/dev/w/.u/e.ts": "",
+ "c:/dev/g.min.js/.g/g.ts": "",
+ }}));
- const caseInsensitiveOrderingDiffersWithCaseHost = new Utils.MockParseConfigHost(caseInsensitiveBasePath, /*useCaseSensitiveFileNames*/ false, [
- "c:/dev/xylophone.ts",
- "c:/dev/Yosemite.ts",
- "c:/dev/zebra.ts",
- ]);
+ const caseInsensitiveOrderingDiffersWithCaseHost = new fakes.ParseConfigHost(new vfs.FileSystem(/*ignoreCase*/ true, { cwd: caseInsensitiveBasePath, files: {
+ "c:/dev/xylophone.ts": "",
+ "c:/dev/Yosemite.ts": "",
+ "c:/dev/zebra.ts": "",
+ }}));
- const caseSensitiveOrderingDiffersWithCaseHost = new Utils.MockParseConfigHost(caseSensitiveBasePath, /*useCaseSensitiveFileNames*/ true, [
- "/dev/xylophone.ts",
- "/dev/Yosemite.ts",
- "/dev/zebra.ts",
- ]);
+ const caseSensitiveOrderingDiffersWithCaseHost = new fakes.ParseConfigHost(new vfs.FileSystem(/*ignoreCase*/ false, { cwd: caseSensitiveBasePath, files: {
+ "/dev/xylophone.ts": "",
+ "/dev/Yosemite.ts": "",
+ "/dev/zebra.ts": "",
+ }}));
function assertParsed(actual: ParsedCommandLine, expected: ParsedCommandLine): void {
assert.deepEqual(actual.fileNames, expected.fileNames);
diff --git a/src/harness/unittests/organizeImports.ts b/src/harness/unittests/organizeImports.ts
index 7d496b97d9968..4471600b1c143 100644
--- a/src/harness/unittests/organizeImports.ts
+++ b/src/harness/unittests/organizeImports.ts
@@ -1,5 +1,5 @@
///
-///
+///
namespace ts {
diff --git a/src/harness/unittests/paths.ts b/src/harness/unittests/paths.ts
new file mode 100644
index 0000000000000..0cd90be0483d0
--- /dev/null
+++ b/src/harness/unittests/paths.ts
@@ -0,0 +1,292 @@
+describe("core paths", () => {
+ it("normalizeSlashes", () => {
+ assert.strictEqual(ts.normalizeSlashes("a"), "a");
+ assert.strictEqual(ts.normalizeSlashes("a/b"), "a/b");
+ assert.strictEqual(ts.normalizeSlashes("a\\b"), "a/b");
+ assert.strictEqual(ts.normalizeSlashes("\\\\server\\path"), "//server/path");
+ });
+ it("getRootLength", () => {
+ assert.strictEqual(ts.getRootLength("a"), 0);
+ assert.strictEqual(ts.getRootLength("/"), 1);
+ assert.strictEqual(ts.getRootLength("/path"), 1);
+ assert.strictEqual(ts.getRootLength("c:"), 2);
+ assert.strictEqual(ts.getRootLength("c:d"), 0);
+ assert.strictEqual(ts.getRootLength("c:/"), 3);
+ assert.strictEqual(ts.getRootLength("c:\\"), 3);
+ assert.strictEqual(ts.getRootLength("//server"), 8);
+ assert.strictEqual(ts.getRootLength("//server/share"), 9);
+ assert.strictEqual(ts.getRootLength("\\\\server"), 8);
+ assert.strictEqual(ts.getRootLength("\\\\server\\share"), 9);
+ assert.strictEqual(ts.getRootLength("file:///"), 8);
+ assert.strictEqual(ts.getRootLength("file:///path"), 8);
+ assert.strictEqual(ts.getRootLength("file:///c:"), 10);
+ assert.strictEqual(ts.getRootLength("file:///c:d"), 8);
+ assert.strictEqual(ts.getRootLength("file:///c:/path"), 11);
+ assert.strictEqual(ts.getRootLength("file:///c%3a"), 12);
+ assert.strictEqual(ts.getRootLength("file:///c%3ad"), 8);
+ assert.strictEqual(ts.getRootLength("file:///c%3a/path"), 13);
+ assert.strictEqual(ts.getRootLength("file:///c%3A"), 12);
+ assert.strictEqual(ts.getRootLength("file:///c%3Ad"), 8);
+ assert.strictEqual(ts.getRootLength("file:///c%3A/path"), 13);
+ assert.strictEqual(ts.getRootLength("file://localhost"), 16);
+ assert.strictEqual(ts.getRootLength("file://localhost/"), 17);
+ assert.strictEqual(ts.getRootLength("file://localhost/path"), 17);
+ assert.strictEqual(ts.getRootLength("file://localhost/c:"), 19);
+ assert.strictEqual(ts.getRootLength("file://localhost/c:d"), 17);
+ assert.strictEqual(ts.getRootLength("file://localhost/c:/path"), 20);
+ assert.strictEqual(ts.getRootLength("file://localhost/c%3a"), 21);
+ assert.strictEqual(ts.getRootLength("file://localhost/c%3ad"), 17);
+ assert.strictEqual(ts.getRootLength("file://localhost/c%3a/path"), 22);
+ assert.strictEqual(ts.getRootLength("file://localhost/c%3A"), 21);
+ assert.strictEqual(ts.getRootLength("file://localhost/c%3Ad"), 17);
+ assert.strictEqual(ts.getRootLength("file://localhost/c%3A/path"), 22);
+ assert.strictEqual(ts.getRootLength("file://server"), 13);
+ assert.strictEqual(ts.getRootLength("file://server/"), 14);
+ assert.strictEqual(ts.getRootLength("file://server/path"), 14);
+ assert.strictEqual(ts.getRootLength("file://server/c:"), 14);
+ assert.strictEqual(ts.getRootLength("file://server/c:d"), 14);
+ assert.strictEqual(ts.getRootLength("file://server/c:/d"), 14);
+ assert.strictEqual(ts.getRootLength("file://server/c%3a"), 14);
+ assert.strictEqual(ts.getRootLength("file://server/c%3ad"), 14);
+ assert.strictEqual(ts.getRootLength("file://server/c%3a/d"), 14);
+ assert.strictEqual(ts.getRootLength("file://server/c%3A"), 14);
+ assert.strictEqual(ts.getRootLength("file://server/c%3Ad"), 14);
+ assert.strictEqual(ts.getRootLength("file://server/c%3A/d"), 14);
+ assert.strictEqual(ts.getRootLength("http://server"), 13);
+ assert.strictEqual(ts.getRootLength("http://server/path"), 14);
+ });
+ it("isUrl", () => {
+ assert.isFalse(ts.isUrl("a"));
+ assert.isFalse(ts.isUrl("/"));
+ assert.isFalse(ts.isUrl("c:"));
+ assert.isFalse(ts.isUrl("c:d"));
+ assert.isFalse(ts.isUrl("c:/"));
+ assert.isFalse(ts.isUrl("c:\\"));
+ assert.isFalse(ts.isUrl("//server"));
+ assert.isFalse(ts.isUrl("//server/share"));
+ assert.isFalse(ts.isUrl("\\\\server"));
+ assert.isFalse(ts.isUrl("\\\\server\\share"));
+ assert.isTrue(ts.isUrl("file:///path"));
+ assert.isTrue(ts.isUrl("file:///c:"));
+ assert.isTrue(ts.isUrl("file:///c:d"));
+ assert.isTrue(ts.isUrl("file:///c:/path"));
+ assert.isTrue(ts.isUrl("file://server"));
+ assert.isTrue(ts.isUrl("file://server/path"));
+ assert.isTrue(ts.isUrl("http://server"));
+ assert.isTrue(ts.isUrl("http://server/path"));
+ });
+ it("isRootedDiskPath", () => {
+ assert.isFalse(ts.isRootedDiskPath("a"));
+ assert.isTrue(ts.isRootedDiskPath("/"));
+ assert.isTrue(ts.isRootedDiskPath("c:"));
+ assert.isFalse(ts.isRootedDiskPath("c:d"));
+ assert.isTrue(ts.isRootedDiskPath("c:/"));
+ assert.isTrue(ts.isRootedDiskPath("c:\\"));
+ assert.isTrue(ts.isRootedDiskPath("//server"));
+ assert.isTrue(ts.isRootedDiskPath("//server/share"));
+ assert.isTrue(ts.isRootedDiskPath("\\\\server"));
+ assert.isTrue(ts.isRootedDiskPath("\\\\server\\share"));
+ assert.isFalse(ts.isRootedDiskPath("file:///path"));
+ assert.isFalse(ts.isRootedDiskPath("file:///c:"));
+ assert.isFalse(ts.isRootedDiskPath("file:///c:d"));
+ assert.isFalse(ts.isRootedDiskPath("file:///c:/path"));
+ assert.isFalse(ts.isRootedDiskPath("file://server"));
+ assert.isFalse(ts.isRootedDiskPath("file://server/path"));
+ assert.isFalse(ts.isRootedDiskPath("http://server"));
+ assert.isFalse(ts.isRootedDiskPath("http://server/path"));
+ });
+ it("getDirectoryPath", () => {
+ assert.strictEqual(ts.getDirectoryPath(""), "");
+ assert.strictEqual(ts.getDirectoryPath("a"), "");
+ assert.strictEqual(ts.getDirectoryPath("a/b"), "a");
+ assert.strictEqual(ts.getDirectoryPath("/"), "/");
+ assert.strictEqual(ts.getDirectoryPath("/a"), "/");
+ assert.strictEqual(ts.getDirectoryPath("/a/"), "/");
+ assert.strictEqual(ts.getDirectoryPath("/a/b"), "/a");
+ assert.strictEqual(ts.getDirectoryPath("/a/b/"), "/a");
+ assert.strictEqual(ts.getDirectoryPath("c:"), "c:");
+ assert.strictEqual(ts.getDirectoryPath("c:d"), "");
+ assert.strictEqual(ts.getDirectoryPath("c:/"), "c:/");
+ assert.strictEqual(ts.getDirectoryPath("c:/path"), "c:/");
+ assert.strictEqual(ts.getDirectoryPath("c:/path/"), "c:/");
+ assert.strictEqual(ts.getDirectoryPath("//server"), "//server");
+ assert.strictEqual(ts.getDirectoryPath("//server/"), "//server/");
+ assert.strictEqual(ts.getDirectoryPath("//server/share"), "//server/");
+ assert.strictEqual(ts.getDirectoryPath("//server/share/"), "//server/");
+ assert.strictEqual(ts.getDirectoryPath("\\\\server"), "//server");
+ assert.strictEqual(ts.getDirectoryPath("\\\\server\\"), "//server/");
+ assert.strictEqual(ts.getDirectoryPath("\\\\server\\share"), "//server/");
+ assert.strictEqual(ts.getDirectoryPath("\\\\server\\share\\"), "//server/");
+ assert.strictEqual(ts.getDirectoryPath("file:///"), "file:///");
+ assert.strictEqual(ts.getDirectoryPath("file:///path"), "file:///");
+ assert.strictEqual(ts.getDirectoryPath("file:///path/"), "file:///");
+ assert.strictEqual(ts.getDirectoryPath("file:///c:"), "file:///c:");
+ assert.strictEqual(ts.getDirectoryPath("file:///c:d"), "file:///");
+ assert.strictEqual(ts.getDirectoryPath("file:///c:/"), "file:///c:/");
+ assert.strictEqual(ts.getDirectoryPath("file:///c:/path"), "file:///c:/");
+ assert.strictEqual(ts.getDirectoryPath("file:///c:/path/"), "file:///c:/");
+ assert.strictEqual(ts.getDirectoryPath("file://server"), "file://server");
+ assert.strictEqual(ts.getDirectoryPath("file://server/"), "file://server/");
+ assert.strictEqual(ts.getDirectoryPath("file://server/path"), "file://server/");
+ assert.strictEqual(ts.getDirectoryPath("file://server/path/"), "file://server/");
+ assert.strictEqual(ts.getDirectoryPath("http://server"), "http://server");
+ assert.strictEqual(ts.getDirectoryPath("http://server/"), "http://server/");
+ assert.strictEqual(ts.getDirectoryPath("http://server/path"), "http://server/");
+ assert.strictEqual(ts.getDirectoryPath("http://server/path/"), "http://server/");
+ });
+ it("getBaseFileName", () => {
+ assert.strictEqual(ts.getBaseFileName(""), "");
+ assert.strictEqual(ts.getBaseFileName("a"), "a");
+ assert.strictEqual(ts.getBaseFileName("a/"), "a");
+ assert.strictEqual(ts.getBaseFileName("/"), "");
+ assert.strictEqual(ts.getBaseFileName("/a"), "a");
+ assert.strictEqual(ts.getBaseFileName("/a/"), "a");
+ assert.strictEqual(ts.getBaseFileName("/a/b"), "b");
+ assert.strictEqual(ts.getBaseFileName("c:"), "");
+ assert.strictEqual(ts.getBaseFileName("c:d"), "c:d");
+ assert.strictEqual(ts.getBaseFileName("c:/"), "");
+ assert.strictEqual(ts.getBaseFileName("c:\\"), "");
+ assert.strictEqual(ts.getBaseFileName("c:/path"), "path");
+ assert.strictEqual(ts.getBaseFileName("c:/path/"), "path");
+ assert.strictEqual(ts.getBaseFileName("//server"), "");
+ assert.strictEqual(ts.getBaseFileName("//server/"), "");
+ assert.strictEqual(ts.getBaseFileName("//server/share"), "share");
+ assert.strictEqual(ts.getBaseFileName("//server/share/"), "share");
+ assert.strictEqual(ts.getBaseFileName("file:///"), "");
+ assert.strictEqual(ts.getBaseFileName("file:///path"), "path");
+ assert.strictEqual(ts.getBaseFileName("file:///path/"), "path");
+ assert.strictEqual(ts.getBaseFileName("file:///c:"), "");
+ assert.strictEqual(ts.getBaseFileName("file:///c:/"), "");
+ assert.strictEqual(ts.getBaseFileName("file:///c:d"), "c:d");
+ assert.strictEqual(ts.getBaseFileName("file:///c:/d"), "d");
+ assert.strictEqual(ts.getBaseFileName("file:///c:/d/"), "d");
+ assert.strictEqual(ts.getBaseFileName("http://server"), "");
+ assert.strictEqual(ts.getBaseFileName("http://server/"), "");
+ assert.strictEqual(ts.getBaseFileName("http://server/a"), "a");
+ assert.strictEqual(ts.getBaseFileName("http://server/a/"), "a");
+ assert.strictEqual(ts.getBaseFileName("/path/a.ext", ".ext", /*ignoreCase*/ false), "a");
+ assert.strictEqual(ts.getBaseFileName("/path/a.ext", ".EXT", /*ignoreCase*/ true), "a");
+ assert.strictEqual(ts.getBaseFileName("/path/a.ext", "ext", /*ignoreCase*/ false), "a");
+ assert.strictEqual(ts.getBaseFileName("/path/a.b", ".ext", /*ignoreCase*/ false), "a.b");
+ assert.strictEqual(ts.getBaseFileName("/path/a.b", [".b", ".c"], /*ignoreCase*/ false), "a");
+ assert.strictEqual(ts.getBaseFileName("/path/a.c", [".b", ".c"], /*ignoreCase*/ false), "a");
+ assert.strictEqual(ts.getBaseFileName("/path/a.d", [".b", ".c"], /*ignoreCase*/ false), "a.d");
+ });
+ it("getAnyExtensionFromPath", () => {
+ assert.strictEqual(ts.getAnyExtensionFromPath(""), "");
+ assert.strictEqual(ts.getAnyExtensionFromPath(".ext"), ".ext");
+ assert.strictEqual(ts.getAnyExtensionFromPath("a.ext"), ".ext");
+ assert.strictEqual(ts.getAnyExtensionFromPath("/a.ext"), ".ext");
+ assert.strictEqual(ts.getAnyExtensionFromPath("a.ext/"), ".ext");
+ assert.strictEqual(ts.getAnyExtensionFromPath("a.ext", ".ext", /*ignoreCase*/ false), ".ext");
+ assert.strictEqual(ts.getAnyExtensionFromPath("a.ext", ".EXT", /*ignoreCase*/ true), ".ext");
+ assert.strictEqual(ts.getAnyExtensionFromPath("a.ext", "ext", /*ignoreCase*/ false), ".ext");
+ assert.strictEqual(ts.getAnyExtensionFromPath("a.b", ".ext", /*ignoreCase*/ false), "");
+ assert.strictEqual(ts.getAnyExtensionFromPath("a.b", [".b", ".c"], /*ignoreCase*/ false), ".b");
+ assert.strictEqual(ts.getAnyExtensionFromPath("a.c", [".b", ".c"], /*ignoreCase*/ false), ".c");
+ assert.strictEqual(ts.getAnyExtensionFromPath("a.d", [".b", ".c"], /*ignoreCase*/ false), "");
+ });
+ it("getPathComponents", () => {
+ assert.deepEqual(ts.getPathComponents(""), [""]);
+ assert.deepEqual(ts.getPathComponents("a"), ["", "a"]);
+ assert.deepEqual(ts.getPathComponents("./a"), ["", ".", "a"]);
+ assert.deepEqual(ts.getPathComponents("/"), ["/"]);
+ assert.deepEqual(ts.getPathComponents("/a"), ["/", "a"]);
+ assert.deepEqual(ts.getPathComponents("/a/"), ["/", "a"]);
+ assert.deepEqual(ts.getPathComponents("c:"), ["c:"]);
+ assert.deepEqual(ts.getPathComponents("c:d"), ["", "c:d"]);
+ assert.deepEqual(ts.getPathComponents("c:/"), ["c:/"]);
+ assert.deepEqual(ts.getPathComponents("c:/path"), ["c:/", "path"]);
+ assert.deepEqual(ts.getPathComponents("//server"), ["//server"]);
+ assert.deepEqual(ts.getPathComponents("//server/"), ["//server/"]);
+ assert.deepEqual(ts.getPathComponents("//server/share"), ["//server/", "share"]);
+ assert.deepEqual(ts.getPathComponents("file:///"), ["file:///"]);
+ assert.deepEqual(ts.getPathComponents("file:///path"), ["file:///", "path"]);
+ assert.deepEqual(ts.getPathComponents("file:///c:"), ["file:///c:"]);
+ assert.deepEqual(ts.getPathComponents("file:///c:d"), ["file:///", "c:d"]);
+ assert.deepEqual(ts.getPathComponents("file:///c:/"), ["file:///c:/"]);
+ assert.deepEqual(ts.getPathComponents("file:///c:/path"), ["file:///c:/", "path"]);
+ assert.deepEqual(ts.getPathComponents("file://server"), ["file://server"]);
+ assert.deepEqual(ts.getPathComponents("file://server/"), ["file://server/"]);
+ assert.deepEqual(ts.getPathComponents("file://server/path"), ["file://server/", "path"]);
+ assert.deepEqual(ts.getPathComponents("http://server"), ["http://server"]);
+ assert.deepEqual(ts.getPathComponents("http://server/"), ["http://server/"]);
+ assert.deepEqual(ts.getPathComponents("http://server/path"), ["http://server/", "path"]);
+ });
+ it("reducePathComponents", () => {
+ assert.deepEqual(ts.reducePathComponents([]), []);
+ assert.deepEqual(ts.reducePathComponents([""]), [""]);
+ assert.deepEqual(ts.reducePathComponents(["", "."]), [""]);
+ assert.deepEqual(ts.reducePathComponents(["", ".", "a"]), ["", "a"]);
+ assert.deepEqual(ts.reducePathComponents(["", "a", "."]), ["", "a"]);
+ assert.deepEqual(ts.reducePathComponents(["", ".."]), ["", ".."]);
+ assert.deepEqual(ts.reducePathComponents(["", "..", ".."]), ["", "..", ".."]);
+ assert.deepEqual(ts.reducePathComponents(["", "..", ".", ".."]), ["", "..", ".."]);
+ assert.deepEqual(ts.reducePathComponents(["", "a", ".."]), [""]);
+ assert.deepEqual(ts.reducePathComponents(["", "..", "a"]), ["", "..", "a"]);
+ assert.deepEqual(ts.reducePathComponents(["/"]), ["/"]);
+ assert.deepEqual(ts.reducePathComponents(["/", "."]), ["/"]);
+ assert.deepEqual(ts.reducePathComponents(["/", ".."]), ["/"]);
+ assert.deepEqual(ts.reducePathComponents(["/", "a", ".."]), ["/"]);
+ });
+ it("combinePaths", () => {
+ assert.strictEqual(ts.combinePaths("/", "/node_modules/@types"), "/node_modules/@types");
+ assert.strictEqual(ts.combinePaths("/a/..", ""), "/a/..");
+ assert.strictEqual(ts.combinePaths("/a/..", "b"), "/a/../b");
+ assert.strictEqual(ts.combinePaths("/a/..", "b/"), "/a/../b/");
+ assert.strictEqual(ts.combinePaths("/a/..", "/"), "/");
+ assert.strictEqual(ts.combinePaths("/a/..", "/b"), "/b");
+ });
+ it("resolvePath", () => {
+ assert.strictEqual(ts.resolvePath(""), "");
+ assert.strictEqual(ts.resolvePath("."), "");
+ assert.strictEqual(ts.resolvePath("./"), "");
+ assert.strictEqual(ts.resolvePath(".."), "..");
+ assert.strictEqual(ts.resolvePath("../"), "../");
+ assert.strictEqual(ts.resolvePath("/"), "/");
+ assert.strictEqual(ts.resolvePath("/."), "/");
+ assert.strictEqual(ts.resolvePath("/./"), "/");
+ assert.strictEqual(ts.resolvePath("/../"), "/");
+ assert.strictEqual(ts.resolvePath("/a"), "/a");
+ assert.strictEqual(ts.resolvePath("/a/"), "/a/");
+ assert.strictEqual(ts.resolvePath("/a/."), "/a");
+ assert.strictEqual(ts.resolvePath("/a/./"), "/a/");
+ assert.strictEqual(ts.resolvePath("/a/./b"), "/a/b");
+ assert.strictEqual(ts.resolvePath("/a/./b/"), "/a/b/");
+ assert.strictEqual(ts.resolvePath("/a/.."), "/");
+ assert.strictEqual(ts.resolvePath("/a/../"), "/");
+ assert.strictEqual(ts.resolvePath("/a/../b"), "/b");
+ assert.strictEqual(ts.resolvePath("/a/../b/"), "/b/");
+ assert.strictEqual(ts.resolvePath("/a/..", "b"), "/b");
+ assert.strictEqual(ts.resolvePath("/a/..", "/"), "/");
+ assert.strictEqual(ts.resolvePath("/a/..", "b/"), "/b/");
+ assert.strictEqual(ts.resolvePath("/a/..", "/b"), "/b");
+ assert.strictEqual(ts.resolvePath("/a/.", "b"), "/a/b");
+ assert.strictEqual(ts.resolvePath("/a/.", "."), "/a");
+ assert.strictEqual(ts.resolvePath("a", "b", "c"), "a/b/c");
+ assert.strictEqual(ts.resolvePath("a", "b", "/c"), "/c");
+ assert.strictEqual(ts.resolvePath("a", "b", "../c"), "a/c");
+ });
+ it("getPathRelativeTo", () => {
+ assert.strictEqual(ts.getRelativePath("/", "/", /*ignoreCase*/ false), "");
+ assert.strictEqual(ts.getRelativePath("/a", "/a", /*ignoreCase*/ false), "");
+ assert.strictEqual(ts.getRelativePath("/a/", "/a", /*ignoreCase*/ false), "");
+ assert.strictEqual(ts.getRelativePath("/a", "/", /*ignoreCase*/ false), "..");
+ assert.strictEqual(ts.getRelativePath("/a", "/b", /*ignoreCase*/ false), "../b");
+ assert.strictEqual(ts.getRelativePath("/a/b", "/b", /*ignoreCase*/ false), "../../b");
+ assert.strictEqual(ts.getRelativePath("/a/b/c", "/b", /*ignoreCase*/ false), "../../../b");
+ assert.strictEqual(ts.getRelativePath("/a/b/c", "/b/c", /*ignoreCase*/ false), "../../../b/c");
+ assert.strictEqual(ts.getRelativePath("/a/b/c", "/a/b", /*ignoreCase*/ false), "..");
+ assert.strictEqual(ts.getRelativePath("c:", "d:", /*ignoreCase*/ false), "d:/");
+ assert.strictEqual(ts.getRelativePath("file:///", "file:///", /*ignoreCase*/ false), "");
+ assert.strictEqual(ts.getRelativePath("file:///a", "file:///a", /*ignoreCase*/ false), "");
+ assert.strictEqual(ts.getRelativePath("file:///a/", "file:///a", /*ignoreCase*/ false), "");
+ assert.strictEqual(ts.getRelativePath("file:///a", "file:///", /*ignoreCase*/ false), "..");
+ assert.strictEqual(ts.getRelativePath("file:///a", "file:///b", /*ignoreCase*/ false), "../b");
+ assert.strictEqual(ts.getRelativePath("file:///a/b", "file:///b", /*ignoreCase*/ false), "../../b");
+ assert.strictEqual(ts.getRelativePath("file:///a/b/c", "file:///b", /*ignoreCase*/ false), "../../../b");
+ assert.strictEqual(ts.getRelativePath("file:///a/b/c", "file:///b/c", /*ignoreCase*/ false), "../../../b/c");
+ assert.strictEqual(ts.getRelativePath("file:///a/b/c", "file:///a/b", /*ignoreCase*/ false), "..");
+ assert.strictEqual(ts.getRelativePath("file:///c:", "file:///d:", /*ignoreCase*/ false), "file:///d:/");
+ });
+});
\ No newline at end of file
diff --git a/src/harness/unittests/programMissingFiles.ts b/src/harness/unittests/programMissingFiles.ts
index 2a7f6d24ea787..66b2ab4cf7f4f 100644
--- a/src/harness/unittests/programMissingFiles.ts
+++ b/src/harness/unittests/programMissingFiles.ts
@@ -1,4 +1,6 @@
///
+///
+///
namespace ts {
function verifyMissingFilePaths(missingPaths: ReadonlyArray, expected: ReadonlyArray) {
@@ -22,33 +24,25 @@ namespace ts {
const emptyFileName = "empty.ts";
const emptyFileRelativePath = "./" + emptyFileName;
- const emptyFile: Harness.Compiler.TestFile = {
- unitName: emptyFileName,
- content: ""
- };
+ const emptyFile = new documents.TextDocument(emptyFileName, "");
const referenceFileName = "reference.ts";
const referenceFileRelativePath = "./" + referenceFileName;
- const referenceFile: Harness.Compiler.TestFile = {
- unitName: referenceFileName,
- content:
- "/// \n" + // Absolute
- "/// \n" + // Relative
- "/// \n" + // Unqualified
- "/// \n" // No extension
- };
-
- const testCompilerHost = Harness.Compiler.createCompilerHost(
- /*inputFiles*/ [emptyFile, referenceFile],
- /*writeFile*/ undefined,
- /*scriptTarget*/ undefined,
- /*useCaseSensitiveFileNames*/ false,
- /*currentDirectory*/ "d:\\pretend\\",
- /*newLineKind*/ NewLineKind.LineFeed,
- /*libFiles*/ undefined
+ const referenceFile = new documents.TextDocument(referenceFileName,
+ "/// \n" + // Absolute
+ "/// \n" + // Relative
+ "/// \n" + // Unqualified
+ "/// \n" // No extension
);
+ const testCompilerHost = new fakes.CompilerHost(
+ vfs.createFromFileSystem(
+ Harness.IO,
+ /*ignoreCase*/ true,
+ { documents: [emptyFile, referenceFile], cwd: "d:\\pretend\\" }),
+ { newLine: NewLineKind.LineFeed });
+
it("handles no missing root files", () => {
const program = createProgram([emptyFileRelativePath], options, testCompilerHost);
const missing = program.getMissingFilePaths();
diff --git a/src/harness/unittests/publicApi.ts b/src/harness/unittests/publicApi.ts
index 35acfec57f530..b99557b67bdcd 100644
--- a/src/harness/unittests/publicApi.ts
+++ b/src/harness/unittests/publicApi.ts
@@ -14,13 +14,12 @@ describe("Public APIs", () => {
});
it("should compile", () => {
- const testFile: Harness.Compiler.TestFile = {
- unitName: builtFile,
- content: fileContent
- };
- const inputFiles = [testFile];
- const output = Harness.Compiler.compileFiles(inputFiles, [], /*harnessSettings*/ undefined, /*options*/ {}, /*currentDirectory*/ undefined);
- assert(!output.result.errors || !output.result.errors.length, Harness.Compiler.minimalDiagnosticsToString(output.result.errors, /*pretty*/ true));
+ const fs = vfs.createFromFileSystem(Harness.IO, /*ignoreCase*/ false);
+ fs.linkSync(`${vfs.builtFolder}/${fileName}`, `${vfs.srcFolder}/${fileName}`);
+ const sys = new fakes.System(fs);
+ const host = new fakes.CompilerHost(sys);
+ const result = compiler.compileFiles(host, [`${vfs.srcFolder}/${fileName}`], {});
+ assert(!result.diagnostics || !result.diagnostics.length, Harness.Compiler.minimalDiagnosticsToString(result.diagnostics, /*pretty*/ true));
});
}
diff --git a/src/harness/unittests/symbolWalker.ts b/src/harness/unittests/symbolWalker.ts
index 6d38fbb5198c9..f793c237da302 100644
--- a/src/harness/unittests/symbolWalker.ts
+++ b/src/harness/unittests/symbolWalker.ts
@@ -4,17 +4,13 @@ namespace ts {
describe("Symbol Walker", () => {
function test(description: string, source: string, verifier: (file: SourceFile, checker: TypeChecker) => void) {
it(description, () => {
- let {result} = Harness.Compiler.compileFiles([{
+ const result = Harness.Compiler.compileFiles([{
unitName: "main.ts",
content: source
}], [], {}, {}, "/");
- let file = result.program.getSourceFile("main.ts");
- let checker = result.program.getTypeChecker();
+ const file = result.program.getSourceFile("main.ts");
+ const checker = result.program.getTypeChecker();
verifier(file, checker);
-
- result = undefined;
- file = undefined;
- checker = undefined;
});
}
diff --git a/src/harness/unittests/tsconfigParsing.ts b/src/harness/unittests/tsconfigParsing.ts
index cf2ef33866bb2..500204fbde84b 100644
--- a/src/harness/unittests/tsconfigParsing.ts
+++ b/src/harness/unittests/tsconfigParsing.ts
@@ -1,5 +1,7 @@
///
///
+///
+///
namespace ts {
describe("parseConfigFileTextToJson", () => {
@@ -31,13 +33,15 @@ namespace ts {
function getParsedCommandJson(jsonText: string, configFileName: string, basePath: string, allFileList: string[]) {
const parsed = parseConfigFileTextToJson(configFileName, jsonText);
- const host: ParseConfigHost = new Utils.MockParseConfigHost(basePath, true, allFileList);
+ const files = allFileList.reduce((files, value) => (files[value] = "", files), {} as vfs.FileSet);
+ const host: ParseConfigHost = new fakes.ParseConfigHost(new vfs.FileSystem(/*ignoreCase*/ false, { cwd: basePath, files: { "/": {}, ...files } }));
return parseJsonConfigFileContent(parsed.config, host, basePath, /*existingOptions*/ undefined, configFileName);
}
function getParsedCommandJsonNode(jsonText: string, configFileName: string, basePath: string, allFileList: string[]) {
const parsed = parseJsonText(configFileName, jsonText);
- const host: ParseConfigHost = new Utils.MockParseConfigHost(basePath, true, allFileList);
+ const files = allFileList.reduce((files, value) => (files[value] = "", files), {} as vfs.FileSet);
+ const host: ParseConfigHost = new fakes.ParseConfigHost(new vfs.FileSystem(/*ignoreCase*/ false, { cwd: basePath, files: { "/": {}, ...files } }));
return parseJsonSourceFileConfigFileContent(parsed, host, basePath, /*existingOptions*/ undefined, configFileName);
}
@@ -109,7 +113,7 @@ namespace ts {
"xx//file.d.ts"
]
}`, { config: { exclude: ["xx//file.d.ts"] } });
- assertParseResult(
+ assertParseResult(
`{
"exclude": [
"xx/*file.d.ts*/"
diff --git a/src/harness/utils.ts b/src/harness/utils.ts
new file mode 100644
index 0000000000000..03e9e56169b01
--- /dev/null
+++ b/src/harness/utils.ts
@@ -0,0 +1,114 @@
+/**
+ * Common utilities
+ */
+namespace utils {
+ 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;
+ const allCommentRegExp = /(['"])(?:(?!\1).|\\[^])*\1|(\/\*[^]*?\*\/|\/\/[^\r\n\u2028\u2029]*[\r\n\u2028\u2029]*)/g;
+
+ export const enum CommentRemoval {
+ leading,
+ trailing,
+ leadingAndTrailing,
+ all
+ }
+
+ export function removeComments(text: string, removal: CommentRemoval) {
+ switch (removal) {
+ case CommentRemoval.leading:
+ return text.replace(leadingCommentRegExp, "");
+ case CommentRemoval.trailing:
+ return text.replace(trailingCommentRegExp, "");
+ case CommentRemoval.leadingAndTrailing:
+ return text.replace(leadingAndTrailingCommentRegExp, "");
+ case CommentRemoval.all:
+ return text.replace(allCommentRegExp, (match, quote) => quote ? match : "");
+ }
+ }
+
+ const testPathPrefixRegExp = /(?:(file:\/{3})|\/)\.(ts|lib|src)\//g;
+ export function removeTestPathPrefixes(text: string, retainTrailingDirectorySeparator?: boolean) {
+ return text !== undefined ? text.replace(testPathPrefixRegExp, (_, scheme) => scheme || (retainTrailingDirectorySeparator ? "/" : "")) : undefined;
+ }
+
+ /**
+ * Removes leading indentation from a template literal string.
+ */
+ export function dedent(array: TemplateStringsArray, ...args: any[]) {
+ let text = array[0];
+ for (let i = 0; i < args.length; i++) {
+ text += args[i];
+ text += array[i + 1];
+ }
+
+ const lineTerminatorRegExp = /\r\n?|\n/g;
+ const lines: string[] = [];
+ const lineTerminators: string[] = [];
+ let match: RegExpExecArray | null;
+ let lineStart = 0;
+ while (match = lineTerminatorRegExp.exec(text)) {
+ if (lineStart !== match.index || lines.length > 0) {
+ lines.push(text.slice(lineStart, match.index));
+ lineTerminators.push(match[0]);
+ }
+ lineStart = match.index + match[0].length;
+ }
+
+ if (lineStart < text.length) {
+ lines.push(text.slice(lineStart));
+ }
+
+ const indentation = guessIndentation(lines);
+
+ let result = "";
+ for (let i = 0; i < lines.length; i++) {
+ const lineText = lines[i];
+ const line = indentation ? lineText.slice(indentation) : lineText;
+ result += line;
+ if (i < lineTerminators.length) {
+ result += lineTerminators[i];
+ }
+ }
+ return result;
+ }
+
+ function guessIndentation(lines: string[]) {
+ let indentation: number;
+ for (const line of lines) {
+ for (let i = 0; i < line.length && (indentation === undefined || i < indentation); i++) {
+ if (!ts.isWhiteSpaceLike(line.charCodeAt(i))) {
+ if (indentation === undefined || i < indentation) {
+ indentation = i;
+ break;
+ }
+ }
+ }
+ }
+ return indentation;
+ }
+
+ export function toUtf8(text: string): string {
+ return new Buffer(text).toString("utf8");
+ }
+
+ export function getByteOrderMarkLength(text: string): number {
+ if (text.length >= 1) {
+ const ch0 = text.charCodeAt(0);
+ if (ch0 === 0xfeff) return 1;
+ if (ch0 === 0xfe) return text.length >= 2 && text.charCodeAt(1) === 0xff ? 2 : 0;
+ if (ch0 === 0xff) return text.length >= 2 && text.charCodeAt(1) === 0xfe ? 2 : 0;
+ if (ch0 === 0xef) return text.length >= 3 && text.charCodeAt(1) === 0xbb && text.charCodeAt(2) === 0xbf ? 3 : 0;
+ }
+ return 0;
+ }
+
+ export function removeByteOrderMark(text: string): string {
+ const length = getByteOrderMarkLength(text);
+ return length ? text.slice(length) : text;
+ }
+
+ export function addUTF8ByteOrderMark(text: string) {
+ return getByteOrderMarkLength(text) === 0 ? "\u00EF\u00BB\u00BF" + text : text;
+ }
+}
\ No newline at end of file
diff --git a/src/harness/vfs.ts b/src/harness/vfs.ts
new file mode 100644
index 0000000000000..e1107537f9e4e
--- /dev/null
+++ b/src/harness/vfs.ts
@@ -0,0 +1,1306 @@
+// tslint:disable:no-null-keyword
+namespace vfs {
+ /**
+ * Posix-style path to the TypeScript compiler build outputs (including tsc.js, lib.d.ts, etc.)
+ */
+ export const builtFolder = "/.ts";
+
+ /**
+ * Posix-style path to additional test libraries
+ */
+ export const testLibFolder = "/.lib";
+
+ /**
+ * Posix-style path to sources under test
+ */
+ export const srcFolder = "/.src";
+
+ // file type
+ const S_IFMT = 0o170000; // file type
+ const S_IFSOCK = 0o140000; // socket
+ const S_IFLNK = 0o120000; // symbolic link
+ const S_IFREG = 0o100000; // regular file
+ const S_IFBLK = 0o060000; // block device
+ const S_IFDIR = 0o040000; // directory
+ const S_IFCHR = 0o020000; // character device
+ const S_IFIFO = 0o010000; // FIFO
+
+ let devCount = 0; // A monotonically increasing count of device ids
+ let inoCount = 0; // A monotonically increasing count of inodes
+
+ /**
+ * Represents a virtual POSIX-like file system.
+ */
+ export class FileSystem {
+ /** Indicates whether the file system is case-sensitive (`false`) or case-insensitive (`true`). */
+ public readonly ignoreCase: boolean;
+
+ /** Gets the comparison function used to compare two paths. */
+ public readonly stringComparer: (a: string, b: string) => number;
+
+ // lazy-initialized state that should be mutable even if the FileSystem is frozen.
+ private _lazy: {
+ links?: collections.SortedMap;
+ shadows?: Map;
+ meta?: collections.Metadata;
+ } = {};
+
+ private _cwd: string; // current working directory
+ private _time: number | Date | (() => number | Date);
+ private _shadowRoot: FileSystem | undefined;
+ private _dirStack: string[] | undefined;
+
+ constructor(ignoreCase: boolean, options: FileSystemOptions = {}) {
+ const { time = -1, files, meta } = options;
+ this.ignoreCase = ignoreCase;
+ this.stringComparer = this.ignoreCase ? vpath.compareCaseInsensitive : vpath.compareCaseSensitive;
+ this._time = time;
+
+ if (meta) {
+ for (const key of Object.keys(meta)) {
+ this.meta.set(key, meta[key]);
+ }
+ }
+
+ if (files) {
+ this._applyFiles(files, /*dirname*/ "");
+ }
+
+ let cwd = options.cwd;
+ if ((!cwd || !vpath.isRoot(cwd)) && this._lazy.links) {
+ const iterator = collections.getIterator(this._lazy.links.keys());
+ try {
+ for (let i = collections.nextResult(iterator); i; i = collections.nextResult(iterator)) {
+ const name = i.value;
+ cwd = cwd ? vpath.resolve(name, cwd) : name;
+ break;
+ }
+ }
+ finally {
+ collections.closeIterator(iterator);
+ }
+ }
+
+ if (cwd) {
+ vpath.validate(cwd, vpath.ValidationFlags.Absolute);
+ this.mkdirpSync(cwd);
+ }
+
+ this._cwd = cwd || "";
+ }
+
+ /**
+ * Gets metadata for this `FileSystem`.
+ */
+ public get meta(): collections.Metadata {
+ if (!this._lazy.meta) {
+ this._lazy.meta = new collections.Metadata(this._shadowRoot ? this._shadowRoot.meta : undefined);
+ }
+ return this._lazy.meta;
+ }
+
+ /**
+ * Gets a value indicating whether the file system is read-only.
+ */
+ public get isReadonly() {
+ return Object.isFrozen(this);
+ }
+
+ /**
+ * Makes the file system read-only.
+ */
+ public makeReadonly() {
+ Object.freeze(this);
+ return this;
+ }
+
+ /**
+ * Gets the file system shadowed by this file system.
+ */
+ public get shadowRoot() {
+ return this._shadowRoot;
+ }
+
+ /**
+ * Gets a shadow copy of this file system. Changes to the shadow copy do not affect the
+ * original, allowing multiple copies of the same core file system without multiple copies
+ * of the same data.
+ */
+ public shadow(ignoreCase = this.ignoreCase) {
+ if (!this.isReadonly) throw new Error("Cannot shadow a mutable file system.");
+ if (ignoreCase && !this.ignoreCase) throw new Error("Cannot create a case-insensitive file system from a case-sensitive one.");
+ const fs = new FileSystem(ignoreCase, { time: this._time });
+ fs._shadowRoot = this;
+ fs._cwd = this._cwd;
+ return fs;
+ }
+
+ /**
+ * Gets or sets the timestamp (in milliseconds) used for file status, returning the previous timestamp.
+ *
+ * @link http://pubs.opengroup.org/onlinepubs/9699919799/functions/time.html
+ */
+ public time(value?: number | Date | (() => number | Date)): number {
+ if (value !== undefined && this.isReadonly) throw createIOError("EPERM");
+ let result = this._time;
+ if (typeof result === "function") result = result();
+ if (typeof result === "object") result = result.getTime();
+ if (result === -1) result = Date.now();
+ if (value !== undefined) {
+ this._time = value;
+ }
+ return result;
+ }
+
+ /**
+ * Gets the metadata object for a path.
+ * @param path
+ */
+ public filemeta(path: string): collections.Metadata {
+ const { node } = this._walk(this._resolve(path));
+ if (!node) throw createIOError("ENOENT");
+ return this._filemeta(node);
+ }
+
+ private _filemeta(node: Inode): collections.Metadata {
+ if (!node.meta) {
+ const parentMeta = node.shadowRoot && this._shadowRoot && this._shadowRoot._filemeta(node.shadowRoot);
+ node.meta = new collections.Metadata(parentMeta);
+ }
+ return node.meta;
+ }
+
+ /**
+ * Get the pathname of the current working directory.
+ *
+ * @link - http://pubs.opengroup.org/onlinepubs/9699919799/functions/getcwd.html
+ */
+ public cwd() {
+ if (!this._cwd) throw new Error("The current working directory has not been set.");
+ const { node } = this._walk(this._cwd);
+ if (!node) throw createIOError("ENOENT");
+ if (!isDirectory(node)) throw createIOError("ENOTDIR");
+ return this._cwd;
+ }
+
+ /**
+ * Changes the current working directory.
+ *
+ * @link http://pubs.opengroup.org/onlinepubs/9699919799/functions/chdir.html
+ */
+ public chdir(path: string) {
+ if (this.isReadonly) throw createIOError("EPERM");
+ path = this._resolve(path);
+ const { node } = this._walk(path);
+ if (!node) throw createIOError("ENOENT");
+ if (!isDirectory(node)) throw createIOError("ENOTDIR");
+ this._cwd = path;
+ }
+
+ /**
+ * Pushes the current directory onto the directory stack and changes the current working directory to the supplied path.
+ */
+ public pushd(path?: string) {
+ if (this.isReadonly) throw createIOError("EPERM");
+ if (path) path = this._resolve(path);
+ if (this._cwd) {
+ if (!this._dirStack) this._dirStack = [];
+ this._dirStack.push(this._cwd);
+ }
+ if (path && path !== this._cwd) {
+ this.chdir(path);
+ }
+ }
+
+ /**
+ * Pops the previous directory from the location stack and changes the current directory to that directory.
+ */
+ public popd() {
+ if (this.isReadonly) throw createIOError("EPERM");
+ const path = this._dirStack && this._dirStack.pop();
+ if (path) {
+ this.chdir(path);
+ }
+ }
+
+ /**
+ * Update the file system with a set of files.
+ */
+ public apply(files: FileSet) {
+ this._applyFiles(files, this._cwd);
+ }
+
+ /**
+ * Scan file system entries along a path. If `path` is a symbolic link, it is dereferenced.
+ * @param path The path at which to start the scan.
+ * @param axis The axis along which to traverse.
+ * @param traversal The traversal scheme to use.
+ */
+ public scanSync(path: string, axis: Axis, traversal: Traversal) {
+ path = this._resolve(path);
+ const results: string[] = [];
+ this._scan(path, this._stat(this._walk(path)), axis, traversal, /*noFollow*/ false, results);
+ return results;
+ }
+
+ /**
+ * Scan file system entries along a path.
+ * @param path The path at which to start the scan.
+ * @param axis The axis along which to traverse.
+ * @param traversal The traversal scheme to use.
+ */
+ public lscanSync(path: string, axis: Axis, traversal: Traversal) {
+ path = this._resolve(path);
+ const results: string[] = [];
+ this._scan(path, this._stat(this._walk(path, /*noFollow*/ true)), axis, traversal, /*noFollow*/ true, results);
+ return results;
+ }
+
+ private _scan(path: string, stats: Stats, axis: Axis, traversal: Traversal, noFollow: boolean, results: string[]) {
+ if (axis === "ancestors-or-self" || axis === "self" || axis === "descendants-or-self") {
+ if (!traversal.accept || traversal.accept(path, stats)) {
+ results.push(path);
+ }
+ }
+ if (axis === "ancestors-or-self" || axis === "ancestors") {
+ const dirname = vpath.dirname(path);
+ if (dirname !== path) {
+ try {
+ const stats = this._stat(this._walk(dirname, noFollow));
+ if (!traversal.traverse || traversal.traverse(dirname, stats)) {
+ this._scan(dirname, stats, "ancestors-or-self", traversal, noFollow, results);
+ }
+ }
+ catch { /*ignored*/ }
+ }
+ }
+ if (axis === "descendants-or-self" || axis === "descendants") {
+ if (stats.isDirectory() && (!traversal.traverse || traversal.traverse(path, stats))) {
+ for (const file of this.readdirSync(path)) {
+ try {
+ const childpath = vpath.combine(path, file);
+ const stats = this._stat(this._walk(childpath, noFollow));
+ this._scan(childpath, stats, "descendants-or-self", traversal, noFollow, results);
+ }
+ catch { /*ignored*/ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Mounts a physical or virtual file system at a location in this virtual file system.
+ *
+ * @param source The path in the physical (or other virtual) file system.
+ * @param target The path in this virtual file system.
+ * @param resolver An object used to resolve files in `source`.
+ */
+ public mountSync(source: string, target: string, resolver: FileSystemResolver) {
+ if (this.isReadonly) throw createIOError("EROFS");
+
+ source = vpath.validate(source, vpath.ValidationFlags.Absolute);
+
+ const { parent, links, node: existingNode, basename } = this._walk(this._resolve(target), /*noFollow*/ true);
+ if (existingNode) throw createIOError("EEXIST");
+
+ const time = this.time();
+ const node = this._mknod(parent ? parent.dev : ++devCount, S_IFDIR, /*mode*/ 0o777, time);
+ node.source = source;
+ node.resolver = resolver;
+ this._addLink(parent, links, basename, node, time);
+ }
+
+ /**
+ * Recursively remove all files and directories underneath the provided path.
+ */
+ public rimrafSync(path: string) {
+ try {
+ const stats = this.lstatSync(path);
+ if (stats.isFile() || stats.isSymbolicLink()) {
+ this.unlinkSync(path);
+ }
+ else if (stats.isDirectory()) {
+ for (const file of this.readdirSync(path)) {
+ this.rimrafSync(vpath.combine(path, file));
+ }
+ this.rmdirSync(path);
+ }
+ }
+ catch (e) {
+ if (e.code === "ENOENT") return;
+ throw e;
+ }
+ }
+
+ private _depth: string[] = [];
+
+ /**
+ * Make a directory and all of its parent paths (if they don't exist).
+ */
+ public mkdirpSync(path: string) {
+ try {
+ this._depth.push(path);
+ path = this._resolve(path);
+ this.mkdirSync(path);
+ }
+ catch (e) {
+ if (e.code === "ENOENT") {
+ if (this._depth.length > 10) {
+ console.log(`path: ${path}`);
+ console.log(`dirname: ${vpath.dirname(path)}`);
+ console.log(this._depth);
+ throw e;
+ }
+ this.mkdirpSync(vpath.dirname(path));
+ this.mkdirSync(path);
+ }
+ else if (e.code !== "EEXIST") {
+ throw e;
+ }
+ }
+ finally {
+ this._depth.pop();
+ }
+ }
+
+ /**
+ * Print diagnostic information about the structure of the file system to the console.
+ */
+ public debugPrint(): void {
+ let result = "";
+ const printLinks = (dirname: string | undefined, links: collections.SortedMap) => {
+ const iterator = collections.getIterator(links);
+ try {
+ for (let i = collections.nextResult(iterator); i; i = collections.nextResult(iterator)) {
+ const [name, node] = i.value;
+ const path = dirname ? vpath.combine(dirname, name) : name;
+ const marker = vpath.compare(this._cwd, path, this.ignoreCase) === 0 ? "*" : " ";
+ if (result) result += "\n";
+ result += marker;
+ if (isDirectory(node)) {
+ result += vpath.addTrailingSeparator(path);
+ printLinks(path, this._getLinks(node));
+ }
+ else if (isFile(node)) {
+ result += path;
+ }
+ else if (isSymlink(node)) {
+ result += path + " -> " + node.symlink;
+ }
+ }
+ }
+ finally {
+ collections.closeIterator(iterator);
+ }
+ };
+ printLinks(/*dirname*/ undefined, this._getRootLinks());
+ console.log(result);
+ }
+
+ // POSIX API (aligns with NodeJS "fs" module API)
+
+ /**
+ * Get file status. If `path` is a symbolic link, it is dereferenced.
+ *
+ * @link http://pubs.opengroup.org/onlinepubs/9699919799/functions/stat.html
+ *
+ * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module.
+ */
+ public statSync(path: string) {
+ return this._stat(this._walk(this._resolve(path)));
+ }
+
+ /**
+ * Get file status.
+ *
+ * @link http://pubs.opengroup.org/onlinepubs/9699919799/functions/lstat.html
+ *
+ * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module.
+ */
+ public lstatSync(path: string) {
+ return this._stat(this._walk(this._resolve(path), /*noFollow*/ true));
+ }
+
+ private _stat(entry: WalkResult) {
+ const node = entry.node;
+ if (!node) throw createIOError("ENOENT");
+ return new Stats(
+ node.dev,
+ node.ino,
+ node.mode,
+ node.nlink,
+ /*rdev*/ 0,
+ /*size*/ isFile(node) ? this._getSize(node) : isSymlink(node) ? node.symlink.length : 0,
+ /*blksize*/ 4096,
+ /*blocks*/ 0,
+ node.atimeMs,
+ node.mtimeMs,
+ node.ctimeMs,
+ node.birthtimeMs,
+ );
+ }
+
+ /**
+ * Read a directory. If `path` is a symbolic link, it is dereferenced.
+ *
+ * @link http://pubs.opengroup.org/onlinepubs/9699919799/functions/readdir.html
+ *
+ * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module.
+ */
+ public readdirSync(path: string) {
+ const { node } = this._walk(this._resolve(path));
+ if (!node) throw createIOError("ENOENT");
+ if (!isDirectory(node)) throw createIOError("ENOTDIR");
+ return Array.from(this._getLinks(node).keys());
+ }
+
+ /**
+ * Make a directory.
+ *
+ * @link http://pubs.opengroup.org/onlinepubs/9699919799/functions/mkdir.html
+ *
+ * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module.
+ */
+ public mkdirSync(path: string) {
+ if (this.isReadonly) throw createIOError("EROFS");
+
+ const { parent, links, node: existingNode, basename } = this._walk(this._resolve(path), /*noFollow*/ true);
+ if (existingNode) throw createIOError("EEXIST");
+
+ const time = this.time();
+ const node = this._mknod(parent ? parent.dev : ++devCount, S_IFDIR, /*mode*/ 0o777, time);
+ this._addLink(parent, links, basename, node, time);
+ }
+
+ /**
+ * Remove a directory.
+ *
+ * @link http://pubs.opengroup.org/onlinepubs/9699919799/functions/rmdir.html
+ *
+ * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module.
+ */
+ public rmdirSync(path: string) {
+ if (this.isReadonly) throw createIOError("EROFS");
+ path = this._resolve(path);
+
+ const { parent, links, node, basename } = this._walk(path, /*noFollow*/ true);
+ if (!parent) throw createIOError("EPERM");
+ if (!isDirectory(node)) throw createIOError("ENOTDIR");
+ if (this._getLinks(node).size !== 0) throw createIOError("ENOTEMPTY");
+
+ this._removeLink(parent, links, basename, node);
+ }
+
+ /**
+ * Link one file to another file (also known as a "hard link").
+ *
+ * @link http://pubs.opengroup.org/onlinepubs/9699919799/functions/link.html
+ *
+ * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module.
+ */
+ public linkSync(oldpath: string, newpath: string) {
+ if (this.isReadonly) throw createIOError("EROFS");
+
+ const { node } = this._walk(this._resolve(oldpath));
+ if (!node) throw createIOError("ENOENT");
+ if (isDirectory(node)) throw createIOError("EPERM");
+
+ const { parent, links, basename, node: existingNode } = this._walk(this._resolve(newpath), /*noFollow*/ true);
+ if (!parent) throw createIOError("EPERM");
+ if (existingNode) throw createIOError("EEXIST");
+
+ this._addLink(parent, links, basename, node);
+ }
+
+ /**
+ * Remove a directory entry.
+ *
+ * @link http://pubs.opengroup.org/onlinepubs/9699919799/functions/unlink.html
+ *
+ * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module.
+ */
+ public unlinkSync(path: string) {
+ if (this.isReadonly) throw createIOError("EROFS");
+
+ const { parent, links, node, basename } = this._walk(this._resolve(path), /*noFollow*/ true);
+ if (!parent) throw createIOError("EPERM");
+ if (!node) throw createIOError("ENOENT");
+ if (isDirectory(node)) throw createIOError("EISDIR");
+
+ this._removeLink(parent, links, basename, node);
+ }
+
+ /**
+ * Rename a file.
+ *
+ * @link http://pubs.opengroup.org/onlinepubs/9699919799/functions/rename.html
+ *
+ * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module.
+ */
+ public renameSync(oldpath: string, newpath: string) {
+ if (this.isReadonly) throw createIOError("EROFS");
+
+ const { parent: oldParent, links: oldParentLinks, node, basename: oldBasename } = this._walk(this._resolve(oldpath), /*noFollow*/ true);
+ if (!oldParent) throw createIOError("EPERM");
+ if (!node) throw createIOError("ENOENT");
+
+ const { parent: newParent, links: newParentLinks, node: existingNode, basename: newBasename } = this._walk(this._resolve(newpath), /*noFollow*/ true);
+ if (!newParent) throw createIOError("EPERM");
+
+ const time = this.time();
+ if (existingNode) {
+ if (isDirectory(node)) {
+ if (!isDirectory(existingNode)) throw createIOError("ENOTDIR");
+ if (this._getLinks(existingNode).size > 0) throw createIOError("ENOTEMPTY");
+ }
+ else {
+ if (isDirectory(existingNode)) throw createIOError("EISDIR");
+ }
+ this._removeLink(newParent, newParentLinks, newBasename, existingNode, time);
+ }
+
+ this._replaceLink(oldParent, oldParentLinks, oldBasename, newParent, newParentLinks, newBasename, node, time);
+ }
+
+ /**
+ * Make a symbolic link.
+ *
+ * @link http://pubs.opengroup.org/onlinepubs/9699919799/functions/symlink.html
+ *
+ * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module.
+ */
+ public symlinkSync(target: string, linkpath: string) {
+ if (this.isReadonly) throw createIOError("EROFS");
+
+ const { parent, links, node: existingNode, basename } = this._walk(this._resolve(linkpath), /*noFollow*/ true);
+ if (!parent) throw createIOError("EPERM");
+ if (existingNode) throw createIOError("EEXIST");
+
+ const time = this.time();
+ const node = this._mknod(parent.dev, S_IFLNK, /*mode*/ 0o666, time);
+ node.symlink = vpath.validate(target, vpath.ValidationFlags.RelativeOrAbsolute);
+ this._addLink(parent, links, basename, node, time);
+ }
+
+ /**
+ * Resolve a pathname.
+ *
+ * @link http://pubs.opengroup.org/onlinepubs/9699919799/functions/realpath.html
+ *
+ * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module.
+ */
+ public realpathSync(path: string) {
+ const { realpath } = this._walk(this._resolve(path));
+ return realpath;
+ }
+
+ /**
+ * Read from a file.
+ *
+ * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module.
+ */
+ public readFileSync(path: string, encoding?: null): Buffer;
+ /**
+ * Read from a file.
+ *
+ * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module.
+ */
+ public readFileSync(path: string, encoding: string): string;
+ /**
+ * Read from a file.
+ *
+ * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module.
+ */
+ public readFileSync(path: string, encoding?: string | null): string | Buffer;
+ public readFileSync(path: string, encoding: string | null = null) {
+ const { node } = this._walk(this._resolve(path));
+ if (!node) throw createIOError("ENOENT");
+ if (isDirectory(node)) throw createIOError("EISDIR");
+ if (!isFile(node)) throw createIOError("EBADF");
+
+ const buffer = this._getBuffer(node).slice();
+ return encoding ? buffer.toString(encoding) : buffer;
+ }
+
+ /**
+ * Write to a file.
+ *
+ * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module.
+ */
+ public writeFileSync(path: string, data: string | Buffer, encoding: string | null = null) {
+ if (this.isReadonly) throw createIOError("EROFS");
+
+ const { parent, links, node: existingNode, basename } = this._walk(this._resolve(path), /*noFollow*/ false);
+ if (!parent) throw createIOError("EPERM");
+
+ const time = this.time();
+ let node = existingNode;
+ if (!node) {
+ node = this._mknod(parent.dev, S_IFREG, 0o666, time);
+ this._addLink(parent, links, basename, node, time);
+ }
+
+ if (isDirectory(node)) throw createIOError("EISDIR");
+ if (!isFile(node)) throw createIOError("EBADF");
+ node.buffer = Buffer.isBuffer(data) ? data.slice() : Buffer.from("" + data, encoding || "utf8");
+ node.size = node.buffer.byteLength;
+ node.mtimeMs = time;
+ node.ctimeMs = time;
+ }
+
+ private _mknod(dev: number, type: typeof S_IFREG, mode: number, time?: number): FileInode;
+ private _mknod(dev: number, type: typeof S_IFDIR, mode: number, time?: number): DirectoryInode;
+ private _mknod(dev: number, type: typeof S_IFLNK, mode: number, time?: number): SymlinkInode;
+ private _mknod(dev: number, type: number, mode: number, time = this.time()) {
+ return {
+ dev,
+ ino: ++inoCount,
+ mode: (mode & ~S_IFMT & ~0o022 & 0o7777) | (type & S_IFMT),
+ atimeMs: time,
+ mtimeMs: time,
+ ctimeMs: time,
+ birthtimeMs: time,
+ nlink: 0
+ };
+ }
+
+ private _addLink(parent: DirectoryInode | undefined, links: collections.SortedMap, name: string, node: Inode, time = this.time()) {
+ links.set(name, node);
+ node.nlink++;
+ node.ctimeMs = time;
+ if (parent) parent.mtimeMs = time;
+ if (!parent && !this._cwd) this._cwd = name;
+ }
+
+ private _removeLink(parent: DirectoryInode | undefined, links: collections.SortedMap, name: string, node: Inode, time = this.time()) {
+ links.delete(name);
+ node.nlink--;
+ node.ctimeMs = time;
+ if (parent) parent.mtimeMs = time;
+ }
+
+ private _replaceLink(oldParent: DirectoryInode, oldLinks: collections.SortedMap, oldName: string, newParent: DirectoryInode, newLinks: collections.SortedMap, newName: string, node: Inode, time: number) {
+ if (oldParent !== newParent) {
+ this._removeLink(oldParent, oldLinks, oldName, node, time);
+ this._addLink(newParent, newLinks, newName, node, time);
+ }
+ else {
+ oldLinks.delete(oldName);
+ oldLinks.set(newName, node);
+ oldParent.mtimeMs = time;
+ newParent.mtimeMs = time;
+ }
+ }
+
+ private _getRootLinks() {
+ if (!this._lazy.links) {
+ this._lazy.links = new collections.SortedMap(this.stringComparer);
+ if (this._shadowRoot) {
+ this._copyShadowLinks(this._shadowRoot._getRootLinks(), this._lazy.links);
+ }
+ this._lazy.links = this._lazy.links;
+ }
+ return this._lazy.links;
+ }
+
+ private _getLinks(node: DirectoryInode) {
+ if (!node.links) {
+ const links = new collections.SortedMap(this.stringComparer);
+ const { source, resolver } = node;
+ if (source && resolver) {
+ node.source = undefined;
+ node.resolver = undefined;
+ for (const name of resolver.readdirSync(source)) {
+ const path = vpath.combine(source, name);
+ const stats = resolver.statSync(path);
+ switch (stats.mode & S_IFMT) {
+ case S_IFDIR:
+ const dir = this._mknod(node.dev, S_IFDIR, 0o777);
+ dir.source = vpath.combine(source, name);
+ dir.resolver = resolver;
+ this._addLink(node, links, name, dir);
+ break;
+ case S_IFREG:
+ const file = this._mknod(node.dev, S_IFREG, 0o666);
+ file.source = vpath.combine(source, name);
+ file.resolver = resolver;
+ file.size = stats.size;
+ this._addLink(node, links, name, file);
+ break;
+ }
+ }
+ }
+ else if (this._shadowRoot && node.shadowRoot) {
+ this._copyShadowLinks(this._shadowRoot._getLinks(node.shadowRoot), links);
+ }
+ node.links = links;
+ }
+ return node.links;
+ }
+
+ private _getShadow(root: DirectoryInode): DirectoryInode;
+ private _getShadow(root: Inode): Inode;
+ private _getShadow(root: Inode) {
+ const shadows = this._lazy.shadows || (this._lazy.shadows = new Map());
+
+ let shadow = shadows.get(root.ino);
+ if (!shadow) {
+ shadow = {
+ dev: root.dev,
+ ino: root.ino,
+ mode: root.mode,
+ atimeMs: root.atimeMs,
+ mtimeMs: root.mtimeMs,
+ ctimeMs: root.ctimeMs,
+ birthtimeMs: root.birthtimeMs,
+ nlink: root.nlink,
+ shadowRoot: root
+ };
+
+ if (isSymlink(root)) (shadow).symlink = root.symlink;
+ shadows.set(shadow.ino, shadow);
+ }
+
+ return shadow;
+ }
+
+ private _copyShadowLinks(source: ReadonlyMap, target: collections.SortedMap) {
+ const iterator = collections.getIterator(source);
+ try {
+ for (let i = collections.nextResult(iterator); i; i = collections.nextResult(iterator)) {
+ const [name, root] = i.value;
+ target.set(name, this._getShadow(root));
+ }
+ }
+ finally {
+ collections.closeIterator(iterator);
+ }
+ }
+
+ private _getSize(node: FileInode): number {
+ if (node.buffer) return node.buffer.byteLength;
+ if (node.size !== undefined) return node.size;
+ if (node.source && node.resolver) return node.size = node.resolver.statSync(node.source).size;
+ if (this._shadowRoot && node.shadowRoot) return node.size = this._shadowRoot._getSize(node.shadowRoot);
+ return 0;
+ }
+
+ private _getBuffer(node: FileInode): Buffer {
+ if (!node.buffer) {
+ const { source, resolver } = node;
+ if (source && resolver) {
+ node.source = undefined;
+ node.resolver = undefined;
+ node.size = undefined;
+ node.buffer = resolver.readFileSync(source);
+ }
+ else if (this._shadowRoot && node.shadowRoot) {
+ node.buffer = this._shadowRoot._getBuffer(node.shadowRoot);
+ }
+ else {
+ node.buffer = Buffer.allocUnsafe(0);
+ }
+ }
+ return node.buffer;
+ }
+
+ /**
+ * Walk a path to its end.
+ *
+ * @param path The path to follow.
+ * @param noFollow A value indicating whether to *not* dereference a symbolic link at the
+ * end of a path.
+ * @param allowPartial A value indicating whether to return a partial result if the node
+ * at the end of the path cannot be found.
+ *
+ * @link http://man7.org/linux/man-pages/man7/path_resolution.7.html
+ */
+ private _walk(path: string, noFollow?: boolean): WalkResult {
+ let links = this._getRootLinks();
+ let parent: DirectoryInode | undefined;
+ let components = vpath.parse(path);
+ let step = 0;
+ let depth = 0;
+ while (true) {
+ if (depth >= 40) throw createIOError("ELOOP");
+ const lastStep = step === components.length - 1;
+ const basename = components[step];
+ const node = links.get(basename);
+ if (lastStep && (noFollow || !isSymlink(node))) {
+ return { realpath: vpath.format(components), basename, parent, links, node };
+ }
+ if (node === undefined) {
+ throw createIOError("ENOENT");
+ }
+ if (isSymlink(node)) {
+ const dirname = vpath.format(components.slice(0, step));
+ const symlink = vpath.resolve(dirname, node.symlink);
+ links = this._getRootLinks();
+ parent = undefined;
+ components = vpath.parse(symlink).concat(components.slice(step + 1));
+ step = 0;
+ depth++;
+ continue;
+ }
+ if (isDirectory(node)) {
+ links = this._getLinks(node);
+ parent = node;
+ step++;
+ continue;
+ }
+ throw createIOError("ENOTDIR");
+ }
+ }
+
+ /**
+ * Resolve a path relative to the current working directory.
+ */
+ private _resolve(path: string) {
+ return this._cwd
+ ? vpath.resolve(this._cwd, vpath.validate(path, vpath.ValidationFlags.RelativeOrAbsolute))
+ : vpath.validate(path, vpath.ValidationFlags.Absolute);
+ }
+
+ private _applyFiles(files: FileSet, dirname: string) {
+ const deferred: [Symlink | Link | Mount, string][] = [];
+ this._applyFilesWorker(files, dirname, deferred);
+ for (const [entry, path] of deferred) {
+ this.mkdirpSync(vpath.dirname(path));
+ this.pushd(vpath.dirname(path));
+ if (entry instanceof Symlink) {
+ if (this.stringComparer(vpath.dirname(path), path) === 0) {
+ throw new TypeError("Roots cannot be symbolic links.");
+ }
+ this.symlinkSync(entry.symlink, path);
+ this._applyFileExtendedOptions(path, entry);
+ }
+ else if (entry instanceof Link) {
+ if (this.stringComparer(vpath.dirname(path), path) === 0) {
+ throw new TypeError("Roots cannot be hard links.");
+ }
+ this.linkSync(entry.path, path);
+ }
+ else {
+ this.mountSync(entry.source, path, entry.resolver);
+ this._applyFileExtendedOptions(path, entry);
+ }
+ this.popd();
+ }
+ }
+
+ private _applyFileExtendedOptions(path: string, entry: Directory | File | Symlink | Mount) {
+ const { meta } = entry;
+ if (meta !== undefined) {
+ const filemeta = this.filemeta(path);
+ for (const key of Object.keys(meta)) {
+ filemeta.set(key, meta[key]);
+ }
+ }
+ }
+
+ private _applyFilesWorker(files: FileSet, dirname: string, deferred: [Symlink | Link | Mount, string][]) {
+ for (const key of Object.keys(files)) {
+ const value = this._normalizeFileSetEntry(files[key]);
+ const path = dirname ? vpath.resolve(dirname, key) : key;
+ vpath.validate(path, vpath.ValidationFlags.Absolute);
+ if (value === null || value === undefined) {
+ if (this.stringComparer(vpath.dirname(path), path) === 0) {
+ throw new TypeError("Roots cannot be deleted.");
+ }
+ this.rimrafSync(path);
+ }
+ else if (value instanceof File) {
+ if (this.stringComparer(vpath.dirname(path), path) === 0) {
+ throw new TypeError("Roots cannot be files.");
+ }
+ this.mkdirpSync(vpath.dirname(path));
+ this.writeFileSync(path, value.data, value.encoding);
+ this._applyFileExtendedOptions(path, value);
+ }
+ else if (value instanceof Directory) {
+ this.mkdirpSync(path);
+ this._applyFileExtendedOptions(path, value);
+ this._applyFilesWorker(value.files, path, deferred);
+ }
+ else {
+ deferred.push([value as Symlink | Link | Mount, path]);
+ }
+ }
+ }
+
+ private _normalizeFileSetEntry(value: FileSet[string]) {
+ if (value === undefined ||
+ value === null ||
+ value instanceof Directory ||
+ value instanceof File ||
+ value instanceof Link ||
+ value instanceof Symlink ||
+ value instanceof Mount) {
+ return value;
+ }
+ return typeof value === "string" || Buffer.isBuffer(value) ? new File(value) : new Directory(value);
+ }
+ }
+
+ export interface FileSystemOptions {
+ // Sets the initial timestamp for new files and directories, or the function used
+ // to calculate timestamps.
+ time?: number | Date | (() => number | Date);
+
+ // A set of file system entries to initially add to the file system.
+ files?: FileSet;
+
+ // Sets the initial working directory for the file system.
+ cwd?: string;
+
+ // Sets initial metadata attached to the file system.
+ meta?: Record;
+ }
+
+ export interface FileSystemCreateOptions {
+ // Sets the documents to add to the file system.
+ documents?: ReadonlyArray;
+
+ // Sets the initial working directory for the file system.
+ cwd?: string;
+ }
+
+ export type Axis = "ancestors" | "ancestors-or-self" | "self" | "descendants-or-self" | "descendants";
+
+ export interface Traversal {
+ /** A function called to choose whether to continue to traverse to either ancestors or descendants. */
+ traverse?(path: string, stats: Stats): boolean;
+ /** A function called to choose whether to accept a path as part of the result. */
+ accept?(path: string, stats: Stats): boolean;
+ }
+
+ export interface FileSystemResolver {
+ statSync(path: string): { mode: number; size: number; };
+ readdirSync(path: string): string[];
+ readFileSync(path: string): Buffer;
+ }
+
+ export interface FileSystemResolverHost {
+ useCaseSensitiveFileNames(): boolean;
+ getAccessibleFileSystemEntries(path: string): ts.FileSystemEntries;
+ directoryExists(path: string): boolean;
+ fileExists(path: string): boolean;
+ getFileSize(path: string): number;
+ readFile(path: string): string;
+ getWorkspaceRoot(): string;
+ }
+
+ export function createResolver(host: FileSystemResolverHost): FileSystemResolver {
+ return {
+ readdirSync(path: string): string[] {
+ const { files, directories } = host.getAccessibleFileSystemEntries(path);
+ return directories.concat(files);
+ },
+ statSync(path: string): { mode: number; size: number; } {
+ if (host.directoryExists(path)) {
+ return { mode: S_IFDIR | 0o777, size: 0 };
+ }
+ else if (host.fileExists(path)) {
+ return { mode: S_IFREG | 0o666, size: host.getFileSize(path) };
+ }
+ else {
+ throw new Error("ENOENT: path does not exist");
+ }
+ },
+ readFileSync(path: string): Buffer {
+ return Buffer.from(host.readFile(path), "utf8");
+ }
+ };
+ }
+
+ /**
+ * Create a virtual file system from a physical file system using the following path mappings:
+ *
+ * - `/.ts` is a directory mapped to `${workspaceRoot}/built/local`
+ * - `/.lib` is a directory mapped to `${workspaceRoot}/tests/lib`
+ * - `/.src` is a virtual directory to be used for tests.
+ *
+ * Unless overridden, `/.src` will be the current working directory for the virtual file system.
+ */
+ export function createFromFileSystem(host: FileSystemResolverHost, ignoreCase: boolean, { documents, cwd }: FileSystemCreateOptions = {}) {
+ const fs = getBuiltLocal(host, ignoreCase).shadow();
+ if (cwd) {
+ fs.mkdirpSync(cwd);
+ fs.chdir(cwd);
+ }
+ if (documents) {
+ for (const document of documents) {
+ fs.mkdirpSync(vpath.dirname(document.file));
+ fs.writeFileSync(document.file, document.text, "utf8");
+ fs.filemeta(document.file).set("document", document);
+ // Add symlinks
+ const symlink = document.meta.get("symlink");
+ if (symlink) {
+ for (const link of symlink.split(",").map(link => link.trim())) {
+ fs.mkdirpSync(vpath.dirname(link));
+ fs.symlinkSync(document.file, link);
+ fs.filemeta(link).set("document", document);
+ }
+ }
+ }
+ }
+ return fs;
+ }
+
+ export class Stats {
+ public dev: number;
+ public ino: number;
+ public mode: number;
+ public nlink: number;
+ public uid: number;
+ public gid: number;
+ public rdev: number;
+ public size: number;
+ public blksize: number;
+ public blocks: number;
+ public atimeMs: number;
+ public mtimeMs: number;
+ public ctimeMs: number;
+ public birthtimeMs: number;
+ public atime: Date;
+ public mtime: Date;
+ public ctime: Date;
+ public birthtime: Date;
+
+ constructor();
+ constructor(dev: number, ino: number, mode: number, nlink: number, rdev: number, size: number, blksize: number, blocks: number, atimeMs: number, mtimeMs: number, ctimeMs: number, birthtimeMs: number);
+ constructor(dev = 0, ino = 0, mode = 0, nlink = 0, rdev = 0, size = 0, blksize = 0, blocks = 0, atimeMs = 0, mtimeMs = 0, ctimeMs = 0, birthtimeMs = 0) {
+ this.dev = dev;
+ this.ino = ino;
+ this.mode = mode;
+ this.nlink = nlink;
+ this.uid = 0;
+ this.gid = 0;
+ this.rdev = rdev;
+ this.size = size;
+ this.blksize = blksize;
+ this.blocks = blocks;
+ this.atimeMs = atimeMs;
+ this.mtimeMs = mtimeMs;
+ this.ctimeMs = ctimeMs;
+ this.birthtimeMs = birthtimeMs;
+ this.atime = new Date(this.atimeMs);
+ this.mtime = new Date(this.mtimeMs);
+ this.ctime = new Date(this.ctimeMs);
+ this.birthtime = new Date(this.birthtimeMs);
+ }
+
+ public isFile() { return (this.mode & S_IFMT) === S_IFREG; }
+ public isDirectory() { return (this.mode & S_IFMT) === S_IFDIR; }
+ public isSymbolicLink() { return (this.mode & S_IFMT) === S_IFLNK; }
+ public isBlockDevice() { return (this.mode & S_IFMT) === S_IFBLK; }
+ public isCharacterDevice() { return (this.mode & S_IFMT) === S_IFCHR; }
+ public isFIFO() { return (this.mode & S_IFMT) === S_IFIFO; }
+ public isSocket() { return (this.mode & S_IFMT) === S_IFSOCK; }
+ }
+
+ // tslint:disable-next-line:variable-name
+ export const IOErrorMessages = Object.freeze({
+ EACCES: "access denied",
+ EIO: "an I/O error occurred",
+ ENOENT: "no such file or directory",
+ EEXIST: "file already exists",
+ ELOOP: "too many symbolic links encountered",
+ ENOTDIR: "no such directory",
+ EISDIR: "path is a directory",
+ EBADF: "invalid file descriptor",
+ EINVAL: "invalid value",
+ ENOTEMPTY: "directory not empty",
+ EPERM: "operation not permitted",
+ EROFS: "file system is read-only"
+ });
+
+ export function createIOError(code: keyof typeof IOErrorMessages) {
+ const err: NodeJS.ErrnoException = new Error(`${code}: ${IOErrorMessages[code]}`);
+ err.code = code;
+ return err;
+ }
+
+ /**
+ * A template used to populate files, directories, links, etc. in a virtual file system.
+ */
+ export interface FileSet {
+ [name: string]: DirectoryLike | FileLike | Link | Symlink | Mount | null | undefined;
+ }
+
+ export type DirectoryLike = FileSet | Directory;
+ export type FileLike = File | Buffer | string;
+
+ /** Extended options for a directory in a `FileSet` */
+ export class Directory {
+ public readonly files: FileSet;
+ public readonly meta: Record | undefined;
+ constructor(files: FileSet, { meta }: { meta?: Record } = {}) {
+ this.files = files;
+ this.meta = meta;
+ }
+ }
+
+ /** Extended options for a file in a `FileSet` */
+ export class File {
+ public readonly data: Buffer | string;
+ public readonly encoding: string | undefined;
+ public readonly meta: Record | undefined;
+ constructor(data: Buffer | string, { meta, encoding }: { encoding?: string, meta?: Record } = {}) {
+ this.data = data;
+ this.encoding = encoding;
+ this.meta = meta;
+ }
+ }
+
+ /** Extended options for a hard link in a `FileSet` */
+ export class Link {
+ public readonly path: string;
+ constructor(path: string) {
+ this.path = path;
+ }
+ }
+
+ /** Extended options for a symbolic link in a `FileSet` */
+ export class Symlink {
+ public readonly symlink: string;
+ public readonly meta: Record | undefined;
+ constructor(symlink: string, { meta }: { meta?: Record } = {}) {
+ this.symlink = symlink;
+ this.meta = meta;
+ }
+ }
+
+ /** Extended options for mounting a virtual copy of an external file system via a `FileSet` */
+ export class Mount {
+ public readonly source: string;
+ public readonly resolver: FileSystemResolver;
+ public readonly meta: Record | undefined;
+ constructor(source: string, resolver: FileSystemResolver, { meta }: { meta?: Record } = {}) {
+ this.source = source;
+ this.resolver = resolver;
+ this.meta = meta;
+ }
+ }
+
+ // a generic POSIX inode
+ type Inode = FileInode | DirectoryInode | SymlinkInode;
+
+ interface FileInode {
+ dev: number; // device id
+ ino: number; // inode id
+ mode: number; // file mode
+ atimeMs: number; // access time
+ mtimeMs: number; // modified time
+ ctimeMs: number; // status change time
+ birthtimeMs: number; // creation time
+ nlink: number; // number of hard links
+ size?: number;
+ buffer?: Buffer;
+ source?: string;
+ resolver?: FileSystemResolver;
+ shadowRoot?: FileInode;
+ meta?: collections.Metadata;
+ }
+
+ interface DirectoryInode {
+ dev: number; // device id
+ ino: number; // inode id
+ mode: number; // file mode
+ atimeMs: number; // access time
+ mtimeMs: number; // modified time
+ ctimeMs: number; // status change time
+ birthtimeMs: number; // creation time
+ nlink: number; // number of hard links
+ links?: collections.SortedMap;
+ source?: string;
+ resolver?: FileSystemResolver;
+ shadowRoot?: DirectoryInode;
+ meta?: collections.Metadata;
+ }
+
+ interface SymlinkInode {
+ dev: number; // device id
+ ino: number; // inode id
+ mode: number; // file mode
+ atimeMs: number; // access time
+ mtimeMs: number; // modified time
+ ctimeMs: number; // status change time
+ birthtimeMs: number; // creation time
+ nlink: number; // number of hard links
+ symlink?: string;
+ shadowRoot?: SymlinkInode;
+ meta?: collections.Metadata;
+ }
+
+ function isFile(node: Inode | undefined): node is FileInode {
+ return node !== undefined && (node.mode & S_IFMT) === S_IFREG;
+ }
+
+ function isDirectory(node: Inode | undefined): node is DirectoryInode {
+ return node !== undefined && (node.mode & S_IFMT) === S_IFDIR;
+ }
+
+ function isSymlink(node: Inode | undefined): node is SymlinkInode {
+ return node !== undefined && (node.mode & S_IFMT) === S_IFLNK;
+ }
+
+ interface WalkResult {
+ realpath: string;
+ basename: string;
+ parent: DirectoryInode | undefined;
+ links: collections.SortedMap | undefined;
+ node: Inode | undefined;
+ }
+
+ // TODO(rbuckton): This patches the baseline to replace lib.d.ts with lib.es5.d.ts.
+ // This is only to make the PR for this change easier to read. A follow-up PR will
+ // revert this change and accept the new baselines.
+ // See https://github.com/Microsoft/TypeScript/pull/20763#issuecomment-352553264
+ function patchResolver(host: FileSystemResolverHost, resolver: FileSystemResolver): FileSystemResolver {
+ const libFile = vpath.combine(host.getWorkspaceRoot(), "built/local/lib.d.ts");
+ const es5File = vpath.combine(host.getWorkspaceRoot(), "built/local/lib.es5.d.ts");
+ const stringComparer = host.useCaseSensitiveFileNames() ? vpath.compareCaseSensitive : vpath.compareCaseInsensitive;
+ return {
+ readdirSync: path => resolver.readdirSync(path),
+ statSync: path => resolver.statSync(fixPath(path)),
+ readFileSync: (path) => resolver.readFileSync(fixPath(path))
+ };
+
+ function fixPath(path: string) {
+ return stringComparer(path, libFile) === 0 ? es5File : path;
+ }
+ }
+
+ let builtLocalHost: FileSystemResolverHost | undefined;
+ let builtLocalCI: FileSystem | undefined;
+ let builtLocalCS: FileSystem | undefined;
+
+ function getBuiltLocal(host: FileSystemResolverHost, ignoreCase: boolean): FileSystem {
+ if (builtLocalHost !== host) {
+ builtLocalCI = undefined;
+ builtLocalCS = undefined;
+ builtLocalHost = host;
+ }
+ if (!builtLocalCI) {
+ const resolver = createResolver(host);
+ builtLocalCI = new FileSystem(/*ignoreCase*/ true, {
+ files: {
+ [builtFolder]: new Mount(vpath.resolve(host.getWorkspaceRoot(), "built/local"), patchResolver(host, resolver)),
+ [testLibFolder]: new Mount(vpath.resolve(host.getWorkspaceRoot(), "tests/lib"), resolver),
+ [srcFolder]: {}
+ },
+ cwd: srcFolder,
+ meta: { defaultLibLocation: builtFolder }
+ });
+ builtLocalCI.makeReadonly();
+ }
+ if (ignoreCase) return builtLocalCI;
+ if (!builtLocalCS) {
+ builtLocalCS = builtLocalCI.shadow(/*ignoreCase*/ false);
+ builtLocalCS.makeReadonly();
+ }
+ return builtLocalCS;
+ }
+}
+// tslint:enable:no-null-keyword
\ No newline at end of file
diff --git a/src/harness/virtualFileSystem.ts b/src/harness/virtualFileSystem.ts
deleted file mode 100644
index 698f99616ca37..0000000000000
--- a/src/harness/virtualFileSystem.ts
+++ /dev/null
@@ -1,222 +0,0 @@
-///
-///
-namespace Utils {
- export class VirtualFileSystemEntry {
- fileSystem: VirtualFileSystem;
- name: string;
-
- constructor(fileSystem: VirtualFileSystem, name: string) {
- this.fileSystem = fileSystem;
- this.name = name;
- }
-
- isDirectory(): this is VirtualDirectory { return false; }
- isFile(): this is VirtualFile { return false; }
- isFileSystem(): this is VirtualFileSystem { return false; }
- }
-
- export class VirtualFile extends VirtualFileSystemEntry {
- content?: Harness.LanguageService.ScriptInfo;
- isFile() { return true; }
- }
-
- export abstract class VirtualFileSystemContainer extends VirtualFileSystemEntry {
- abstract getFileSystemEntries(): VirtualFileSystemEntry[];
-
- getFileSystemEntry(name: string): VirtualFileSystemEntry {
- for (const entry of this.getFileSystemEntries()) {
- if (this.fileSystem.sameName(entry.name, name)) {
- return entry;
- }
- }
- return undefined;
- }
-
- getDirectories(): VirtualDirectory[] {
- return ts.filter(this.getFileSystemEntries(), entry => entry.isDirectory());
- }
-
- getFiles(): VirtualFile[] {
- return ts.filter(this.getFileSystemEntries(), entry => entry.isFile());
- }
-
- getDirectory(name: string): VirtualDirectory {
- const entry = this.getFileSystemEntry(name);
- return entry.isDirectory() ? entry : undefined;
- }
-
- getFile(name: string): VirtualFile {
- const entry = this.getFileSystemEntry(name);
- return entry.isFile() ? entry : undefined;
- }
- }
-
- export class VirtualDirectory extends VirtualFileSystemContainer {
- private entries: VirtualFileSystemEntry[] = [];
-
- isDirectory() { return true; }
-
- getFileSystemEntries() { return this.entries.slice(); }
-
- addDirectory(name: string): VirtualDirectory {
- const entry = this.getFileSystemEntry(name);
- if (entry === undefined) {
- const directory = new VirtualDirectory(this.fileSystem, name);
- this.entries.push(directory);
- return directory;
- }
- else if (entry.isDirectory()) {
- return entry;
- }
- else {
- return undefined;
- }
- }
-
- addFile(name: string, content?: Harness.LanguageService.ScriptInfo): VirtualFile {
- const entry = this.getFileSystemEntry(name);
- if (entry === undefined) {
- const file = new VirtualFile(this.fileSystem, name);
- file.content = content;
- this.entries.push(file);
- return file;
- }
- else if (entry.isFile()) {
- entry.content = content;
- return entry;
- }
- else {
- return undefined;
- }
- }
- }
-
- export class VirtualFileSystem extends VirtualFileSystemContainer {
- private root: VirtualDirectory;
-
- currentDirectory: string;
- useCaseSensitiveFileNames: boolean;
-
- constructor(currentDirectory: string, useCaseSensitiveFileNames: boolean) {
- super(/*fileSystem*/ undefined, "");
- this.fileSystem = this;
- this.root = new VirtualDirectory(this, "");
- this.currentDirectory = currentDirectory;
- this.useCaseSensitiveFileNames = useCaseSensitiveFileNames;
- }
-
- isFileSystem() { return true; }
-
- getFileSystemEntries() { return this.root.getFileSystemEntries(); }
-
- addDirectory(path: string) {
- path = ts.normalizePath(path);
- const components = ts.getNormalizedPathComponents(path, this.currentDirectory);
- let directory: VirtualDirectory = this.root;
- for (const component of components) {
- directory = directory.addDirectory(component);
- if (directory === undefined) {
- break;
- }
- }
-
- return directory;
- }
-
- addFile(path: string, content?: Harness.LanguageService.ScriptInfo) {
- const absolutePath = ts.normalizePath(ts.getNormalizedAbsolutePath(path, this.currentDirectory));
- const fileName = ts.getBaseFileName(absolutePath);
- const directoryPath = ts.getDirectoryPath(absolutePath);
- const directory = this.addDirectory(directoryPath);
- return directory ? directory.addFile(fileName, content) : undefined;
- }
-
- fileExists(path: string) {
- const entry = this.traversePath(path);
- return entry !== undefined && entry.isFile();
- }
-
- sameName(a: string, b: string) {
- return this.useCaseSensitiveFileNames ? a === b : a.toLowerCase() === b.toLowerCase();
- }
-
- traversePath(path: string) {
- path = ts.normalizePath(path);
- let directory: VirtualDirectory = this.root;
- for (const component of ts.getNormalizedPathComponents(path, this.currentDirectory)) {
- const entry = directory.getFileSystemEntry(component);
- if (entry === undefined) {
- return undefined;
- }
- else if (entry.isDirectory()) {
- directory = entry;
- }
- else {
- return entry;
- }
- }
-
- return directory;
- }
-
- /**
- * Reads the directory at the given path and retrieves a list of file names and a list
- * of directory names within it. Suitable for use with ts.matchFiles()
- * @param path The path to the directory to be read
- */
- getAccessibleFileSystemEntries(path: string) {
- const entry = this.traversePath(path);
- if (entry && entry.isDirectory()) {
- return {
- files: ts.map(entry.getFiles(), f => f.name),
- directories: ts.map(entry.getDirectories(), d => d.name)
- };
- }
- return { files: [], directories: [] };
- }
-
- getAllFileEntries() {
- const fileEntries: VirtualFile[] = [];
- getFilesRecursive(this.root, fileEntries);
- return fileEntries;
-
- function getFilesRecursive(dir: VirtualDirectory, result: VirtualFile[]) {
- const files = dir.getFiles();
- const dirs = dir.getDirectories();
- for (const file of files) {
- result.push(file);
- }
- for (const subDir of dirs) {
- getFilesRecursive(subDir, result);
- }
- }
- }
- }
-
- export class MockParseConfigHost extends VirtualFileSystem implements ts.ParseConfigHost {
- constructor(currentDirectory: string, ignoreCase: boolean, files: ts.Map | string[]) {
- super(currentDirectory, ignoreCase);
- if (files instanceof Array) {
- for (const file of files) {
- this.addFile(file, new Harness.LanguageService.ScriptInfo(file, undefined, /*isRootFile*/false));
- }
- }
- else {
- files.forEach((fileContent, fileName) => {
- this.addFile(fileName, new Harness.LanguageService.ScriptInfo(fileName, fileContent, /*isRootFile*/false));
- });
- }
- }
-
- readFile(path: string): string | undefined {
- const value = this.traversePath(path);
- if (value && value.isFile()) {
- return value.content.content;
- }
- }
-
- readDirectory(path: string, extensions: ReadonlyArray, excludes: ReadonlyArray, includes: ReadonlyArray, depth: number) {
- return ts.matchFiles(path, extensions, excludes, includes, this.useCaseSensitiveFileNames, this.currentDirectory, depth, (path: string) => this.getAccessibleFileSystemEntries(path));
- }
- }
-}
\ No newline at end of file
diff --git a/src/harness/vpath.ts b/src/harness/vpath.ts
new file mode 100644
index 0000000000000..9b42ba2a28db8
--- /dev/null
+++ b/src/harness/vpath.ts
@@ -0,0 +1,127 @@
+namespace vpath {
+ export import sep = ts.directorySeparator;
+ export import normalizeSeparators = ts.normalizeSlashes;
+ export import isAbsolute = ts.isRootedDiskPath;
+ export import isRoot = ts.isDiskPathRoot;
+ export import hasTrailingSeparator = ts.hasTrailingDirectorySeparator;
+ export import addTrailingSeparator = ts.ensureTrailingDirectorySeparator;
+ export import removeTrailingSeparator = ts.removeTrailingDirectorySeparator;
+ export import normalize = ts.normalizePath;
+ export import combine = ts.combinePaths;
+ export import parse = ts.getPathComponents;
+ export import reduce = ts.reducePathComponents;
+ export import format = ts.getPathFromPathComponents;
+ export import resolve = ts.resolvePath;
+ export import compare = ts.comparePaths;
+ export import compareCaseSensitive = ts.comparePathsCaseSensitive;
+ export import compareCaseInsensitive = ts.comparePathsCaseInsensitive;
+ export import dirname = ts.getDirectoryPath;
+ export import basename = ts.getBaseFileName;
+ export import extname = ts.getAnyExtensionFromPath;
+ export import relative = ts.getRelativePath;
+ export import beneath = ts.containsPath;
+ export import changeExtension = ts.changeAnyExtension;
+ export import isTypeScript = ts.hasTypeScriptFileExtension;
+ export import isJavaScript = ts.hasJavaScriptFileExtension;
+
+ const invalidRootComponentRegExp = /^(?!(\/|\/\/\w+\/|[a-zA-Z]:\/?|)$)/;
+ const invalidNavigableComponentRegExp = /[:*?"<>|]/;
+ const invalidNonNavigableComponentRegExp = /^\.{1,2}$|[:*?"<>|]/;
+ const extRegExp = /\.\w+$/;
+
+ export const enum ValidationFlags {
+ None = 0,
+
+ RequireRoot = 1 << 0,
+ RequireDirname = 1 << 1,
+ RequireBasename = 1 << 2,
+ RequireExtname = 1 << 3,
+ RequireTrailingSeparator = 1 << 4,
+
+ AllowRoot = 1 << 5,
+ AllowDirname = 1 << 6,
+ AllowBasename = 1 << 7,
+ AllowExtname = 1 << 8,
+ AllowTrailingSeparator = 1 << 9,
+ AllowNavigation = 1 << 10,
+
+ /** Path must be a valid directory root */
+ Root = RequireRoot | AllowRoot | AllowTrailingSeparator,
+
+ /** Path must be a absolute */
+ Absolute = RequireRoot | AllowRoot | AllowDirname | AllowBasename | AllowExtname | AllowTrailingSeparator | AllowNavigation,
+
+ /** Path may be relative or absolute */
+ RelativeOrAbsolute = AllowRoot | AllowDirname | AllowBasename | AllowExtname | AllowTrailingSeparator | AllowNavigation,
+
+ /** Path may only be a filename */
+ Basename = RequireBasename | AllowExtname,
+ }
+
+ function validateComponents(components: string[], flags: ValidationFlags, hasTrailingSeparator: boolean) {
+ const hasRoot = !!components[0];
+ const hasDirname = components.length > 2;
+ const hasBasename = components.length > 1;
+ const hasExtname = hasBasename && extRegExp.test(components[components.length - 1]);
+ const invalidComponentRegExp = flags & ValidationFlags.AllowNavigation ? invalidNavigableComponentRegExp : invalidNonNavigableComponentRegExp;
+
+ // Validate required components
+ if (flags & ValidationFlags.RequireRoot && !hasRoot) return false;
+ if (flags & ValidationFlags.RequireDirname && !hasDirname) return false;
+ if (flags & ValidationFlags.RequireBasename && !hasBasename) return false;
+ if (flags & ValidationFlags.RequireExtname && !hasExtname) return false;
+ if (flags & ValidationFlags.RequireTrailingSeparator && !hasTrailingSeparator) return false;
+
+ // Required components indicate allowed components
+ if (flags & ValidationFlags.RequireRoot) flags |= ValidationFlags.AllowRoot;
+ if (flags & ValidationFlags.RequireDirname) flags |= ValidationFlags.AllowDirname;
+ if (flags & ValidationFlags.RequireBasename) flags |= ValidationFlags.AllowBasename;
+ if (flags & ValidationFlags.RequireExtname) flags |= ValidationFlags.AllowExtname;
+ if (flags & ValidationFlags.RequireTrailingSeparator) flags |= ValidationFlags.AllowTrailingSeparator;
+
+ // Validate disallowed components
+ if (~flags & ValidationFlags.AllowRoot && hasRoot) return false;
+ if (~flags & ValidationFlags.AllowDirname && hasDirname) return false;
+ if (~flags & ValidationFlags.AllowBasename && hasBasename) return false;
+ if (~flags & ValidationFlags.AllowExtname && hasExtname) return false;
+ if (~flags & ValidationFlags.AllowTrailingSeparator && hasTrailingSeparator) return false;
+
+ // Validate component strings
+ if (invalidRootComponentRegExp.test(components[0])) return false;
+ for (let i = 1; i < components.length; i++) {
+ if (invalidComponentRegExp.test(components[i])) return false;
+ }
+
+ return true;
+ }
+
+ export function validate(path: string, flags: ValidationFlags = ValidationFlags.RelativeOrAbsolute) {
+ const components = parse(path);
+ const trailing = hasTrailingSeparator(path);
+ if (!validateComponents(components, flags, trailing)) throw vfs.createIOError("ENOENT");
+ return components.length > 1 && trailing ? format(reduce(components)) + sep : format(reduce(components));
+ }
+
+ export function isDeclaration(path: string) {
+ return extname(path, ".d.ts", /*ignoreCase*/ false).length > 0;
+ }
+
+ export function isSourceMap(path: string) {
+ return extname(path, ".map", /*ignoreCase*/ false).length > 0;
+ }
+
+ const javaScriptSourceMapExtensions: ReadonlyArray = [".js.map", ".jsx.map"];
+
+ export function isJavaScriptSourceMap(path: string) {
+ return extname(path, javaScriptSourceMapExtensions, /*ignoreCase*/ false).length > 0;
+ }
+
+ export function isJson(path: string) {
+ return extname(path, ".json", /*ignoreCase*/ false).length > 0;
+ }
+
+ export function isDefaultLibrary(path: string) {
+ return isDeclaration(path)
+ && basename(path).startsWith("lib.");
+ }
+}
\ No newline at end of file
diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts
index d66fe48682e63..2f53faae2cff9 100644
--- a/src/server/editorServices.ts
+++ b/src/server/editorServices.ts
@@ -521,7 +521,11 @@ namespace ts.server {
}
}
- updateTypingsForProject(response: SetTypings | InvalidateCachedTypings | PackageInstalledResponse): void {
+ updateTypingsForProject(response: SetTypings | InvalidateCachedTypings | PackageInstalledResponse): void;
+ /** @internal */
+ // tslint:disable-next-line:unified-signatures
+ updateTypingsForProject(response: SetTypings | InvalidateCachedTypings | PackageInstalledResponse | BeginInstallTypes | EndInstallTypes): void;
+ updateTypingsForProject(response: SetTypings | InvalidateCachedTypings | PackageInstalledResponse | BeginInstallTypes | EndInstallTypes): void {
const project = this.findProject(response.projectName);
if (!project) {
return;
diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts
index d863021ff9222..68b6e9a856f62 100644
--- a/src/services/codefixes/importFixes.ts
+++ b/src/services/codefixes/importFixes.ts
@@ -266,7 +266,7 @@ namespace ts.codefix {
return [global];
}
- const relativePath = removeExtensionAndIndexPostFix(getRelativePath(moduleFileName, sourceDirectory, getCanonicalFileName), moduleResolutionKind, addJsExtension);
+ const relativePath = removeExtensionAndIndexPostFix(ensurePathIsNonModuleName(getRelativePath(sourceDirectory, moduleFileName, getCanonicalFileName)), moduleResolutionKind, addJsExtension);
if (!baseUrl || preferences.importModuleSpecifierPreference === "relative") {
return [relativePath];
}
@@ -321,7 +321,7 @@ namespace ts.codefix {
1 < 2 = true
In this case we should prefer using the relative path "../a" instead of the baseUrl path "foo/a".
*/
- const pathFromSourceToBaseUrl = getRelativePath(baseUrl, sourceDirectory, getCanonicalFileName);
+ const pathFromSourceToBaseUrl = ensurePathIsNonModuleName(getRelativePath(sourceDirectory, baseUrl, getCanonicalFileName));
const relativeFirst = getRelativePathNParents(relativePath) < getRelativePathNParents(pathFromSourceToBaseUrl);
return relativeFirst ? [relativePath, importRelativeToBaseUrl] : [importRelativeToBaseUrl, relativePath];
});
@@ -343,11 +343,12 @@ namespace ts.codefix {
}
function getRelativePathNParents(relativePath: string): number {
- let count = 0;
- for (let i = 0; i + 3 <= relativePath.length && relativePath.slice(i, i + 3) === "../"; i += 3) {
- count++;
+ const components = getPathComponents(relativePath);
+ if (components[0] || components.length === 1) return 0;
+ for (let i = 1; i < components.length; i++) {
+ if (components[i] !== "..") return i - 1;
}
- return count;
+ return components.length - 1;
}
function tryGetModuleNameFromAmbientModule(moduleSymbol: Symbol): string | undefined {
@@ -389,7 +390,7 @@ namespace ts.codefix {
}
const normalizedSourcePath = getPathRelativeToRootDirs(sourceDirectory, rootDirs, getCanonicalFileName);
- const relativePath = normalizedSourcePath !== undefined ? getRelativePath(normalizedTargetPath, normalizedSourcePath, getCanonicalFileName) : normalizedTargetPath;
+ const relativePath = normalizedSourcePath !== undefined ? ensurePathIsNonModuleName(getRelativePath(normalizedSourcePath, normalizedTargetPath, getCanonicalFileName)) : normalizedTargetPath;
return removeFileExtension(relativePath);
}
@@ -472,7 +473,7 @@ namespace ts.codefix {
return path.substring(parts.topLevelPackageNameIndex + 1);
}
else {
- return getRelativePath(path, sourceDirectory, getCanonicalFileName);
+ return ensurePathIsNonModuleName(getRelativePath(sourceDirectory, path, getCanonicalFileName));
}
}
}
diff --git a/src/services/getEditsForFileRename.ts b/src/services/getEditsForFileRename.ts
index 09bbdc11e7890..741ba977184cf 100644
--- a/src/services/getEditsForFileRename.ts
+++ b/src/services/getEditsForFileRename.ts
@@ -63,10 +63,10 @@ namespace ts {
function getPathUpdater(oldFilePath: string, newFilePath: string, host: LanguageServiceHost): (oldPath: string) => string | undefined {
// Get the relative path from old to new location, and append it on to the end of imports and normalize.
- const rel = getRelativePath(newFilePath, getDirectoryPath(oldFilePath), createGetCanonicalFileName(hostUsesCaseSensitiveFileNames(host)));
+ const rel = ensurePathIsNonModuleName(getRelativePath(getDirectoryPath(oldFilePath), newFilePath, createGetCanonicalFileName(hostUsesCaseSensitiveFileNames(host))));
return oldPath => {
if (!pathIsRelative(oldPath)) return;
- return ensurePathIsRelative(normalizePath(combinePaths(getDirectoryPath(oldPath), rel)));
+ return ensurePathIsNonModuleName(normalizePath(combinePaths(getDirectoryPath(oldPath), rel)));
};
}
diff --git a/src/services/pathCompletions.ts b/src/services/pathCompletions.ts
index 99ce0afb36cdb..7fd3b7cfd0bf6 100644
--- a/src/services/pathCompletions.ts
+++ b/src/services/pathCompletions.ts
@@ -89,7 +89,9 @@ namespace ts.Completions.PathCompletions {
* Remove the basename from the path. Note that we don't use the basename to filter completions;
* the client is responsible for refining completions.
*/
- fragment = getDirectoryPath(fragment);
+ if (!hasTrailingDirectorySeparator(fragment)) {
+ fragment = getDirectoryPath(fragment);
+ }
if (fragment === "") {
fragment = "." + directorySeparator;
@@ -97,8 +99,9 @@ namespace ts.Completions.PathCompletions {
fragment = ensureTrailingDirectorySeparator(fragment);
- const absolutePath = normalizeAndPreserveTrailingSlash(isRootedDiskPath(fragment) ? fragment : combinePaths(scriptPath, fragment));
- const baseDirectory = getDirectoryPath(absolutePath);
+ // const absolutePath = normalizeAndPreserveTrailingSlash(isRootedDiskPath(fragment) ? fragment : combinePaths(scriptPath, fragment)); // TODO(rbuckton): should use resolvePaths
+ const absolutePath = resolvePath(scriptPath, fragment);
+ const baseDirectory = hasTrailingDirectorySeparator(absolutePath) ? absolutePath : getDirectoryPath(absolutePath);
const ignoreCase = !(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames());
if (tryDirectoryExists(host, baseDirectory)) {
@@ -178,7 +181,7 @@ namespace ts.Completions.PathCompletions {
}
}
- const fragmentDirectory = containsSlash(fragment) ? getDirectoryPath(fragment) : undefined;
+ const fragmentDirectory = containsSlash(fragment) ? hasTrailingDirectorySeparator(fragment) ? fragment : getDirectoryPath(fragment) : undefined;
for (const ambientName of getAmbientModuleCompletions(fragment, fragmentDirectory, typeChecker)) {
result.push(nameAndKind(ambientName, ScriptElementKind.externalModuleName));
}
@@ -239,14 +242,15 @@ namespace ts.Completions.PathCompletions {
// The prefix has two effective parts: the directory path and the base component after the filepath that is not a
// full directory component. For example: directory/path/of/prefix/base*
- const normalizedPrefix = normalizeAndPreserveTrailingSlash(parsed.prefix);
- const normalizedPrefixDirectory = getDirectoryPath(normalizedPrefix);
- const normalizedPrefixBase = getBaseFileName(normalizedPrefix);
+ const normalizedPrefix = resolvePath(parsed.prefix);
+ const normalizedPrefixDirectory = hasTrailingDirectorySeparator(parsed.prefix) ? normalizedPrefix : getDirectoryPath(normalizedPrefix);
+ const normalizedPrefixBase = hasTrailingDirectorySeparator(parsed.prefix) ? "" : getBaseFileName(normalizedPrefix);
const fragmentHasPath = containsSlash(fragment);
+ const fragmentDirectory = fragmentHasPath ? hasTrailingDirectorySeparator(fragment) ? fragment : getDirectoryPath(fragment) : undefined;
// Try and expand the prefix to include any path from the fragment so that we can limit the readDirectory call
- const expandedPrefixDirectory = fragmentHasPath ? combinePaths(normalizedPrefixDirectory, normalizedPrefixBase + getDirectoryPath(fragment)) : normalizedPrefixDirectory;
+ const expandedPrefixDirectory = fragmentHasPath ? combinePaths(normalizedPrefixDirectory, normalizedPrefixBase + fragmentDirectory) : normalizedPrefixDirectory;
const normalizedSuffix = normalizePath(parsed.suffix);
// Need to normalize after combining: If we combinePaths("a", "../b"), we want "b" and not "a/../b".
@@ -418,16 +422,6 @@ namespace ts.Completions.PathCompletions {
return false;
}
- function normalizeAndPreserveTrailingSlash(path: string) {
- if (normalizeSlashes(path) === "./") {
- // normalizePath turns "./" into "". "" + "/" would then be a rooted path instead of a relative one, so avoid this particular case.
- // There is no problem for adding "/" to a non-empty string -- it's only a problem at the beginning.
- return "";
- }
- const norm = normalizePath(path);
- return hasTrailingDirectorySeparator(path) ? ensureTrailingDirectorySeparator(norm) : norm;
- }
-
/**
* Matches a triple slash reference directive with an incomplete string literal for its path. Used
* to determine if the caret is currently within the string literal and capture the literal fragment
diff --git a/src/services/utilities.ts b/src/services/utilities.ts
index 78f18869b372c..0c14599d8bca4 100644
--- a/src/services/utilities.ts
+++ b/src/services/utilities.ts
@@ -1122,11 +1122,6 @@ namespace ts {
return false;
}
- export function hasTrailingDirectorySeparator(path: string) {
- const lastCharacter = path.charAt(path.length - 1);
- return lastCharacter === "/" || lastCharacter === "\\";
- }
-
export function isInReferenceComment(sourceFile: SourceFile, position: number): boolean {
return isInComment(sourceFile, position, /*tokenAtPosition*/ undefined, c => {
const commentText = sourceFile.text.substring(c.pos, c.end);
diff --git a/tests/baselines/reference/importWithTrailingSlash.js b/tests/baselines/reference/importWithTrailingSlash.js
index 9df01dcee66fd..cf1db0a2c561c 100644
--- a/tests/baselines/reference/importWithTrailingSlash.js
+++ b/tests/baselines/reference/importWithTrailingSlash.js
@@ -38,6 +38,6 @@ _2["default"].aIndex;
"use strict";
exports.__esModule = true;
var __1 = require("..");
-var _1 = require("../");
+var __2 = require("../");
__1["default"].a;
-_1["default"].aIndex;
+__2["default"].aIndex;
diff --git a/tests/baselines/reference/library-reference-2.trace.json b/tests/baselines/reference/library-reference-2.trace.json
index 6111a5e78cfe8..649189fbbdda3 100644
--- a/tests/baselines/reference/library-reference-2.trace.json
+++ b/tests/baselines/reference/library-reference-2.trace.json
@@ -9,7 +9,7 @@
"File '/types/jquery/jquery.d.ts' exist - use it as a name resolution result.",
"Resolving real path for '/types/jquery/jquery.d.ts', result '/types/jquery/jquery.d.ts'.",
"======== Type reference directive 'jquery' was successfully resolved to '/types/jquery/jquery.d.ts', primary: true. ========",
- "======== Resolving type reference directive 'jquery', containing file 'test/__inferred type names__.ts', root directory '/types'. ========",
+ "======== Resolving type reference directive 'jquery', containing file '/test/__inferred type names__.ts', root directory '/types'. ========",
"Resolving with primary search path '/types'.",
"'package.json' does not have a 'typings' field.",
"'package.json' has 'types' field 'jquery.d.ts' that references '/types/jquery/jquery.d.ts'.",
diff --git a/tests/baselines/reference/variableDeclarationInStrictMode1.errors.txt b/tests/baselines/reference/variableDeclarationInStrictMode1.errors.txt
index 46782aaa1d69d..9143e4c81c8b1 100644
--- a/tests/baselines/reference/variableDeclarationInStrictMode1.errors.txt
+++ b/tests/baselines/reference/variableDeclarationInStrictMode1.errors.txt
@@ -1,6 +1,6 @@
-lib.d.ts(32,18): error TS2300: Duplicate identifier 'eval'.
tests/cases/compiler/variableDeclarationInStrictMode1.ts(2,5): error TS1100: Invalid use of 'eval' in strict mode.
tests/cases/compiler/variableDeclarationInStrictMode1.ts(2,5): error TS2300: Duplicate identifier 'eval'.
+lib.d.ts(32,18): error TS2300: Duplicate identifier 'eval'.
==== tests/cases/compiler/variableDeclarationInStrictMode1.ts (2 errors) ====
diff --git a/tests/cases/compiler/reactImportDropped.ts b/tests/cases/compiler/reactImportDropped.ts
index a345e7c74fdff..0f4e9ef3aa664 100644
--- a/tests/cases/compiler/reactImportDropped.ts
+++ b/tests/cases/compiler/reactImportDropped.ts
@@ -5,7 +5,7 @@
//@allowSyntheticDefaultImports: true
//@allowJs: true
//@jsx: react
-//@outDir: "build"
+//@outDir: build
//@filename: react.d.ts
export = React;
diff --git a/tests/cases/conformance/references/library-reference-2.ts b/tests/cases/conformance/references/library-reference-2.ts
index 9ac7cd4f4d321..09ed15f653ded 100644
--- a/tests/cases/conformance/references/library-reference-2.ts
+++ b/tests/cases/conformance/references/library-reference-2.ts
@@ -1,7 +1,7 @@
// @noImplicitReferences: true
// @traceResolution: true
// @typeRoots: /types
-// @currentDirectory: test
+// @currentDirectory: /test
// package.json in a primary reference can refer to another file
diff --git a/tests/cases/conformance/types/never/neverInference.ts b/tests/cases/conformance/types/never/neverInference.ts
index 549d27ae1098b..11e55c2efcf0e 100644
--- a/tests/cases/conformance/types/never/neverInference.ts
+++ b/tests/cases/conformance/types/never/neverInference.ts
@@ -1,3 +1,4 @@
+// @lib: es2015
// @strict: true
// @target: es2015
diff --git a/tests/cases/fourslash/completionForStringLiteralExport.ts b/tests/cases/fourslash/completionForStringLiteralExport.ts
index 9239f6173fa17..f20ae1b39f1e2 100644
--- a/tests/cases/fourslash/completionForStringLiteralExport.ts
+++ b/tests/cases/fourslash/completionForStringLiteralExport.ts
@@ -21,7 +21,7 @@
// @Filename: my_typings/some-module/index.d.ts
//// export var x = 9;
-verify.completionsAt(["0", "4"], ["someFile1", "sub", "my_typings"], { isNewIdentifierLocation: true });
+verify.completionsAt(["0", "4"], ["someFile1", "my_typings", "sub"], { isNewIdentifierLocation: true });
verify.completionsAt("1", ["someFile2"], { isNewIdentifierLocation: true });
verify.completionsAt("2", [{ name: "some-module", replacementSpan: test.ranges()[0] }], { isNewIdentifierLocation: true });
verify.completionsAt("3", ["fourslash"], { isNewIdentifierLocation: true });
diff --git a/tests/cases/fourslash/completionForStringLiteralImport1.ts b/tests/cases/fourslash/completionForStringLiteralImport1.ts
index c0c20eb3df86a..0c279d7504caf 100644
--- a/tests/cases/fourslash/completionForStringLiteralImport1.ts
+++ b/tests/cases/fourslash/completionForStringLiteralImport1.ts
@@ -20,7 +20,7 @@
// @Filename: my_typings/some-module/index.d.ts
//// export var x = 9;
-verify.completionsAt("0", ["someFile1", "sub", "my_typings"], { isNewIdentifierLocation: true });
+verify.completionsAt("0", ["someFile1", "my_typings", "sub"], { isNewIdentifierLocation: true });
verify.completionsAt("1", ["someFile2"], { isNewIdentifierLocation: true });
verify.completionsAt("2", [{ name: "some-module", replacementSpan: test.ranges()[0] }], { isNewIdentifierLocation: true });
verify.completionsAt("3", ["fourslash"], { isNewIdentifierLocation: true });
diff --git a/tests/cases/fourslash/completionForStringLiteralImport2.ts b/tests/cases/fourslash/completionForStringLiteralImport2.ts
index 6c73becedc560..885686f499904 100644
--- a/tests/cases/fourslash/completionForStringLiteralImport2.ts
+++ b/tests/cases/fourslash/completionForStringLiteralImport2.ts
@@ -20,7 +20,7 @@
// @Filename: my_typings/some-module/index.d.ts
//// export var x = 9;
-verify.completionsAt("0", ["someFile.ts", "sub", "my_typings"], { isNewIdentifierLocation: true });
+verify.completionsAt("0", ["someFile.ts", "my_typings", "sub"], { isNewIdentifierLocation: true });
verify.completionsAt("1", ["some-module"], { isNewIdentifierLocation: true });
verify.completionsAt("2", ["someOtherFile.ts"], { isNewIdentifierLocation: true });
verify.completionsAt("3", ["some-module"], { isNewIdentifierLocation: true });
diff --git a/tests/cases/fourslash/completionForStringLiteralNonrelativeImport8.ts b/tests/cases/fourslash/completionForStringLiteralNonrelativeImport8.ts
index 24557535699d2..ad0a2fec8258b 100644
--- a/tests/cases/fourslash/completionForStringLiteralNonrelativeImport8.ts
+++ b/tests/cases/fourslash/completionForStringLiteralNonrelativeImport8.ts
@@ -42,6 +42,6 @@
verify.completions({
at: test.markerNames(),
- are: ["prefix", "prefix-only", "2test", "0test", "1test"],
+ are: ["2test", "prefix", "prefix-only", "0test", "1test"],
isNewIdentifierLocation: true,
});
diff --git a/tests/cases/fourslash/completionForStringLiteralRelativeImport3.ts b/tests/cases/fourslash/completionForStringLiteralRelativeImport3.ts
index 84e2ab85e2304..309c0925a775d 100644
--- a/tests/cases/fourslash/completionForStringLiteralRelativeImport3.ts
+++ b/tests/cases/fourslash/completionForStringLiteralRelativeImport3.ts
@@ -39,6 +39,6 @@ verify.completions(
},
{
at: kinds.map(k => `${k}2`),
- are: ["e1", "f1", "f2", "tests", "folder"],
+ are: ["e1", "f1", "f2", "folder", "tests"],
isNewIdentifierLocation: true,
});
diff --git a/tests/cases/fourslash/completionForStringLiteralWithDynamicImport.ts b/tests/cases/fourslash/completionForStringLiteralWithDynamicImport.ts
index 3343eb3874961..4815e52edf516 100644
--- a/tests/cases/fourslash/completionForStringLiteralWithDynamicImport.ts
+++ b/tests/cases/fourslash/completionForStringLiteralWithDynamicImport.ts
@@ -20,7 +20,7 @@
// @Filename: my_typings/some-module/index.d.ts
//// export var x = 9;
-verify.completionsAt("0", ["someFile1", "sub", "my_typings"], { isNewIdentifierLocation: true });
+verify.completionsAt("0", ["someFile1", "my_typings", "sub"], { isNewIdentifierLocation: true });
verify.completionsAt("1", ["someFile2"], { isNewIdentifierLocation: true });
verify.completionsAt("2", [{ name: "some-module", replacementSpan: test.ranges()[0] }], { isNewIdentifierLocation: true });
verify.completionsAt("3", ["fourslash"], { isNewIdentifierLocation: true });
diff --git a/tests/cases/fourslash/findAllRefsForModule.ts b/tests/cases/fourslash/findAllRefsForModule.ts
index a8641625075ef..a9a44d6ffaa82 100644
--- a/tests/cases/fourslash/findAllRefsForModule.ts
+++ b/tests/cases/fourslash/findAllRefsForModule.ts
@@ -16,6 +16,6 @@
const ranges = test.ranges();
const [r0, r1, r2] = ranges;
-verify.referenceGroups(ranges, [{ definition: 'module "/a"', ranges: [r0, r2, r1] }]);
+verify.referenceGroups(ranges, [{ definition: 'module "/a"', ranges: [r0, r1, r2] }]);
// Testing that it works with documentHighlights too
verify.rangesAreDocumentHighlights();
diff --git a/tests/cases/fourslash/navigationBarItemsItemsExternalModules3.ts b/tests/cases/fourslash/navigationBarItemsItemsExternalModules3.ts
index 91ac533d75cd1..e0a8db186f663 100644
--- a/tests/cases/fourslash/navigationBarItemsItemsExternalModules3.ts
+++ b/tests/cases/fourslash/navigationBarItemsItemsExternalModules3.ts
@@ -1,13 +1,13 @@
///
-// @Filename: test/my fil"e.ts
+// @Filename: test/my fil e.ts
////export class Bar {
//// public s: string;
////}
////export var x: number;
verify.navigationTree({
- "text": "\"my fil\\\"e\"",
+ "text": "\"my fil\\te\"",
"kind": "module",
"childItems": [
{
@@ -32,7 +32,7 @@ verify.navigationTree({
verify.navigationBar([
{
- "text": "\"my fil\\\"e\"",
+ "text": "\"my fil\\te\"",
"kind": "module",
"childItems": [
{
diff --git a/tests/webTestResults.html b/tests/webTestResults.html
index f747cdfa75c65..487abfc117deb 100644
--- a/tests/webTestResults.html
+++ b/tests/webTestResults.html
@@ -21,8 +21,10 @@
+
+
-
+