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 @@
+ + - +