Merge pull request #20763 from Microsoft/vfs

Update harness to use single robust virtual file system for tests.
This commit is contained in:
Ron Buckton 2018-05-03 10:25:20 -07:00 committed by GitHub
commit 56648ad0f1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
62 changed files with 5807 additions and 2388 deletions

48
.vscode/tasks.json vendored
View file

@ -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", // See https://go.microsoft.com/fwlink/?LinkId=733558
"command": "gulp", // for the documentation about the tasks.json format
"isShellCommand": true, "version": "2.0.0",
"showOutput": "silent",
"tasks": [ "tasks": [
{ {
"taskName": "local", "type": "shell",
"isBuildCommand": true, "identifier": "local",
"showOutput": "silent", "label": "gulp: local",
"problemMatcher": [ "command": "gulp",
"$tsc" "args": ["local"],
] "group": { "kind": "build", "isDefault": true },
"problemMatcher": ["$gulp-tsc"]
}, },
{ {
"taskName": "tests", "type": "shell",
"showOutput": "silent", "identifier": "tsc",
"problemMatcher": [ "label": "gulp: tsc",
"$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"]
} }
] ]
} }

View file

@ -761,7 +761,7 @@ const nodeServerOutFile = "tests/webTestServer.js";
const nodeServerInFile = "tests/webTestServer.ts"; const nodeServerInFile = "tests/webTestServer.ts";
gulp.task(nodeServerOutFile, /*help*/ false, [servicesFile], () => { gulp.task(nodeServerOutFile, /*help*/ false, [servicesFile], () => {
/** @type {tsc.Settings} */ /** @type {tsc.Settings} */
const settings = getCompilerSettings({ module: "commonjs" }, /*useBuiltCompiler*/ true); const settings = getCompilerSettings({ module: "commonjs", target: "es2015" }, /*useBuiltCompiler*/ true);
return gulp.src(nodeServerInFile) return gulp.src(nodeServerInFile)
.pipe(newer(nodeServerOutFile)) .pipe(newer(nodeServerOutFile))
.pipe(sourcemaps.init()) .pipe(sourcemaps.init())

View file

@ -1,5 +1,6 @@
// This file contains the build logic for the public repo // This file contains the build logic for the public repo
// @ts-check // @ts-check
/// <reference types="jake" />
var fs = require("fs"); var fs = require("fs");
var os = require("os"); var os = require("os");
@ -171,35 +172,34 @@ var compilerFilename = "tsc.js";
var LKGCompiler = path.join(LKGDirectory, compilerFilename); var LKGCompiler = path.join(LKGDirectory, compilerFilename);
var builtLocalCompiler = path.join(builtLocalDirectory, compilerFilename); var builtLocalCompiler = path.join(builtLocalDirectory, compilerFilename);
/* Compiles a file from a list of sources /**
* @param outFile: the target file name * Compiles a file from a list of sources
* @param sources: an array of the names of the source files * @param {string} outFile the target file name
* @param prereqs: prerequisite tasks to compiling the file * @param {string[]} sources an array of the names of the source files
* @param prefixes: a list of files to prepend to the target file * @param {string[]} prereqs prerequisite tasks to compiling the file
* @param useBuiltCompiler: true to use the built compiler, false to use the LKG * @param {string[]} prefixes a list of files to prepend to the target file
* @parap {Object} opts - property bag containing auxiliary options * @param {boolean} useBuiltCompiler true to use the built compiler, false to use the LKG
* @param {boolean} opts.noOutFile: true to compile without using --out * @param {object} [opts] property bag containing auxiliary options
* @param {boolean} opts.generateDeclarations: true to compile using --declaration * @param {boolean} [opts.noOutFile] true to compile without using --out
* @param {string} opts.outDir: value for '--outDir' command line option * @param {boolean} [opts.generateDeclarations] true to compile using --declaration
* @param {boolean} opts.keepComments: false to compile using --removeComments * @param {string} [opts.outDir] value for '--outDir' command line option
* @param {boolean} opts.preserveConstEnums: true if compiler should keep const enums in code * @param {boolean} [opts.keepComments] false to compile using --removeComments
* @param {boolean} opts.noResolve: true if compiler should not include non-rooted files in compilation * @param {boolean} [opts.preserveConstEnums] true if compiler should keep const enums in code
* @param {boolean} opts.stripInternal: true if compiler should remove declarations marked as @internal * @param {boolean} [opts.noResolve] true if compiler should not include non-rooted files in compilation
* @param {boolean} opts.inlineSourceMap: true if compiler should inline sourceMap * @param {boolean} [opts.stripInternal] true if compiler should remove declarations marked as internal
* @param {Array} opts.types: array of types to include in compilation * @param {boolean} [opts.inlineSourceMap] true if compiler should inline sourceMap
* @param callback: a function to execute after the compilation process ends * @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) { function compileFile(outFile, sources, prereqs, prefixes, useBuiltCompiler, opts, callback) {
file(outFile, prereqs, function() { file(outFile, prereqs, function() {
if (process.env.USE_TRANSFORMS === "false") {
useBuiltCompiler = false;
}
var startCompileTime = mark(); var startCompileTime = mark();
opts = opts || {}; opts = opts || {};
var compilerPath = useBuiltCompiler ? builtLocalCompiler : LKGCompiler; var compilerPath = useBuiltCompiler ? builtLocalCompiler : LKGCompiler;
var options = "--noImplicitAny --noImplicitThis --alwaysStrict --noEmitOnError --types "; var options = "--noImplicitAny --noImplicitThis --alwaysStrict --noEmitOnError";
if (opts.types) { if (opts.types) {
options += opts.types.join(","); options += " --types " + opts.types.join(",");
} }
options += " --pretty"; options += " --pretty";
// Keep comments when specifically requested // Keep comments when specifically requested
@ -236,7 +236,7 @@ function compileFile(outFile, sources, prereqs, prefixes, useBuiltCompiler, opts
options += " --inlineSourceMap --inlineSources"; options += " --inlineSourceMap --inlineSources";
} }
else { else {
options += " -sourcemap"; options += " --sourcemap";
} }
} }
options += " --newLine LF"; options += " --newLine LF";
@ -362,7 +362,6 @@ var buildProtocolTs = path.join(scriptsDirectory, "buildProtocol.ts");
var buildProtocolJs = path.join(scriptsDirectory, "buildProtocol.js"); var buildProtocolJs = path.join(scriptsDirectory, "buildProtocol.js");
var buildProtocolDts = path.join(builtLocalDirectory, "protocol.d.ts"); var buildProtocolDts = path.join(builtLocalDirectory, "protocol.d.ts");
var typescriptServicesDts = path.join(builtLocalDirectory, "typescriptServices.d.ts"); var typescriptServicesDts = path.join(builtLocalDirectory, "typescriptServices.d.ts");
var typesMapJson = path.join(builtLocalDirectory, "typesMap.json");
file(buildProtocolTs); 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" }); 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 tsserverLibraryFile = path.join(builtLocalDirectory, "tsserverlibrary.js");
var tsserverLibraryDefinitionFile = path.join(builtLocalDirectory, "tsserverlibrary.d.ts"); var tsserverLibraryDefinitionFile = path.join(builtLocalDirectory, "tsserverlibrary.d.ts");
file(typesMapOutputPath, function() { file(typesMapOutputPath, /** @type {*} */(function() {
var content = fs.readFileSync(path.join(serverDirectory, 'typesMap.json')); var content = fs.readFileSync(path.join(serverDirectory, 'typesMap.json'));
// Validate that it's valid JSON // Validate that it's valid JSON
try { try {
@ -549,7 +548,7 @@ file(typesMapOutputPath, function() {
console.log("Parse error in typesMap.json: " + e); console.log("Parse error in typesMap.json: " + e);
} }
fs.writeFileSync(typesMapOutputPath, content); fs.writeFileSync(typesMapOutputPath, content);
}); }));
compileFile( compileFile(
tsserverLibraryFile, tsserverLibraryFile,
languageServiceLibrarySources, languageServiceLibrarySources,
@ -693,7 +692,7 @@ desc("Builds the test infrastructure using the built compiler");
task("tests", ["local", run].concat(libraryTargets)); task("tests", ["local", run].concat(libraryTargets));
function exec(cmd, completeHandler, errorHandler) { 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 // Add listeners for output and error
ex.addListener("stdout", function (output) { ex.addListener("stdout", function (output) {
process.stdout.write(output); process.stdout.write(output);
@ -1170,7 +1169,7 @@ task("lint", ["build-rules"], () => {
function lint(project, cb) { function lint(project, cb) {
const cmd = `node node_modules/tslint/bin/tslint --project ${project} --formatters-dir ./built/local/tslint/formatters --format autolinkableStylish`; const cmd = `node node_modules/tslint/bin/tslint --project ${project} --formatters-dir ./built/local/tslint/formatters --format autolinkableStylish`;
console.log("Linting: " + cmd); 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", () => { lint("scripts/tslint/tsconfig.json", () => lint("src/tsconfig-base.json", () => {
if (fold.isTravis()) console.log(fold.end("lint")); if (fold.isTravis()) console.log(fold.end("lint"));

View file

@ -39,6 +39,7 @@
"@types/gulp-help": "latest", "@types/gulp-help": "latest",
"@types/gulp-newer": "latest", "@types/gulp-newer": "latest",
"@types/gulp-sourcemaps": "latest", "@types/gulp-sourcemaps": "latest",
"@types/jake": "latest",
"@types/merge2": "latest", "@types/merge2": "latest",
"@types/minimatch": "latest", "@types/minimatch": "latest",
"@types/minimist": "latest", "@types/minimist": "latest",
@ -47,6 +48,7 @@
"@types/node": "8.5.5", "@types/node": "8.5.5",
"@types/q": "latest", "@types/q": "latest",
"@types/run-sequence": "latest", "@types/run-sequence": "latest",
"@types/source-map-support": "latest",
"@types/through2": "latest", "@types/through2": "latest",
"@types/travis-fold": "latest", "@types/travis-fold": "latest",
"@types/xml2js": "^0.4.0", "@types/xml2js": "^0.4.0",

View file

@ -1820,7 +1820,7 @@ namespace ts {
} }
function getDefaultCompilerOptions(configFileName?: string) { 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 } ? { allowJs: true, maxNodeModuleJsDepth: 2, allowSyntheticDefaultImports: true, skipLibCheck: true, noEmit: true }
: {}; : {};
return options; return options;
@ -1835,7 +1835,7 @@ namespace ts {
} }
function getDefaultTypeAcquisition(configFileName?: string): TypeAcquisition { 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, function convertTypeAcquisitionFromJsonWorker(jsonOptions: any,

View file

@ -1890,12 +1890,12 @@ namespace ts {
comparer(a[key], b[key]); comparer(a[key], b[key]);
} }
function getDiagnosticFileName(diagnostic: Diagnostic): string { function getDiagnosticFilePath(diagnostic: Diagnostic): string {
return diagnostic.file ? diagnostic.file.fileName : undefined; return diagnostic.file ? diagnostic.file.path : undefined;
} }
export function compareDiagnostics(d1: Diagnostic, d2: Diagnostic): Comparison { 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.start, d2.start) ||
compareValues(d1.length, d2.length) || compareValues(d1.length, d2.length) ||
compareValues(d1.code, d2.code) || compareValues(d1.code, d2.code) ||
@ -1932,110 +1932,6 @@ namespace ts {
return text1 ? Comparison.GreaterThan : Comparison.LessThan; 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://<host>/<path>
// if <host> is omitted then it is assumed that host value is 'localhost',
// however slash after the omitted <host> 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) { export function getEmitScriptTarget(compilerOptions: CompilerOptions) {
return compilerOptions.target || ScriptTarget.ES3; return compilerOptions.target || ScriptTarget.ES3;
} }
@ -2089,8 +1985,208 @@ namespace ts {
return true; 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) { 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 { 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); : getRelativePathToDirectoryOrUrl(basePath, absoluteOrRelativePath, basePath, getCanonicalFileName, /*isAbsolutePathAnUrl*/ false);
} }
function normalizedPathComponents(path: string, rootLength: number) { function pathComponents(path: string, rootLength: number) {
const normalizedParts = getNormalizedParts(path, rootLength); const root = path.substring(0, rootLength);
return [path.substr(0, rootLength)].concat(normalizedParts); const rest = path.substring(rootLength).split(directorySeparator);
if (rest.length && !lastOrUndefined(rest)) rest.pop();
return [root, ...rest];
} }
/**
* 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<string>) {
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;
}
/**
* 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) { export function getNormalizedPathComponents(path: string, currentDirectory: string) {
path = normalizeSlashes(path); return reducePathComponents(getPathComponents(path, currentDirectory));
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);
}
return normalizedPathComponents(path, rootLength);
} }
export function getNormalizedAbsolutePath(fileName: string, currentDirectory: string) { export function getNormalizedAbsolutePath(fileName: string, currentDirectory: string) {
return getNormalizedPathFromPathComponents(getNormalizedPathComponents(fileName, currentDirectory)); return getPathFromPathComponents(getNormalizedPathComponents(fileName, currentDirectory));
} }
export function getNormalizedPathFromPathComponents(pathComponents: ReadonlyArray<string>) { /**
if (pathComponents && pathComponents.length) { * Formats a parsed path consisting of a root component (at index 0) and zero or more path
return pathComponents[0] + pathComponents.slice(1).join(directorySeparator); * segments (at indices > 0).
} */
export function getPathFromPathComponents(pathComponents: ReadonlyArray<string>) {
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) { function getPathComponentsRelativeTo(from: string, to: string, stringEqualityComparer: (a: string, b: string) => boolean, getCanonicalFileName: GetCanonicalFileName) {
// Get root length of http://www.website.com/folder1/folder2/ const fromComponents = reducePathComponents(getPathComponents(from));
// In this example the root is: http://www.website.com/ const toComponents = reducePathComponents(getPathComponents(to));
// normalized path components should be ["http://www.website.com/", "folder1", "folder2"]
const urlLength = url.length; let start: number;
// Initial root length is http:// part for (start = 0; start < fromComponents.length && start < toComponents.length; start++) {
let rootLength = url.indexOf("://") + "://".length; const fromComponent = getCanonicalFileName(fromComponents[start]);
while (rootLength < urlLength) { const toComponent = getCanonicalFileName(toComponents[start]);
// Consume all immediate slashes in the protocol const comparer = start === 0 ? equateStringsCaseInsensitive : stringEqualityComparer;
// eg.initial rootlength is just file:// but it needs to consume another "/" in file:/// if (!comparer(fromComponent, toComponent)) break;
if (url.charCodeAt(rootLength) === CharacterCodes.slash) {
rootLength++;
}
else {
// non slash character means we continue proceeding to next component of root search
break;
}
} }
// there are no parts after http:// just return current string as the pathComponent if (start === 0) {
if (rootLength === urlLength) { return toComponents;
return [url];
} }
// Find the index of "/" after website.com so the root can be http://www.website.com/ (from existing http://) const components = toComponents.slice(start);
const indexOfNextSlash = url.indexOf(directorySeparator, rootLength); const relative: string[] = [];
if (indexOfNextSlash !== -1) { for (; start < fromComponents.length; start++) {
// Found the "/" after the website.com so the root is length of http://www.website.com/ relative.push("..");
// 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];
} }
return ["", ...relative, ...components];
} }
function getNormalizedPathOrUrlComponents(pathOrUrl: string, currentDirectory: string) { /**
if (isUrl(pathOrUrl)) { * Gets a relative path that can be used to traverse between `from` and `to`.
return getNormalizedPathComponentsOfUrl(pathOrUrl); */
} export function getRelativePath(from: string, to: string, ignoreCase: boolean): string;
else { /**
return getNormalizedPathComponents(pathOrUrl, currentDirectory); * 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) { export function getRelativePathToDirectoryOrUrl(directoryPathOrUrl: string, relativeOrAbsolutePath: string, currentDirectory: string, getCanonicalFileName: GetCanonicalFileName, isAbsolutePathAnUrl: boolean) {
const pathComponents = getNormalizedPathOrUrlComponents(relativeOrAbsolutePath, currentDirectory); const pathComponents = getPathComponentsRelativeTo(
const directoryComponents = getNormalizedPathOrUrlComponents(directoryPathOrUrl, currentDirectory); resolvePath(currentDirectory, directoryPathOrUrl),
if (directoryComponents.length > 1 && lastOrUndefined(directoryComponents) === "") { resolvePath(currentDirectory, relativeOrAbsolutePath),
// If the directory path given was of type test/cases/ then we really need components of directory to be only till its name equateStringsCaseSensitive,
// that is ["test", "cases", ""] needs to be actually ["test", "cases"] getCanonicalFileName
directoryComponents.pop(); );
const firstComponent = pathComponents[0];
if (isAbsolutePathAnUrl && isRootedDiskPath(firstComponent)) {
const prefix = firstComponent.charAt(0) === directorySeparator ? "file://" : "file:///";
pathComponents[0] = prefix + firstComponent;
} }
// Find the component that differs return getPathFromPathComponents(pathComponents);
let joinStartIndex: number;
for (joinStartIndex = 0; joinStartIndex < pathComponents.length && joinStartIndex < directoryComponents.length; joinStartIndex++) {
if (getCanonicalFileName(directoryComponents[joinStartIndex]) !== getCanonicalFileName(pathComponents[joinStartIndex])) {
break;
}
} }
// Get the relative path /**
if (joinStartIndex) { * Ensures a path is either absolute (prefixed with `/` or `c:`) or dot-relative (prefixed
let relativePath = ""; * with `./` or `../`) so as not to be confused with an unprefixed module name.
const relativePathComponents = pathComponents.slice(joinStartIndex, pathComponents.length); */
for (; joinStartIndex < directoryComponents.length; joinStartIndex++) { export function ensurePathIsNonModuleName(path: string): string {
if (directoryComponents[joinStartIndex] !== "") { return getRootLength(path) === 0 && !pathIsRelative(path) ? "./" + path : path;
relativePath = relativePath + ".." + directorySeparator;
}
} }
return relativePath + relativePathComponents.join(directorySeparator); /**
* 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<string>, ignoreCase: boolean): string;
export function getBaseFileName(path: string, extensions?: string | ReadonlyArray<string>, ignoreCase?: boolean) {
path = normalizeSlashes(path);
// if the path provided is itself the root, then it has not file name.
const rootLength = getRootLength(path);
if (rootLength === path.length) return "";
// 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;
} }
// Cant find the relative path, get the absolute path /**
let absolutePath = getNormalizedPathFromPathComponents(pathComponents); * Combines paths. If a path is absolute, it replaces any previous path.
if (isAbsolutePathAnUrl && isRootedDiskPath(absolutePath)) { */
absolutePath = "file:///" + absolutePath; 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;
} }
return absolutePath; /**
* 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 getRelativePath(path: string, directoryPath: string, getCanonicalFileName: GetCanonicalFileName) { /**
const relativePath = getRelativePathToDirectoryOrUrl(directoryPath, path, directoryPath, getCanonicalFileName, /*isAbsolutePathAnUrl*/ false); * Determines whether a path has a trailing separator (`/` or `\\`).
return ensurePathIsRelative(relativePath); */
} export function hasTrailingDirectorySeparator(path: string) {
if (path.length === 0) return false;
export function ensurePathIsRelative(path: string): string { const ch = path.charCodeAt(path.length - 1);
return !pathIsRelative(path) ? "./" + path : path; return ch === CharacterCodes.slash || ch === CharacterCodes.backslash;
}
export function getBaseFileName(path: string) {
if (path === undefined) {
return undefined;
}
const i = path.lastIndexOf(directorySeparator);
return i < 0 ? path : path.substring(i + 1);
}
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;
} }
/** /**
@ -2248,7 +2412,7 @@ namespace ts {
export function removeTrailingDirectorySeparator(path: Path): Path; export function removeTrailingDirectorySeparator(path: Path): Path;
export function removeTrailingDirectorySeparator(path: string): string; export function removeTrailingDirectorySeparator(path: string): string;
export function removeTrailingDirectorySeparator(path: string) { export function removeTrailingDirectorySeparator(path: string) {
if (path.charAt(path.length - 1) === directorySeparator) { if (hasTrailingDirectorySeparator(path)) {
return path.substr(0, path.length - 1); return path.substr(0, path.length - 1);
} }
@ -2262,47 +2426,78 @@ namespace ts {
export function ensureTrailingDirectorySeparator(path: Path): Path; export function ensureTrailingDirectorySeparator(path: Path): Path;
export function ensureTrailingDirectorySeparator(path: string): string; export function ensureTrailingDirectorySeparator(path: string): string;
export function ensureTrailingDirectorySeparator(path: string) { export function ensureTrailingDirectorySeparator(path: string) {
if (path.charAt(path.length - 1) !== directorySeparator) { if (!hasTrailingDirectorySeparator(path)) {
return path + directorySeparator; return path + directorySeparator;
} }
return path; 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 === b) return Comparison.EqualTo;
if (a === undefined) return Comparison.LessThan; if (a === undefined) return Comparison.LessThan;
if (b === undefined) return Comparison.GreaterThan; if (b === undefined) return Comparison.GreaterThan;
a = removeTrailingDirectorySeparator(a); const aComponents = reducePathComponents(getPathComponents(a));
b = removeTrailingDirectorySeparator(b); const bComponents = reducePathComponents(getPathComponents(b));
const aComponents = getNormalizedPathComponents(a, currentDirectory);
const bComponents = getNormalizedPathComponents(b, currentDirectory);
const sharedLength = Math.min(aComponents.length, bComponents.length); const sharedLength = Math.min(aComponents.length, bComponents.length);
const comparer = getStringComparer(ignoreCase);
for (let i = 0; i < sharedLength; i++) { 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) { if (result !== Comparison.EqualTo) {
return result; return result;
} }
} }
return compareValues(aComponents.length, bComponents.length); 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 === undefined || child === undefined) return false;
if (parent === child) return true; if (parent === child) return true;
parent = removeTrailingDirectorySeparator(parent); const parentComponents = reducePathComponents(getPathComponents(parent));
child = removeTrailingDirectorySeparator(child); const childComponents = reducePathComponents(getPathComponents(child));
if (parent === child) return true;
const parentComponents = getNormalizedPathComponents(parent, currentDirectory);
const childComponents = getNormalizedPathComponents(child, currentDirectory);
if (childComponents.length < parentComponents.length) { if (childComponents.length < parentComponents.length) {
return false; return false;
} }
const equalityComparer = ignoreCase ? equateStringsCaseInsensitive : equateStringsCaseSensitive; const componentEqualityComparer = ignoreCase ? equateStringsCaseInsensitive : equateStringsCaseSensitive;
for (let i = 0; i < parentComponents.length; i++) { for (let i = 0; i < parentComponents.length; i++) {
const equalityComparer = i === 0 ? equateStringsCaseInsensitive : componentEqualityComparer;
if (!equalityComparer(parentComponents[i], childComponents[i])) { if (!equalityComparer(parentComponents[i], childComponents[i])) {
return false; return false;
} }
@ -2791,7 +2986,14 @@ namespace ts {
} }
export function changeExtension<T extends string | Path>(path: T, newExtension: string): T { export function changeExtension<T extends string | Path>(path: T, newExtension: string): T {
return <T>(removeFileExtension(path) + newExtension); return <T>changeAnyExtension(path, newExtension, extensionsToRemove, /*ignoreCase*/ false);
}
export function changeAnyExtension(path: string, ext: string): string;
export function changeAnyExtension(path: string, ext: string, extensions: string | ReadonlyArray<string>, ignoreCase: boolean): string;
export function changeAnyExtension(path: string, ext: string, extensions?: string | ReadonlyArray<string>, 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<Extension>(supportedTypescriptExtensionsForExtractExtension, e => fileExtensionIs(path, e)) || find(supportedJavascriptExtensions, e => fileExtensionIs(path, e)); return find<Extension>(supportedTypescriptExtensionsForExtractExtension, e => fileExtensionIs(path, e)) || find(supportedJavascriptExtensions, e => fileExtensionIs(path, e));
} }
function getAnyExtensionFromPathWorker(path: string, extensions: string | ReadonlyArray<string>, 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<string>, ignoreCase: boolean): string;
export function getAnyExtensionFromPath(path: string, extensions?: string | ReadonlyArray<string>, ignoreCase?: boolean): string {
// Retrieves any string from the final "." onwards from a base file name. // Retrieves any string from the final "." onwards from a base file name.
// Unlike extensionFromPath, which throws an exception on unrecognized extensions. // Unlike extensionFromPath, which throws an exception on unrecognized extensions.
export function getAnyExtensionFromPath(path: string): string | undefined { if (extensions) {
return getAnyExtensionFromPathWorker(path, extensions, ignoreCase ? equateStringsCaseInsensitive : equateStringsCaseSensitive);
}
const baseFileName = getBaseFileName(path); const baseFileName = getBaseFileName(path);
const extensionIndex = baseFileName.lastIndexOf("."); const extensionIndex = baseFileName.lastIndexOf(".");
if (extensionIndex >= 0) { if (extensionIndex >= 0) {
return baseFileName.substring(extensionIndex); return baseFileName.substring(extensionIndex);
} }
return "";
} }
export function isCheckJsEnabledForFile(sourceFile: SourceFile, compilerOptions: CompilerOptions) { export function isCheckJsEnabledForFile(sourceFile: SourceFile, compilerOptions: CompilerOptions) {

View file

@ -65,7 +65,8 @@ namespace ts {
// JavaScript files are always LanguageVariant.JSX, as JSX syntax is allowed in .js files also. // 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. // 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 // 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 (options.jsx === JsxEmit.Preserve) {
if (isSourceFileJavaScript(sourceFile)) { if (isSourceFileJavaScript(sourceFile)) {
if (fileExtensionIs(sourceFile.fileName, Extension.Jsx)) { if (fileExtensionIs(sourceFile.fileName, Extension.Jsx)) {

View file

@ -806,7 +806,7 @@ namespace ts {
if (state.traceEnabled) { if (state.traceEnabled) {
trace(state.host, Diagnostics.Loading_module_as_file_Slash_folder_candidate_module_location_0_target_file_type_1, candidate, Extensions[extensions]); 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) { if (!onlyRecordFailures) {
const parentOfCandidate = getDirectoryPath(candidate); const parentOfCandidate = getDirectoryPath(candidate);
if (!directoryProbablyExists(parentOfCandidate, state.host)) { if (!directoryProbablyExists(parentOfCandidate, state.host)) {

View file

@ -57,7 +57,7 @@ namespace ts {
return currentDirectory; return currentDirectory;
} }
return getNormalizedPathFromPathComponents(commonPathComponents); return getPathFromPathComponents(commonPathComponents);
} }
interface OutputFingerprint { interface OutputFingerprint {

View file

@ -63,7 +63,7 @@ namespace ts {
} }
function getCachedFileSystemEntries(rootDirPath: Path): MutableFileSystemEntries | undefined { function getCachedFileSystemEntries(rootDirPath: Path): MutableFileSystemEntries | undefined {
return cachedReadDirectoryResult.get(rootDirPath); return cachedReadDirectoryResult.get(ensureTrailingDirectorySeparator(rootDirPath));
} }
function getCachedFileSystemEntriesForBaseDir(path: Path): MutableFileSystemEntries | undefined { function getCachedFileSystemEntriesForBaseDir(path: Path): MutableFileSystemEntries | undefined {
@ -80,7 +80,7 @@ namespace ts {
directories: host.getDirectories(rootDir) || [] directories: host.getDirectories(rootDir) || []
}; };
cachedReadDirectoryResult.set(rootDirPath, resultFromHost); cachedReadDirectoryResult.set(ensureTrailingDirectorySeparator(rootDirPath), resultFromHost);
return resultFromHost; return resultFromHost;
} }
@ -90,6 +90,7 @@ namespace ts {
* The host request is done under try catch block to avoid caching incorrect result * The host request is done under try catch block to avoid caching incorrect result
*/ */
function tryReadDirectory(rootDir: string, rootDirPath: Path): MutableFileSystemEntries | undefined { function tryReadDirectory(rootDir: string, rootDirPath: Path): MutableFileSystemEntries | undefined {
rootDirPath = ensureTrailingDirectorySeparator(rootDirPath);
const cachedResult = getCachedFileSystemEntries(rootDirPath); const cachedResult = getCachedFileSystemEntries(rootDirPath);
if (cachedResult) { if (cachedResult) {
return cachedResult; return cachedResult;
@ -100,7 +101,7 @@ namespace ts {
} }
catch (_e) { catch (_e) {
// If there is exception to read directories, dont cache the result and direct the calls to host // 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; return undefined;
} }
} }
@ -142,7 +143,7 @@ namespace ts {
function directoryExists(dirPath: string): boolean { function directoryExists(dirPath: string): boolean {
const path = toPath(dirPath); const path = toPath(dirPath);
return cachedReadDirectoryResult.has(path) || host.directoryExists(dirPath); return cachedReadDirectoryResult.has(ensureTrailingDirectorySeparator(path)) || host.directoryExists(dirPath);
} }
function createDirectory(dirPath: string) { function createDirectory(dirPath: string) {

321
src/harness/collections.ts Normal file
View file

@ -0,0 +1,321 @@
namespace collections {
export interface SortOptions<T> {
comparer: (a: T, b: T) => number;
sort: "insertion" | "comparison";
}
export class SortedMap<K, V> {
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<K>, 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<T>(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<T>(iterable: Iterable<T>): Iterator<T> {
return iterable[Symbol.iterator]();
}
export function nextResult<T>(iterator: Iterator<T>): IteratorResult<T> | undefined {
const result = iterator.next();
return result.done ? undefined : result;
}
export function closeIterator<T>(iterator: Iterator<T>) {
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);
}
}
}

248
src/harness/compiler.ts Normal file
View file

@ -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<documents.TextDocument>;
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<ts.Diagnostic>;
public readonly js: ReadonlyMap<string, documents.TextDocument>;
public readonly dts: ReadonlyMap<string, documents.TextDocument>;
public readonly maps: ReadonlyMap<string, documents.TextDocument>;
private _inputs: documents.TextDocument[] = [];
private _inputsAndOutputs: collections.SortedMap<string, CompilationOutput>;
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<string, documents.TextDocument>({ comparer: this.vfs.stringComparer, sort: "insertion" });
const dts = this.dts = new collections.SortedMap<string, documents.TextDocument>({ comparer: this.vfs.stringComparer, sort: "insertion" });
const maps = this.maps = new collections.SortedMap<string, documents.TextDocument>({ 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<string, CompilationOutput>({ 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<documents.TextDocument> {
return this._inputs;
}
public get outputs(): ReadonlyArray<documents.TextDocument> {
return this.host.outputs;
}
public get traces(): ReadonlyArray<string> {
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<documents.TextDocument> | 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);
}
}

View file

@ -1,6 +1,10 @@
/// <reference path="harness.ts" /> /// <reference path="harness.ts" />
/// <reference path="runnerbase.ts" /> /// <reference path="runnerbase.ts" />
/// <reference path="typeWriter.ts" /> /// <reference path="typeWriter.ts" />
/// <reference path="./vpath.ts" />
/// <reference path="./vfs.ts" />
/// <reference path="./compiler.ts" />
/// <reference path="./documents.ts" />
const enum CompilerTestType { const enum CompilerTestType {
Conformance, Conformance,
@ -41,151 +45,6 @@ class CompilerBaselineRunner extends RunnerBase {
return this.enumerateFiles(this.basePath, /\.tsx?$/, { recursive: true }); 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 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);
}
}
lastUnit = units[units.length - 1];
hasNonDtsFiles = ts.forEach(units, unit => !ts.fileExtensionIs(unit.name, ts.Extension.Dts));
// We need to assemble the list of input files for the compiler and other related files on the 'filesystem' (ie in a multi-file test)
// If the last file in a test uses require or a triple slash reference we'll assume all other files will be brought in via references,
// otherwise, assume all files are just meant to be in the same compilation session without explicit references to one another.
toBeCompiled = [];
otherFiles = [];
if (testCaseContent.settings.noImplicitReferences || /require\(/.test(lastUnit.content) || /reference\spath/.test(lastUnit.content)) {
toBeCompiled.push(this.createHarnessTestFile(lastUnit, rootDir));
units.forEach(unit => {
if (unit.name !== lastUnit.name) {
otherFiles.push(this.createHarnessTestFile(unit, rootDir));
}
});
}
else {
toBeCompiled = units.map(unit => {
return this.createHarnessTestFile(unit, rootDir);
});
}
if (tsConfigOptions && tsConfigOptions.configFilePath !== undefined) {
tsConfigOptions.configFilePath = ts.combinePaths(rootDir, tsConfigOptions.configFilePath);
tsConfigOptions.configFile.fileName = tsConfigOptions.configFilePath;
}
const output = Harness.Compiler.compileFiles(
toBeCompiled, otherFiles, harnessSettings, /*options*/ tsConfigOptions, /*currentDirectory*/ harnessSettings.currentDirectory);
options = output.options;
result = output.result;
});
after(() => {
// Mocha holds onto the closure environment of the describe callback even after the test is done.
// Therefore we have to clean out large objects after the test is done.
justName = undefined;
lastUnit = undefined;
hasNonDtsFiles = undefined;
result = undefined;
options = undefined;
toBeCompiled = undefined;
otherFiles = undefined;
tsConfigFiles = undefined;
});
// check errors
it("Correct errors for " + fileName, () => {
Harness.Compiler.doErrorBaseline(justName, tsConfigFiles.concat(toBeCompiled, otherFiles), result.errors, !!options.pretty);
});
it (`Correct module resolution tracing for ${fileName}`, () => {
if (options.traceResolution) {
Harness.Baseline.runBaseline(justName.replace(/\.tsx?$/, ".trace.json"), () => {
return JSON.stringify(result.traceResults || [], undefined, 4);
});
}
});
// 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;
});
}
});
it("Correct JS output for " + fileName, () => {
if (hasNonDtsFiles && this.emit) {
Harness.Compiler.doJsEmitBaseline(justName, fileName, options, result, tsConfigFiles, toBeCompiled, otherFiles, harnessSettings);
}
});
it("Correct Sourcemap output for " + fileName, () => {
Harness.Compiler.doSourcemapBaseline(justName, options, result, harnessSettings);
});
it("Correct type/symbol baselines for " + fileName, () => {
if (fileName.indexOf("APISample") >= 0) {
return;
}
Harness.Compiler.doTypeAndSymbolBaseline(justName, result.program, toBeCompiled.concat(otherFiles).filter(file => !!result.program.getSourceFile(file.unitName)));
});
});
}
private createHarnessTestFile(lastUnit: Harness.TestCaseParser.TestUnitData, rootDir: string, unitName?: string): Harness.Compiler.TestFile {
return { unitName: unitName || this.makeUnitName(lastUnit.name, rootDir), content: lastUnit.content, fileOptions: lastUnit.fileOptions };
}
public initializeTests() { public initializeTests() {
describe(this.testSuiteName + " tests", () => { describe(this.testSuiteName + " tests", () => {
describe("Setup compiler for compiler baselines", () => { describe("Setup compiler for compiler baselines", () => {
@ -193,18 +52,32 @@ class CompilerBaselineRunner extends RunnerBase {
}); });
// this will set up a series of describe/it blocks to run between the setup and cleanup phases // this will set up a series of describe/it blocks to run between the setup and cleanup phases
if (this.tests.length === 0) { const files = this.tests.length > 0 ? this.tests : this.enumerateTestFiles();
const testFiles = this.enumerateTestFiles(); files.forEach(file => { this.checkTestCodeOutput(vpath.normalizeSeparators(file)); });
testFiles.forEach(fn => {
fn = fn.replace(/\\/g, "/");
this.checkTestCodeOutput(fn);
}); });
} }
else {
this.tests.forEach(test => this.checkTestCodeOutput(test)); public checkTestCodeOutput(fileName: string) {
} for (const { name, payload } of CompilerTest.getConfigurations(fileName)) {
describe(`${this.testSuiteName} tests for ${fileName}${name ? ` (${name})` : ``}`, () => {
this.runSuite(fileName, payload);
}); });
} }
}
private runSuite(fileName: string, testCaseContent: Harness.TestCaseParser.TestCaseContent) {
// Mocha holds onto the closure environment of the describe callback even after the test is done.
// Everything declared here should be cleared out in the "after" callback.
let compilerTest: CompilerTest | undefined;
before(() => { compilerTest = new CompilerTest(fileName, testCaseContent); });
it(`Correct errors for ${fileName}`, () => { compilerTest.verifyDiagnostics(); });
it(`Correct module resolution tracing for ${fileName}`, () => { compilerTest.verifyModuleResolution(); });
it(`Correct sourcemap content for ${fileName}`, () => { compilerTest.verifySourceMapRecord(); });
it(`Correct JS output for ${fileName}`, () => { if (this.emit) compilerTest.verifyJavaScriptOutput(); });
it(`Correct Sourcemap output for ${fileName}`, () => { compilerTest.verifySourceMapOutput(); });
it(`Correct type/symbol baselines for ${fileName}`, () => { compilerTest.verifyTypesAndSymbols(); });
after(() => { compilerTest = undefined; });
}
private parseOptions() { private parseOptions() {
if (this.options && this.options.length > 0) { if (this.options && this.options.length > 0) {
@ -223,3 +96,189 @@ class CompilerBaselineRunner extends RunnerBase {
} }
} }
} }
interface CompilerTestConfiguration {
name: string;
payload: Harness.TestCaseParser.TestCaseContent;
}
class CompilerTest {
private fileName: string;
private justName: string;
private lastUnit: Harness.TestCaseParser.TestUnitData;
private harnessSettings: Harness.TestCaseParser.CompilerSettings;
private hasNonDtsFiles: boolean;
private result: 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[];
constructor(fileName: string, testCaseContent: Harness.TestCaseParser.TestCaseContent) {
this.fileName = fileName;
this.justName = vpath.basename(fileName);
const rootDir = fileName.indexOf("conformance") === -1 ? "tests/cases/compiler/" : ts.getDirectoryPath(fileName) + "/";
const units = testCaseContent.testUnitData;
this.harnessSettings = testCaseContent.settings;
let tsConfigOptions: ts.CompilerOptions;
this.tsConfigFiles = [];
if (testCaseContent.tsConfig) {
assert.equal(testCaseContent.tsConfig.fileNames.length, 0, `list of files in tsconfig is not currently supported`);
tsConfigOptions = ts.cloneCompilerOptions(testCaseContent.tsConfig.options);
this.tsConfigFiles.push(this.createHarnessTestFile(testCaseContent.tsConfigFileUnitData, rootDir, ts.combinePaths(rootDir, tsConfigOptions.configFilePath)));
}
else {
const baseUrl = this.harnessSettings.baseUrl;
if (baseUrl !== undefined && !ts.isRootedDiskPath(baseUrl)) {
this.harnessSettings.baseUrl = ts.getNormalizedAbsolutePath(baseUrl, rootDir);
}
}
this.lastUnit = units[units.length - 1];
this.hasNonDtsFiles = ts.forEach(units, unit => !ts.fileExtensionIs(unit.name, ts.Extension.Dts));
// We need to assemble the list of input files for the compiler and other related files on the 'filesystem' (ie in a multi-file test)
// If the last file in a test uses require or a triple slash reference we'll assume all other files will be brought in via references,
// otherwise, assume all files are just meant to be in the same compilation session without explicit references to one another.
this.toBeCompiled = [];
this.otherFiles = [];
if (testCaseContent.settings.noImplicitReferences || /require\(/.test(this.lastUnit.content) || /reference\spath/.test(this.lastUnit.content)) {
this.toBeCompiled.push(this.createHarnessTestFile(this.lastUnit, rootDir));
units.forEach(unit => {
if (unit.name !== this.lastUnit.name) {
this.otherFiles.push(this.createHarnessTestFile(unit, rootDir));
}
});
}
else {
this.toBeCompiled = units.map(unit => {
return this.createHarnessTestFile(unit, rootDir);
});
}
if (tsConfigOptions && tsConfigOptions.configFilePath !== undefined) {
tsConfigOptions.configFilePath = ts.combinePaths(rootDir, tsConfigOptions.configFilePath);
tsConfigOptions.configFile.fileName = tsConfigOptions.configFilePath;
}
this.result = Harness.Compiler.compileFiles(
this.toBeCompiled,
this.otherFiles,
this.harnessSettings,
/*options*/ tsConfigOptions,
/*currentDirectory*/ this.harnessSettings.currentDirectory);
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"}`;
}
const settings = { ...testCaseContent.settings };
if (scriptTarget) settings.target = scriptTarget;
if (moduleKind) settings.module = moduleKind;
configurations.push({ name, payload: { ...testCaseContent, settings } });
}
}
return configurations;
}
public verifyDiagnostics() {
// check errors
Harness.Compiler.doErrorBaseline(
this.justName,
this.tsConfigFiles.concat(this.toBeCompiled, this.otherFiles),
this.result.diagnostics,
!!this.options.pretty);
}
public verifyModuleResolution() {
if (this.options.traceResolution) {
Harness.Baseline.runBaseline(this.justName.replace(/\.tsx?$/, ".trace.json"), () => {
return utils.removeTestPathPrefixes(JSON.stringify(this.result.traces, undefined, 4));
});
}
}
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;
});
}
}
public verifyJavaScriptOutput() {
if (this.hasNonDtsFiles) {
Harness.Compiler.doJsEmitBaseline(
this.justName,
this.fileName,
this.options,
this.result,
this.tsConfigFiles,
this.toBeCompiled,
this.otherFiles,
this.harnessSettings);
}
}
public verifySourceMapOutput() {
Harness.Compiler.doSourcemapBaseline(
this.justName,
this.options,
this.result,
this.harnessSettings);
}
public verifyTypesAndSymbols() {
if (this.fileName.indexOf("APISample") >= 0) {
return;
}
Harness.Compiler.doTypeAndSymbolBaseline(
this.justName,
this.result.program,
this.toBeCompiled.concat(this.otherFiles).filter(file => !!this.result.program.getSourceFile(file.unitName)));
}
private static _split(text: string) {
const entries = text && text.split(",").map(s => s.toLowerCase().trim()).filter(s => s.length > 0);
return entries && entries.length > 0 ? entries : [""];
}
private makeUnitName(name: string, root: string) {
const path = ts.toPath(name, root, 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 };
}
}

187
src/harness/documents.ts Normal file
View file

@ -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<string, string>;
public readonly file: string;
public readonly text: string;
private _lineStarts: ReadonlyArray<number> | undefined;
private _testFile: Harness.Compiler.TestFile | undefined;
constructor(file: string, text: string, meta?: Map<string, string>) {
this.file = file;
this.text = text;
this.meta = meta || new Map<string, string>();
}
public get lineStarts(): ReadonlyArray<number> {
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<string, string>()));
}
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<string, string>)
});
}
}
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<string> = [];
public readonly sourcesContent: ReadonlyArray<string> | undefined;
public readonly mappings: ReadonlyArray<Mapping> = [];
public readonly names: ReadonlyArray<string> | 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<Mapping> | undefined {
return this._emittedLineMappings[emittedLine];
}
public getMappingsForSourceLine(sourceIndex: number, sourceLine: number): ReadonlyArray<Mapping> | 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;
}
}
}

View file

@ -41,7 +41,7 @@ abstract class ExternalCompileRunnerBase extends RunnerBase {
const cp = require("child_process"); const cp = require("child_process");
it("should build successfully", () => { 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"; const stdio = isWorker ? "pipe" : "inherit";
let types: string[]; let types: string[];
if (fs.existsSync(path.join(cwd, "test.json"))) { 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 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()}`); 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) { if (types) {
args.push("--types", types.join(",")); args.push("--types", types.join(","));
} }

376
src/harness/fakes.ts Normal file
View file

@ -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<string, string>;
}
/**
* 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<string, string> | 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<string>, exclude?: ReadonlyArray<string>, include?: ReadonlyArray<string>, 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<string, ts.SourceFile>;
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<string, ts.SourceFile>({ 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;
}
}
}

View file

@ -18,6 +18,7 @@
/// <reference path="harnessLanguageService.ts" /> /// <reference path="harnessLanguageService.ts" />
/// <reference path="harness.ts" /> /// <reference path="harness.ts" />
/// <reference path="fourslashRunner.ts" /> /// <reference path="fourslashRunner.ts" />
/// <reference path="./compiler.ts" />
namespace FourSlash { namespace FourSlash {
ts.disableIncrementalParsing = false; ts.disableIncrementalParsing = false;
@ -277,7 +278,13 @@ namespace FourSlash {
if (configFileName) { if (configFileName) {
const baseDir = ts.normalizePath(ts.getDirectoryPath(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)); const jsonSourceFile = ts.parseJsonText(configFileName, this.inputFiles.get(configFileName));
compilationOptions = ts.parseJsonSourceFileConfigFileContent(jsonSourceFile, host, baseDir, compilationOptions, configFileName).options; compilationOptions = ts.parseJsonSourceFileConfigFileContent(jsonSourceFile, host, baseDir, compilationOptions, configFileName).options;
} }
@ -333,7 +340,10 @@ namespace FourSlash {
} }
for (const file of testData.files) { 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 = { this.formatCodeSettings = {

File diff suppressed because it is too large Load diff

View file

@ -117,11 +117,17 @@ namespace Harness.LanguageService {
} }
export abstract class LanguageServiceAdapterHost { export abstract class LanguageServiceAdapterHost {
public readonly sys = new fakes.System(new vfs.FileSystem(/*ignoreCase*/ true, { cwd: virtualFileSystemRoot }));
public typesRegistry: ts.Map<void> | undefined; public typesRegistry: ts.Map<void> | undefined;
protected virtualFileSystem: Utils.VirtualFileSystem = new Utils.VirtualFileSystem(virtualFileSystemRoot, /*useCaseSensitiveFilenames*/false); private scriptInfos: collections.SortedMap<string, ScriptInfo>;
constructor(protected cancellationToken = DefaultHostCancellationToken.instance, constructor(protected cancellationToken = DefaultHostCancellationToken.instance,
protected settings = ts.getDefaultCompilerOptions()) { 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 { public getNewLine(): string {
@ -130,38 +136,38 @@ namespace Harness.LanguageService {
public getFilenames(): string[] { public getFilenames(): string[] {
const fileNames: string[] = []; const fileNames: string[] = [];
for (const virtualEntry of this.virtualFileSystem.getAllFileEntries()) { this.scriptInfos.forEach(scriptInfo => {
const scriptInfo = virtualEntry.content;
if (scriptInfo.isRootFile) { if (scriptInfo.isRootFile) {
// only include root files here // 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. // 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); fileNames.push(scriptInfo.fileName);
} }
} });
return fileNames; return fileNames;
} }
public getScriptInfo(fileName: string): ScriptInfo { public getScriptInfo(fileName: string): ScriptInfo {
const fileEntry = this.virtualFileSystem.traversePath(fileName); return this.scriptInfos.get(vpath.resolve(this.vfs.cwd(), fileName));
return fileEntry && fileEntry.isFile() ? fileEntry.content : undefined;
} }
public addScript(fileName: string, content: string, isRootFile: boolean): void { 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) { public editScript(fileName: string, start: number, end: number, newText: string) {
const script = this.getScriptInfo(fileName); const script = this.getScriptInfo(fileName);
if (script !== undefined) { if (script) {
script.editContent(start, end, newText); script.editContent(start, end, newText);
this.vfs.mkdirpSync(vpath.dirname(fileName));
this.vfs.writeFileSync(fileName, script.content);
return; return;
} }
throw new Error("No script with name '" + fileName + "'"); 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*/ } public openFile(_fileName: string, _content?: string, _scriptKindName?: string): void { /*overridden*/ }
/** /**
@ -178,62 +184,60 @@ namespace Harness.LanguageService {
/// Native adapter /// Native adapter
class NativeLanguageServiceHost extends LanguageServiceAdapterHost implements ts.LanguageServiceHost, LanguageServiceAdapterHost { class NativeLanguageServiceHost extends LanguageServiceAdapterHost implements ts.LanguageServiceHost, LanguageServiceAdapterHost {
symlinks = ts.createMap<string>();
isKnownTypesPackageName(name: string): boolean { isKnownTypesPackageName(name: string): boolean {
return this.typesRegistry && this.typesRegistry.has(name); return this.typesRegistry && this.typesRegistry.has(name);
} }
installPackage = ts.notImplemented; installPackage = ts.notImplemented;
getCompilationSettings() { return this.settings; } getCompilationSettings() { return this.settings; }
getCancellationToken() { return this.cancellationToken; } getCancellationToken() { return this.cancellationToken; }
getDirectories(path: string): string[] { getDirectories(path: string): string[] {
const dir = this.virtualFileSystem.traversePath(path); return this.sys.getDirectories(path);
return dir && dir.isDirectory() ? dir.getDirectories().map(d => d.name) : [];
} }
getCurrentDirectory(): string { return virtualFileSystemRoot; } getCurrentDirectory(): string { return virtualFileSystemRoot; }
getDefaultLibFileName(): string { return Compiler.defaultLibFileName; } getDefaultLibFileName(): string { return Compiler.defaultLibFileName; }
getScriptFileNames(): string[] { getScriptFileNames(): string[] {
return this.getFilenames().filter(ts.isAnySupportedFileExtension); return this.getFilenames().filter(ts.isAnySupportedFileExtension);
} }
getScriptSnapshot(fileName: string): ts.IScriptSnapshot { getScriptSnapshot(fileName: string): ts.IScriptSnapshot {
const script = this.getScriptInfo(fileName); const script = this.getScriptInfo(fileName);
return script ? new ScriptSnapshot(script) : undefined; return script ? new ScriptSnapshot(script) : undefined;
} }
getScriptKind(): ts.ScriptKind { return ts.ScriptKind.Unknown; } getScriptKind(): ts.ScriptKind { return ts.ScriptKind.Unknown; }
getScriptVersion(fileName: string): string { getScriptVersion(fileName: string): string {
const script = this.getScriptInfo(fileName); const script = this.getScriptInfo(fileName);
return script ? script.version.toString() : undefined; return script ? script.version.toString() : undefined;
} }
directoryExists(dirName: string): boolean { directoryExists(dirName: string): boolean {
if (ts.forEachEntry(this.symlinks, (_, key) => ts.forSomeAncestorDirectory(key, ancestor => ancestor === dirName))) { return this.sys.directoryExists(dirName);
return true;
}
const fileEntry = this.virtualFileSystem.traversePath(dirName);
return fileEntry && fileEntry.isDirectory();
} }
fileExists(fileName: string): boolean { fileExists(fileName: string): boolean {
return this.symlinks.has(fileName) || this.getScriptSnapshot(fileName) !== undefined; return this.sys.fileExists(fileName);
} }
readDirectory(path: string, extensions?: ReadonlyArray<string>, exclude?: ReadonlyArray<string>, include?: ReadonlyArray<string>, depth?: number): string[] { readDirectory(path: string, extensions?: ReadonlyArray<string>, exclude?: ReadonlyArray<string>, include?: ReadonlyArray<string>, depth?: number): string[] {
return ts.matchFiles(path, extensions, exclude, include, return this.sys.readDirectory(path, extensions, exclude, include, depth);
/*useCaseSensitiveFileNames*/ false,
this.getCurrentDirectory(),
depth,
(p) => this.virtualFileSystem.getAccessibleFileSystemEntries(p));
} }
readFile(path: string): string | undefined { readFile(path: string): string | undefined {
const target = this.symlinks.get(path); return this.sys.readFile(path);
return target !== undefined ? this.readFile(target) : ts.getSnapshotText(this.getScriptSnapshot(path));
} }
addSymlink(from: string, target: string) { this.symlinks.set(from, target); }
realpath(path: string): string { realpath(path: string): string {
const target = this.symlinks.get(path); return this.sys.realpath(path);
return target === undefined ? path : target;
} }
getTypeRootsVersion() { getTypeRootsVersion() {
return 0; return 0;
} }
@ -262,8 +266,6 @@ namespace Harness.LanguageService {
public getModuleResolutionsForFile: (fileName: string) => string; public getModuleResolutionsForFile: (fileName: string) => string;
public getTypeReferenceDirectiveResolutionsForFile: (fileName: string) => string; public getTypeReferenceDirectiveResolutionsForFile: (fileName: string) => string;
addSymlink() { return ts.notImplemented(); }
constructor(preprocessToResolve: boolean, cancellationToken?: ts.HostCancellationToken, options?: ts.CompilerOptions) { constructor(preprocessToResolve: boolean, cancellationToken?: ts.HostCancellationToken, options?: ts.CompilerOptions) {
super(cancellationToken, options); super(cancellationToken, options);
this.nativeHost = new NativeLanguageServiceHost(cancellationToken, options); this.nativeHost = new NativeLanguageServiceHost(cancellationToken, options);

View file

@ -110,7 +110,7 @@ namespace Playback {
return run; return run;
} }
export interface PlaybackIO extends Harness.Io, PlaybackControl { } export interface PlaybackIO extends Harness.IO, PlaybackControl { }
export interface PlaybackSystem extends ts.System, 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) { for (const file of log.filesAppended) {
if (file.contentsPath) { if (file.contentsPath) {
file.contents = host.readFile(ts.combinePaths(baseName, 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: PlaybackSystem, underlying: ts.System): void;
function initWrapper(wrapper: PlaybackIO, underlying: Harness.Io): void; function initWrapper(wrapper: PlaybackIO, underlying: Harness.IO): void;
function initWrapper(wrapper: PlaybackSystem | PlaybackIO, underlying: ts.System | Harness.Io): void { function initWrapper(wrapper: PlaybackSystem | PlaybackIO, underlying: ts.System | Harness.IO): void {
ts.forEach(Object.keys(underlying), prop => { ts.forEach(Object.keys(underlying), prop => {
(<any>wrapper)[prop] = (<any>underlying)[prop]; (<any>wrapper)[prop] = (<any>underlying)[prop];
}); });
@ -427,7 +427,7 @@ namespace Playback {
// console.log("Swallowed write operation during replay: " + name); // 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 = <any>{}; const wrapper: PlaybackIO = <any>{};
initWrapper(wrapper, underlying); initWrapper(wrapper, underlying);

View file

@ -28,12 +28,14 @@ namespace Harness.Parallel.Worker {
(global as any).describe = ((name, callback) => { (global as any).describe = ((name, callback) => {
testList.push({ name, callback, kind: "suite" }); testList.push({ name, callback, kind: "suite" });
}) as Mocha.IContextDefinition; }) as Mocha.IContextDefinition;
(global as any).describe.skip = ts.noop;
(global as any).it = ((name, callback) => { (global as any).it = ((name, callback) => {
if (!testList) { if (!testList) {
throw new Error("Tests must occur within a describe block"); throw new Error("Tests must occur within a describe block");
} }
testList.push({ name, callback, kind: "test" }); testList.push({ name, callback, kind: "test" });
}) as Mocha.ITestDefinition; }) as Mocha.ITestDefinition;
(global as any).it.skip = ts.noop;
} }
function setTimeoutAndExecute(timeout: number | undefined, f: () => void) { function setTimeoutAndExecute(timeout: number | undefined, f: () => void) {

View file

@ -1,8 +1,14 @@
///<reference path="harness.ts" /> /// <reference path="harness.ts" />
///<reference path="runnerbase.ts" /> /// <reference path="runnerbase.ts" />
/// <reference path="./vpath.ts" />
/// <reference path="./vfs.ts" />
/// <reference path="./documents.ts" />
/// <reference path="./compiler.ts" />
/// <reference path="./fakes.ts" />
// Test case is json of below type in tests/cases/project/ namespace project {
interface ProjectRunnerTestCase { // Test case is json of below type in tests/cases/project/
interface ProjectRunnerTestCase {
scenario: string; scenario: string;
projectRoot: string; // project where it lives - this also is the current directory when compiling projectRoot: string; // project where it lives - this also is the current directory when compiling
inputFiles: ReadonlyArray<string>; // list of input files to be given to program inputFiles: ReadonlyArray<string>; // list of input files to be given to program
@ -11,33 +17,28 @@ interface ProjectRunnerTestCase {
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 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 runTest?: boolean; // Run the resulting test
bug?: string; // If there is any bug associated with this test case bug?: string; // If there is any bug associated with this test case
} }
interface ProjectRunnerTestCaseResolutionInfo extends ProjectRunnerTestCase { interface ProjectRunnerTestCaseResolutionInfo extends ProjectRunnerTestCase {
// Apart from actual test case the results of the resolution // Apart from actual test case the results of the resolution
resolvedInputFiles: ReadonlyArray<string>; // List of files that were asked to read by compiler resolvedInputFiles: ReadonlyArray<string>; // List of files that were asked to read by compiler
emittedFiles: ReadonlyArray<string>; // List of files that were emitted by the compiler emittedFiles: ReadonlyArray<string>; // List of files that were emitted by the compiler
} }
interface BatchCompileProjectTestCaseEmittedFile extends Harness.Compiler.GeneratedFile { interface CompileProjectFilesResult {
emittedFileName: string;
}
interface CompileProjectFilesResult {
configFileSourceFiles: ReadonlyArray<ts.SourceFile>; configFileSourceFiles: ReadonlyArray<ts.SourceFile>;
moduleKind: ts.ModuleKind; moduleKind: ts.ModuleKind;
program?: ts.Program; program?: ts.Program;
compilerOptions?: ts.CompilerOptions; compilerOptions?: ts.CompilerOptions;
errors: ReadonlyArray<ts.Diagnostic>; errors: ReadonlyArray<ts.Diagnostic>;
sourceMapData?: ReadonlyArray<ts.SourceMapData>; sourceMapData?: ReadonlyArray<ts.SourceMapData>;
} }
interface BatchCompileProjectTestCaseResult extends CompileProjectFilesResult { interface BatchCompileProjectTestCaseResult extends CompileProjectFilesResult {
outputFiles?: BatchCompileProjectTestCaseEmittedFile[]; outputFiles?: ReadonlyArray<documents.TextDocument>;
} }
class ProjectRunner extends RunnerBase {
export class ProjectRunner extends RunnerBase {
public enumerateTestFiles() { public enumerateTestFiles() {
return this.enumerateFiles("tests/cases/project", /\.json$/, { recursive: true }); return this.enumerateFiles("tests/cases/project", /\.json$/, { recursive: true });
} }
@ -47,18 +48,134 @@ class ProjectRunner extends RunnerBase {
} }
public initializeTests() { public initializeTests() {
if (this.tests.length === 0) { describe("projects tests", () => {
const testFiles = this.enumerateTestFiles(); const tests = this.tests.length === 0 ? this.enumerateTestFiles() : this.tests;
testFiles.forEach(fn => { for (const test of tests) {
this.runProjectTestCase(fn); this.runProjectTestCase(test);
}
}); });
} }
else {
this.tests.forEach(test => this.runProjectTestCase(test));
}
}
private runProjectTestCase(testCaseFileName: string) { 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; });
});
}
}
}
class ProjectCompilerHost extends fakes.CompilerHost {
private _testCase: ProjectRunnerTestCase & ts.CompilerOptions;
private _projectParseConfigHost: ProjectParseConfigHost;
constructor(sys: fakes.System | vfs.FileSystem, compilerOptions: ts.CompilerOptions, _testCaseJustName: string, testCase: ProjectRunnerTestCase & ts.CompilerOptions, _moduleKind: ts.ModuleKind) {
super(sys, compilerOptions);
this._testCase = testCase;
}
public get parseConfigHost(): fakes.ParseConfigHost {
return this._projectParseConfigHost || (this._projectParseConfigHost = new ProjectParseConfigHost(this.sys, this._testCase));
}
public getDefaultLibFileName(_options: ts.CompilerOptions) {
return vpath.resolve(this.getDefaultLibLocation(), "lib.es5.d.ts");
}
}
class ProjectParseConfigHost extends fakes.ParseConfigHost {
private _testCase: ProjectRunnerTestCase & ts.CompilerOptions;
constructor(sys: fakes.System, testCase: ProjectRunnerTestCase & ts.CompilerOptions) {
super(sys);
this._testCase = testCase;
}
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
));
}
}
interface ProjectTestConfiguration {
name: string;
payload: ProjectTestPayload;
}
interface ProjectTestPayload {
testCase: ProjectRunnerTestCase & ts.CompilerOptions;
moduleKind: ts.ModuleKind;
vfs: vfs.FileSystem;
}
class ProjectTestCase {
private testCase: ProjectRunnerTestCase & ts.CompilerOptions;
private testCaseJustName: string;
private sys: fakes.System;
private compilerOptions: ts.CompilerOptions;
private compilerResult: BatchCompileProjectTestCaseResult;
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;
let inputFiles = testCase.inputFiles;
if (this.compilerOptions.project) {
// Parse project
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("", path => this.sys.fileExists(path));
}
let errors: ts.Diagnostic[];
const configFileSourceFiles: ts.SourceFile[] = [];
if (configFileName) {
const result = ts.readJsonConfigFile(configFileName, path => this.sys.readFile(path));
configFileSourceFiles.push(result);
const configParseHost = new ProjectParseConfigHost(this.sys, this.testCase);
const configParseResult = ts.parseJsonSourceFileConfigFileContent(result, configParseHost, ts.getDirectoryPath(configFileName), this.compilerOptions);
inputFiles = configParseResult.fileNames;
this.compilerOptions = configParseResult.options;
errors = result.parseDiagnostics.concat(configParseResult.errors);
}
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: this.compilerOptions,
sourceMapData: projectCompilerResult.sourceMapData,
outputFiles: compilerHost.outputs,
errors: errors ? ts.concatenate(errors, projectCompilerResult.errors) : projectCompilerResult.errors,
};
}
private get vfs() {
return this.sys.vfs;
}
public static getConfigurations(testCaseFileName: string): ProjectTestConfiguration[] {
let testCase: ProjectRunnerTestCase & ts.CompilerOptions; let testCase: ProjectRunnerTestCase & ts.CompilerOptions;
let testFileText: string; let testFileText: string;
@ -75,33 +192,115 @@ class ProjectRunner extends RunnerBase {
catch (e) { catch (e) {
assert(false, "Testcase: " + testCaseFileName + " does not contain valid json format: " + e.message); assert(false, "Testcase: " + testCaseFileName + " does not contain valid json format: " + e.message);
} }
let testCaseJustName = testCaseFileName.replace(/^.*[\\\/]/, "").replace(/\.json/, "");
function moduleNameToString(moduleKind: ts.ModuleKind) { const fs = vfs.createFromFileSystem(Harness.IO, /*ignoreCase*/ false);
return moduleKind === ts.ModuleKind.AMD fs.mountSync(vpath.resolve(Harness.IO.getWorkspaceRoot(), "tests"), vpath.combine(vfs.srcFolder, "tests"), vfs.createResolver(Harness.IO));
? "amd" fs.mkdirpSync(vpath.combine(vfs.srcFolder, testCase.projectRoot));
: moduleKind === ts.ModuleKind.CommonJS fs.chdir(vpath.combine(vfs.srcFolder, testCase.projectRoot));
? "node" fs.makeReadonly();
: "none";
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);
});
}
}
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);
}
}
if (errs.length) {
throw Error(errs.join("\n "));
}
}
}
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);
});
}
}
} }
// Project baselines verified go in project/testCaseName/moduleKind/ // Project baselines verified go in project/testCaseName/moduleKind/
function getBaselineFolder(moduleKind: ts.ModuleKind) { private getBaselineFolder(moduleKind: ts.ModuleKind) {
return "project/" + testCaseJustName + "/" + moduleNameToString(moduleKind) + "/"; return "project/" + this.testCaseJustName + "/" + moduleNameToString(moduleKind) + "/";
} }
// When test case output goes to tests/baselines/local/projectOutput/testCaseName/moduleKind/ private cleanProjectUrl(url: string) {
// We have these two separate locations because when comparing baselines the baseline verifier will delete the existing file let diskProjectPath = ts.normalizeSlashes(Harness.IO.resolvePath(this.testCase.projectRoot));
// 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);
}
function cleanProjectUrl(url: string) {
let diskProjectPath = ts.normalizeSlashes(Harness.IO.resolvePath(testCase.projectRoot));
let projectRootUrl = "file:///" + diskProjectPath; let projectRootUrl = "file:///" + diskProjectPath;
const normalizedProjectRoot = ts.normalizeSlashes(testCase.projectRoot); const normalizedProjectRoot = ts.normalizeSlashes(this.testCase.projectRoot);
diskProjectPath = diskProjectPath.substr(0, diskProjectPath.lastIndexOf(normalizedProjectRoot)); diskProjectPath = diskProjectPath.substr(0, diskProjectPath.lastIndexOf(normalizedProjectRoot));
projectRootUrl = projectRootUrl.substr(0, projectRootUrl.lastIndexOf(normalizedProjectRoot)); projectRootUrl = projectRootUrl.substr(0, projectRootUrl.lastIndexOf(normalizedProjectRoot));
if (url && url.length) { if (url && url.length) {
@ -121,17 +320,12 @@ class ProjectRunner extends RunnerBase {
return url; return url;
} }
function getCurrentDirectory() { private compileProjectFiles(moduleKind: ts.ModuleKind, configFileSourceFiles: ReadonlyArray<ts.SourceFile>,
return Harness.IO.resolvePath(testCase.projectRoot);
}
function compileProjectFiles(moduleKind: ts.ModuleKind, configFileSourceFiles: ReadonlyArray<ts.SourceFile>,
getInputFiles: () => ReadonlyArray<string>, getInputFiles: () => ReadonlyArray<string>,
getSourceFileTextImpl: (fileName: string) => string, compilerHost: ts.CompilerHost,
writeFile: (fileName: string, data: string, writeByteOrderMark: boolean) => void,
compilerOptions: ts.CompilerOptions): CompileProjectFilesResult { compilerOptions: ts.CompilerOptions): CompileProjectFilesResult {
const program = ts.createProgram(getInputFiles(), compilerOptions, createCompilerHost()); const program = ts.createProgram(getInputFiles(), compilerOptions, compilerHost);
const errors = ts.getPreEmitDiagnostics(program); const errors = ts.getPreEmitDiagnostics(program);
const emitResult = program.emit(); const emitResult = program.emit();
@ -142,10 +336,10 @@ class ProjectRunner extends RunnerBase {
if (sourceMapData) { if (sourceMapData) {
for (const data of sourceMapData) { for (const data of sourceMapData) {
for (let j = 0; j < data.sourceMapSources.length; j++) { for (let j = 0; j < data.sourceMapSources.length; j++) {
data.sourceMapSources[j] = cleanProjectUrl(data.sourceMapSources[j]); data.sourceMapSources[j] = this.cleanProjectUrl(data.sourceMapSources[j]);
} }
data.jsSourceMappingURL = cleanProjectUrl(data.jsSourceMappingURL); data.jsSourceMappingURL = this.cleanProjectUrl(data.jsSourceMappingURL);
data.sourceMapSourceRoot = cleanProjectUrl(data.sourceMapSourceRoot); data.sourceMapSourceRoot = this.cleanProjectUrl(data.sourceMapSourceRoot);
} }
} }
@ -156,218 +350,22 @@ class ProjectRunner extends RunnerBase {
errors, errors,
sourceMapData sourceMapData
}; };
function getSourceFileText(fileName: string): string {
const text = getSourceFileTextImpl(fileName);
return text !== undefined ? text : getSourceFileTextImpl(ts.getNormalizedAbsolutePath(fileName, getCurrentDirectory()));
} }
function getSourceFile(fileName: string, languageVersion: ts.ScriptTarget): ts.SourceFile { private compileDeclarations(compilerResult: BatchCompileProjectTestCaseResult) {
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);
}
}
return sourceFile;
}
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)
};
}
}
function batchCompilerProjectTestCase(moduleKind: ts.ModuleKind): BatchCompileProjectTestCaseResult {
let nonSubfolderDiskFiles = 0;
const outputFiles: BatchCompileProjectTestCaseEmittedFile[] = [];
let inputFiles = testCase.inputFiles;
let compilerOptions = createCompilerOptions();
const configFileSourceFiles: ts.SourceFile[] = [];
let configFileName: string;
if (compilerOptions.project) {
// Parse project
configFileName = ts.normalizePath(ts.combinePaths(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);
}
let errors: ts.Diagnostic[];
if (configFileName) {
const result = ts.readJsonConfigFile(configFileName, getSourceFileText);
configFileSourceFiles.push(result);
const configParseHost: ts.ParseConfigHost = {
useCaseSensitiveFileNames: Harness.IO.useCaseSensitiveFileNames(),
fileExists,
readDirectory,
readFile
};
const configParseResult = ts.parseJsonSourceFileConfigFileContent(result, configParseHost, ts.getDirectoryPath(configFileName), compilerOptions);
inputFiles = configParseResult.fileNames;
compilerOptions = configParseResult.options;
errors = result.parseDiagnostics.concat(configParseResult.errors);
}
const projectCompilerResult = compileProjectFiles(moduleKind, configFileSourceFiles, () => inputFiles, getSourceFileText, writeFile, compilerOptions);
return {
configFileSourceFiles,
moduleKind,
program: projectCompilerResult.program,
compilerOptions,
sourceMapData: projectCompilerResult.sourceMapData,
outputFiles,
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 = <any>testCase[name];
if (!ts.isString(optType)) {
const key = value.toLowerCase();
const optTypeValue = optType.get(key);
if (optTypeValue) {
value = optTypeValue;
}
}
compilerOptions[option.name] = value;
}
}
}
return compilerOptions;
}
function getFileNameInTheProjectTest(fileName: string): string {
return ts.isRootedDiskPath(fileName)
? fileName
: ts.normalizeSlashes(testCase.projectRoot) + "/" + ts.normalizeSlashes(fileName);
}
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;
}
function fileExists(fileName: string): boolean {
return Harness.IO.fileExists(getFileNameInTheProjectTest(fileName));
}
function readFile(fileName: string): string | undefined {
return Harness.IO.readFile(getFileNameInTheProjectTest(fileName));
}
function getSourceFileText(fileName: string): string {
let text: string;
try {
text = Harness.IO.readFile(getFileNameInTheProjectTest(fileName));
}
catch (e) {
// text doesn't get defined.
}
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++;
}
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));
}
}
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]);
}
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);
}
}
}
ensureDirectoryStructure(ts.getDirectoryPath(ts.normalizePath(outputFilePath)));
Harness.IO.writeFile(outputFilePath, data);
outputFiles.push({ emittedFileName: fileName, code: data, fileName: diskRelativeName, writeByteOrderMark });
}
}
function compileCompileDTsFiles(compilerResult: BatchCompileProjectTestCaseResult) {
const allInputFiles: { emittedFileName: string; code: string; }[] = [];
if (!compilerResult.program) { if (!compilerResult.program) {
return; return;
} }
const compilerOptions = compilerResult.program.getCompilerOptions();
const compilerOptions = compilerResult.program.getCompilerOptions();
const allInputFiles: documents.TextDocument[] = [];
const rootFiles: string[] = [];
ts.forEach(compilerResult.program.getSourceFiles(), sourceFile => { ts.forEach(compilerResult.program.getSourceFiles(), sourceFile => {
if (sourceFile.isDeclarationFile) { 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)) { else if (!(compilerOptions.outFile || compilerOptions.out)) {
let emitOutputFilePathWithoutExtension: string; let emitOutputFilePathWithoutExtension: string;
@ -384,6 +382,7 @@ class ProjectRunner extends RunnerBase {
const file = findOutputDtsFile(outputDtsFileName); const file = findOutputDtsFile(outputDtsFileName);
if (file) { if (file) {
allInputFiles.unshift(file); allInputFiles.unshift(file);
rootFiles.unshift(file.meta.get("fileName") || file.file);
} }
} }
else { else {
@ -391,31 +390,32 @@ class ProjectRunner extends RunnerBase {
const outputDtsFile = findOutputDtsFile(outputDtsFileName); const outputDtsFile = findOutputDtsFile(outputDtsFileName);
if (!ts.contains(allInputFiles, outputDtsFile)) { if (!ts.contains(allInputFiles, outputDtsFile)) {
allInputFiles.unshift(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 // 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) { function findOutputDtsFile(fileName: string) {
return ts.forEach(compilerResult.outputFiles, outputFile => outputFile.emittedFileName === fileName ? outputFile : undefined); return ts.forEach(compilerResult.outputFiles, outputFile => outputFile.meta.get("fileName") === 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) { function moduleNameToString(moduleKind: ts.ModuleKind) {
return inputFile.code; return moduleKind === ts.ModuleKind.AMD
} ? "amd"
} : moduleKind === ts.ModuleKind.CommonJS
return undefined; ? "node"
} : "none";
} }
function getErrorsBaseline(compilerResult: CompileProjectFilesResult) { function getErrorsBaseline(compilerResult: CompileProjectFilesResult) {
@ -438,106 +438,42 @@ class ProjectRunner extends RunnerBase {
return Harness.Compiler.getErrorBaseline(inputFiles, compilerResult.errors); return Harness.Compiler.getErrorBaseline(inputFiles, compilerResult.errors);
} }
const name = "Compiling project for " + testCase.scenario + ": testcase " + testCaseFileName; 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,
describe("projects tests", () => { sourceRoot: testCase.resolveSourceRoot && testCase.sourceRoot
describe(name, () => { ? vpath.resolve(vfs.srcFolder, testCase.sourceRoot)
function verifyCompilerResults(moduleKind: ts.ModuleKind) { : testCase.sourceRoot
let compilerResult: BatchCompileProjectTestCaseResult; };
function getCompilerResolutionInfo() { // Set the values specified using json
const resolutionInfo: ProjectRunnerTestCaseResolutionInfo & ts.CompilerOptions = JSON.parse(JSON.stringify(testCase)); const optionNameMap = ts.arrayToMap(ts.optionDeclarations, option => option.name);
resolutionInfo.resolvedInputFiles = ts.map(compilerResult.program.getSourceFiles(), inputFile => { for (const name in testCase) {
return ts.convertToRelativePath(inputFile.fileName, getCurrentDirectory(), path => Harness.Compiler.getCanonicalFileName(path)); if (name !== "mapRoot" && name !== "sourceRoot") {
}); const option = optionNameMap.get(name);
resolutionInfo.emittedFiles = ts.map(compilerResult.outputFiles, outputFile => { if (option) {
return ts.convertToRelativePath(outputFile.emittedFileName, getCurrentDirectory(), path => Harness.Compiler.getCanonicalFileName(path)); const optType = option.type;
}); let value = <any>testCase[name];
return resolutionInfo; if (!ts.isString(optType)) {
} const key = value.toLowerCase();
const optTypeValue = optType.get(key);
it(name + ": " + moduleNameToString(moduleKind), () => { if (optTypeValue) {
// Compile using node value = optTypeValue;
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);
});
}
});
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 "));
} }
} }
}); compilerOptions[option.name] = value;
// 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);
});
} }
} }
});
after(() => {
compilerResult = undefined;
});
} }
verifyCompilerResults(ts.ModuleKind.CommonJS); return compilerOptions;
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;
});
});
});
} }
} }

View file

@ -55,7 +55,7 @@ function createRunner(kind: TestRunnerKind): RunnerBase {
case "fourslash-server": case "fourslash-server":
return new FourSlashRunner(FourSlashTestType.Server); return new FourSlashRunner(FourSlashTestType.Server);
case "project": case "project":
return new ProjectRunner(); return new project.ProjectRunner();
case "rwc": case "rwc":
return new RWCRunner(); return new RWCRunner();
case "test262": case "test262":
@ -68,10 +68,6 @@ function createRunner(kind: TestRunnerKind): RunnerBase {
ts.Debug.fail(`Unknown runner kind ${kind}`); 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 // 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"; const mytestconfigFileName = "mytest.config";
@ -161,13 +157,13 @@ function handleTestConfig() {
case "compiler": case "compiler":
runners.push(new CompilerBaselineRunner(CompilerTestType.Conformance)); runners.push(new CompilerBaselineRunner(CompilerTestType.Conformance));
runners.push(new CompilerBaselineRunner(CompilerTestType.Regressions)); runners.push(new CompilerBaselineRunner(CompilerTestType.Regressions));
runners.push(new ProjectRunner()); runners.push(new project.ProjectRunner());
break; break;
case "conformance": case "conformance":
runners.push(new CompilerBaselineRunner(CompilerTestType.Conformance)); runners.push(new CompilerBaselineRunner(CompilerTestType.Conformance));
break; break;
case "project": case "project":
runners.push(new ProjectRunner()); runners.push(new project.ProjectRunner());
break; break;
case "fourslash": case "fourslash":
runners.push(new FourSlashRunner(FourSlashTestType.Native)); runners.push(new FourSlashRunner(FourSlashTestType.Native));
@ -208,7 +204,7 @@ function handleTestConfig() {
// TODO: project tests don"t work in the browser yet // TODO: project tests don"t work in the browser yet
if (Utils.getExecutionEnvironment() !== Utils.ExecutionEnvironment.Browser) { if (Utils.getExecutionEnvironment() !== Utils.ExecutionEnvironment.Browser) {
runners.push(new ProjectRunner()); runners.push(new project.ProjectRunner());
} }
// language services // language services

View file

@ -6,7 +6,7 @@
/* tslint:disable:no-null-keyword */ /* tslint:disable:no-null-keyword */
namespace RWC { 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 oldIO = Harness.IO;
const wrappedIO = Playback.wrapIO(oldIO); const wrappedIO = Playback.wrapIO(oldIO);
@ -30,7 +30,7 @@ namespace RWC {
let inputFiles: Harness.Compiler.TestFile[] = []; let inputFiles: Harness.Compiler.TestFile[] = [];
let otherFiles: Harness.Compiler.TestFile[] = []; let otherFiles: Harness.Compiler.TestFile[] = [];
let tsconfigFiles: Harness.Compiler.TestFile[] = []; let tsconfigFiles: Harness.Compiler.TestFile[] = [];
let compilerResult: Harness.Compiler.CompilerResult; let compilerResult: compiler.CompilationResult;
let compilerOptions: ts.CompilerOptions; let compilerOptions: ts.CompilerOptions;
const baselineOpts: Harness.Baseline.BaselineOptions = { const baselineOpts: Harness.Baseline.BaselineOptions = {
Subfolder: "rwc", Subfolder: "rwc",
@ -142,8 +142,7 @@ namespace RWC {
opts.options.noLib = true; opts.options.noLib = true;
// Emit the results // Emit the results
compilerOptions = undefined; compilerResult = Harness.Compiler.compileFiles(
const output = Harness.Compiler.compileFiles(
inputFiles, inputFiles,
otherFiles, otherFiles,
/* harnessOptions */ undefined, /* harnessOptions */ undefined,
@ -151,9 +150,7 @@ namespace RWC {
// Since each RWC json file specifies its current directory in its json file, we need // 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. // to pass this information in explicitly instead of acquiring it from the process.
currentDirectory); currentDirectory);
compilerOptions = compilerResult.options;
compilerOptions = output.options;
compilerResult = output.result;
}); });
function getHarnessCompilerInputUnit(fileName: string): Harness.Compiler.TestFile { function getHarnessCompilerInputUnit(fileName: string): Harness.Compiler.TestFile {
@ -173,38 +170,38 @@ namespace RWC {
it("has the expected emitted code", function(this: Mocha.ITestCallbackContext) { it("has the expected emitted code", function(this: Mocha.ITestCallbackContext) {
this.timeout(100_000); // Allow longer timeouts for RWC js verification this.timeout(100_000); // Allow longer timeouts for RWC js verification
Harness.Baseline.runMultifileBaseline(baseName, "", () => { Harness.Baseline.runMultifileBaseline(baseName, "", () => {
return Harness.Compiler.iterateOutputs(compilerResult.files); return Harness.Compiler.iterateOutputs(compilerResult.js.values());
}, baselineOpts, [".js", ".jsx"]); }, baselineOpts, [".js", ".jsx"]);
}); });
it("has the expected declaration file content", () => { it("has the expected declaration file content", () => {
Harness.Baseline.runMultifileBaseline(baseName, "", () => { Harness.Baseline.runMultifileBaseline(baseName, "", () => {
if (!compilerResult.declFilesCode.length) { if (!compilerResult.dts.size) {
return null; return null;
} }
return Harness.Compiler.iterateOutputs(compilerResult.declFilesCode); return Harness.Compiler.iterateOutputs(compilerResult.dts.values());
}, baselineOpts, [".d.ts"]); }, baselineOpts, [".d.ts"]);
}); });
it("has the expected source maps", () => { it("has the expected source maps", () => {
Harness.Baseline.runMultifileBaseline(baseName, "", () => { Harness.Baseline.runMultifileBaseline(baseName, "", () => {
if (!compilerResult.sourceMaps.length) { if (!compilerResult.maps.size) {
return null; return null;
} }
return Harness.Compiler.iterateOutputs(compilerResult.sourceMaps); return Harness.Compiler.iterateOutputs(compilerResult.maps.values());
}, baselineOpts, [".map"]); }, baselineOpts, [".map"]);
}); });
it("has the expected errors", () => { it("has the expected errors", () => {
Harness.Baseline.runMultifileBaseline(baseName, ".errors.txt", () => { Harness.Baseline.runMultifileBaseline(baseName, ".errors.txt", () => {
if (compilerResult.errors.length === 0) { if (compilerResult.diagnostics.length === 0) {
return null; return null;
} }
// Do not include the library in the baselines to avoid noise // Do not include the library in the baselines to avoid noise
const baselineFiles = tsconfigFiles.concat(inputFiles, otherFiles).filter(f => !Harness.isDefaultLibraryFile(f.unitName)); 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); return Harness.Compiler.iterateErrorBaseline(baselineFiles, errors);
}, baselineOpts); }, baselineOpts);
}); });
@ -212,9 +209,9 @@ namespace RWC {
// Ideally, a generated declaration file will have no errors. But we allow generated // Ideally, a generated declaration file will have no errors. But we allow generated
// declaration file errors as part of the baseline. // declaration file errors as part of the baseline.
it("has the expected errors in generated declaration files", () => { 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", () => { Harness.Baseline.runMultifileBaseline(baseName, ".dts.errors.txt", () => {
if (compilerResult.errors.length === 0) { if (compilerResult.diagnostics.length === 0) {
return null; return null;
} }
@ -225,7 +222,7 @@ namespace RWC {
compilerResult = undefined; compilerResult = undefined;
const declFileCompilationResult = Harness.Compiler.compileDeclarationFiles(declContext); 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); }, baselineOpts);
} }
}); });

View file

@ -206,8 +206,8 @@ namespace Harness.SourceMapRecorder {
let sourceMapSources: string[]; let sourceMapSources: string[];
let sourceMapNames: string[]; let sourceMapNames: string[];
let jsFile: Compiler.GeneratedFile; let jsFile: documents.TextDocument;
let jsLineMap: number[]; let jsLineMap: ReadonlyArray<number>;
let tsCode: string; let tsCode: string;
let tsLineMap: number[]; let tsLineMap: number[];
@ -216,13 +216,13 @@ namespace Harness.SourceMapRecorder {
let prevWrittenJsLine: number; let prevWrittenJsLine: number;
let spanMarkerContinues: boolean; 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; sourceMapRecorder = sourceMapRecordWriter;
sourceMapSources = sourceMapData.sourceMapSources; sourceMapSources = sourceMapData.sourceMapSources;
sourceMapNames = sourceMapData.sourceMapNames; sourceMapNames = sourceMapData.sourceMapNames;
jsFile = currentJsFile; jsFile = currentJsFile;
jsLineMap = ts.computeLineStarts(jsFile.code); jsLineMap = jsFile.lineStarts;
spansOnSingleLine = []; spansOnSingleLine = [];
prevWrittenSourcePos = 0; prevWrittenSourcePos = 0;
@ -290,7 +290,7 @@ namespace Harness.SourceMapRecorder {
assert.isTrue(spansOnSingleLine.length === 1); assert.isTrue(spansOnSingleLine.length === 1);
sourceMapRecorder.WriteLine("-------------------------------------------------------------------"); sourceMapRecorder.WriteLine("-------------------------------------------------------------------");
sourceMapRecorder.WriteLine("emittedFile:" + jsFile.fileName); sourceMapRecorder.WriteLine("emittedFile:" + jsFile.file);
sourceMapRecorder.WriteLine("sourceFile:" + sourceMapSources[spansOnSingleLine[0].sourceMapSpan.sourceIndex]); sourceMapRecorder.WriteLine("sourceFile:" + sourceMapSources[spansOnSingleLine[0].sourceMapSpan.sourceIndex]);
sourceMapRecorder.WriteLine("-------------------------------------------------------------------"); sourceMapRecorder.WriteLine("-------------------------------------------------------------------");
@ -313,15 +313,16 @@ namespace Harness.SourceMapRecorder {
writeJsFileLines(jsLineMap.length); writeJsFileLines(jsLineMap.length);
} }
function getTextOfLine(line: number, lineMap: number[], code: string) { function getTextOfLine(line: number, lineMap: ReadonlyArray<number>, code: string) {
const startPos = lineMap[line]; const startPos = lineMap[line];
const endPos = lineMap[line + 1]; 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) { function writeJsFileLines(endJsLine: number) {
for (; prevWrittenJsLine < endJsLine; prevWrittenJsLine++) { 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 // Emit markers
iterateSpans(writeSourceMapMarker); iterateSpans(writeSourceMapMarker);
const jsFileText = getTextOfLine(currentJsLine, jsLineMap, jsFile.code); const jsFileText = getTextOfLine(currentJsLine, jsLineMap, jsFile.text);
if (prevEmittedCol < jsFileText.length) { if (prevEmittedCol < jsFileText.length) {
// There is remaining text on this line that will be part of next source span so write marker that continues // 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); 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<ts.SourceMapData>, program: ts.Program, jsFiles: ReadonlyArray<documents.TextDocument>, declarationFiles: ReadonlyArray<documents.TextDocument>) {
const sourceMapRecorder = new Compiler.WriterAggregator(); const sourceMapRecorder = new Compiler.WriterAggregator();
for (let i = 0; i < sourceMapDataList.length; i++) { for (let i = 0; i < sourceMapDataList.length; i++) {
const sourceMapData = sourceMapDataList[i]; const sourceMapData = sourceMapDataList[i];
let prevSourceFile: ts.SourceFile; let prevSourceFile: ts.SourceFile;
let currentFile: Compiler.GeneratedFile; let currentFile: documents.TextDocument;
if (ts.endsWith(sourceMapData.sourceMapFile, ts.Extension.Dts)) { if (ts.endsWith(sourceMapData.sourceMapFile, ts.Extension.Dts)) {
if (sourceMapDataList.length > jsFiles.length) { if (sourceMapDataList.length > jsFiles.length) {
currentFile = declarationFiles[Math.floor(i / 2)]; // When both kinds of source map are present, they alternate js/dts currentFile = declarationFiles[Math.floor(i / 2)]; // When both kinds of source map are present, they alternate js/dts

View file

@ -31,7 +31,7 @@ class Test262BaselineRunner extends RunnerBase {
// Everything declared here should be cleared out in the "after" callback. // Everything declared here should be cleared out in the "after" callback.
let testState: { let testState: {
filename: string; filename: string;
compilerResult: Harness.Compiler.CompilerResult; compilerResult: compiler.CompilationResult;
inputFiles: Harness.Compiler.TestFile[]; inputFiles: Harness.Compiler.TestFile[];
}; };
@ -52,14 +52,12 @@ class Test262BaselineRunner extends RunnerBase {
compilerResult: undefined, compilerResult: undefined,
}; };
const output = Harness.Compiler.compileFiles( testState.compilerResult = Harness.Compiler.compileFiles(
[Test262BaselineRunner.helperFile].concat(inputFiles), [Test262BaselineRunner.helperFile].concat(inputFiles),
/*otherFiles*/ [], /*otherFiles*/ [],
/* harnessOptions */ undefined, /* harnessOptions */ undefined,
Test262BaselineRunner.options, Test262BaselineRunner.options,
/* currentDirectory */ undefined /* currentDirectory */ undefined);
);
testState.compilerResult = output.result;
}); });
after(() => { after(() => {
@ -68,14 +66,14 @@ class Test262BaselineRunner extends RunnerBase {
it("has the expected emitted code", () => { it("has the expected emitted code", () => {
Harness.Baseline.runBaseline(testState.filename + ".output.js", () => { 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); return Harness.Compiler.collateOutputs(files);
}, Test262BaselineRunner.baselineOptions); }, Test262BaselineRunner.baselineOptions);
}); });
it("has the expected errors", () => { it("has the expected errors", () => {
Harness.Baseline.runBaseline(testState.filename + ".errors.txt", () => { Harness.Baseline.runBaseline(testState.filename + ".errors.txt", () => {
const errors = testState.compilerResult.errors; const errors = testState.compilerResult.diagnostics;
if (errors.length === 0) { if (errors.length === 0) {
return null; return null;
} }

View file

@ -135,12 +135,19 @@
"../server/session.ts", "../server/session.ts",
"../server/scriptVersionCache.ts", "../server/scriptVersionCache.ts",
"collections.ts",
"utils.ts",
"documents.ts",
"vpath.ts",
"vfs.ts",
"compiler.ts",
"fakes.ts",
"sourceMapRecorder.ts", "sourceMapRecorder.ts",
"runnerbase.ts", "runnerbase.ts",
"virtualFileSystem.ts",
"harness.ts", "harness.ts",
"virtualFileSystemWithWatch.ts",
"harnessLanguageService.ts", "harnessLanguageService.ts",
"virtualFileSystemWithWatch.ts",
"fourslashRunner.ts", "fourslashRunner.ts",
"fourslash.ts", "fourslash.ts",
"typeWriter.ts", "typeWriter.ts",

View file

@ -1,29 +1,34 @@
/// <reference path="..\harness.ts" /> /// <reference path="../harness.ts" />
/// <reference path="..\virtualFileSystem.ts" /> /// <reference path="../vfs.ts" />
/// <reference path="../compiler.ts" />
namespace ts { namespace ts {
const testContentsJson = createMapFromTemplate({ function createFileSystem(ignoreCase: boolean, cwd: string, root: string) {
"/dev/tsconfig.json": { return new vfs.FileSystem(ignoreCase, {
cwd,
files: {
[root]: {
"dev/tsconfig.json": JSON.stringify({
extends: "./configs/base", extends: "./configs/base",
files: [ files: [
"main.ts", "main.ts",
"supplemental.ts" "supplemental.ts"
] ]
}, }),
"/dev/tsconfig.nostrictnull.json": { "dev/tsconfig.nostrictnull.json": JSON.stringify({
extends: "./tsconfig", extends: "./tsconfig",
compilerOptions: { compilerOptions: {
strictNullChecks: false strictNullChecks: false
} }
}, }),
"/dev/configs/base.json": { "dev/configs/base.json": JSON.stringify({
compilerOptions: { compilerOptions: {
allowJs: true, allowJs: true,
noImplicitAny: true, noImplicitAny: true,
strictNullChecks: true strictNullChecks: true
} }
}, }),
"/dev/configs/tests.json": { "dev/configs/tests.json": JSON.stringify({
compilerOptions: { compilerOptions: {
preserveConstEnums: true, preserveConstEnums: true,
removeComments: false, removeComments: false,
@ -36,57 +41,57 @@ namespace ts {
include: [ include: [
"../tests/**/*.ts" "../tests/**/*.ts"
] ]
}, }),
"/dev/circular.json": { "dev/circular.json": JSON.stringify({
extends: "./circular2", extends: "./circular2",
compilerOptions: { compilerOptions: {
module: "amd" module: "amd"
} }
}, }),
"/dev/circular2.json": { "dev/circular2.json": JSON.stringify({
extends: "./circular", extends: "./circular",
compilerOptions: { compilerOptions: {
module: "commonjs" module: "commonjs"
} }
}, }),
"/dev/missing.json": { "dev/missing.json": JSON.stringify({
extends: "./missing2", extends: "./missing2",
compilerOptions: { compilerOptions: {
types: [] types: []
} }
}, }),
"/dev/failure.json": { "dev/failure.json": JSON.stringify({
extends: "./failure2.json", extends: "./failure2.json",
compilerOptions: { compilerOptions: {
typeRoots: [] typeRoots: []
} }
}, }),
"/dev/failure2.json": { "dev/failure2.json": JSON.stringify({
excludes: ["*.js"] excludes: ["*.js"]
}, }),
"/dev/configs/first.json": { "dev/configs/first.json": JSON.stringify({
extends: "./base", extends: "./base",
compilerOptions: { compilerOptions: {
module: "commonjs" module: "commonjs"
}, },
files: ["../main.ts"] files: ["../main.ts"]
}, }),
"/dev/configs/second.json": { "dev/configs/second.json": JSON.stringify({
extends: "./base", extends: "./base",
compilerOptions: { compilerOptions: {
module: "amd" module: "amd"
}, },
include: ["../supplemental.*"] include: ["../supplemental.*"]
}, }),
"/dev/configs/third.json": { "dev/configs/third.json": JSON.stringify({
extends: "./second", extends: "./second",
compilerOptions: { compilerOptions: {
// tslint:disable-next-line:no-null-keyword // tslint:disable-next-line:no-null-keyword
module: null module: null
}, },
include: ["../supplemental.*"] include: ["../supplemental.*"]
}, }),
"/dev/configs/fourth.json": { "dev/configs/fourth.json": JSON.stringify({
extends: "./third", extends: "./third",
compilerOptions: { compilerOptions: {
module: "system" module: "system"
@ -94,23 +99,25 @@ namespace ts {
// tslint:disable-next-line:no-null-keyword // tslint:disable-next-line:no-null-keyword
include: null, include: null,
files: ["../main.ts"] files: ["../main.ts"]
}, }),
"/dev/extends.json": { extends: 42 }, "dev/extends.json": JSON.stringify({ extends: 42 }),
"/dev/extends2.json": { extends: "configs/base" }, "dev/extends2.json": JSON.stringify({ extends: "configs/base" }),
"/dev/main.ts": "", "dev/main.ts": "",
"/dev/supplemental.ts": "", "dev/supplemental.ts": "",
"/dev/tests/unit/spec.ts": "", "dev/tests/unit/spec.ts": "",
"/dev/tests/utils.ts": "", "dev/tests/utils.ts": "",
"/dev/tests/scenarios/first.json": "", "dev/tests/scenarios/first.json": "",
"/dev/tests/baselines/first/output.ts": "" "dev/tests/baselines/first/output.ts": ""
}
}
}); });
const testContents = mapEntries(testContentsJson, (k, v) => [k, isString(v) ? v : JSON.stringify(v)]); }
const caseInsensitiveBasePath = "c:/dev/"; 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 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}[]) { 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)}.`); assert.isTrue(expected.length === actual.length, `Expected error: ${JSON.stringify(expected)}. Actual error: ${JSON.stringify(actual)}.`);
@ -124,7 +131,7 @@ namespace ts {
} }
describe("configurationExtension", () => { describe("configurationExtension", () => {
forEach<[string, string, Utils.MockParseConfigHost], void>([ forEach<[string, string, fakes.ParseConfigHost], void>([
["under a case insensitive host", caseInsensitiveBasePath, caseInsensitiveHost], ["under a case insensitive host", caseInsensitiveBasePath, caseInsensitiveHost],
["under a case sensitive host", caseSensitiveBasePath, caseSensitiveHost] ["under a case sensitive host", caseSensitiveBasePath, caseSensitiveHost]
], ([testName, basePath, host]) => { ], ([testName, basePath, host]) => {

View file

@ -1,5 +1,7 @@
/// <reference path="..\harness.ts" /> /// <reference path="..\harness.ts" />
/// <reference path="..\..\compiler\commandLineParser.ts" /> /// <reference path="..\..\compiler\commandLineParser.ts" />
/// <reference path="../compiler.ts" />
/// <reference path="../vfs.ts" />
namespace ts { namespace ts {
describe("convertCompilerOptionsFromJson", () => { describe("convertCompilerOptionsFromJson", () => {
@ -31,7 +33,7 @@ namespace ts {
const result = parseJsonText(configFileName, fileText); const result = parseJsonText(configFileName, fileText);
assert(!result.parseDiagnostics.length); assert(!result.parseDiagnostics.length);
assert(!!result.endOfFileToken); 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); const { options: actualCompilerOptions, errors: actualParseErrors } = parseJsonSourceFileConfigFileContent(result, host, "/apath/", /*existingOptions*/ undefined, configFileName);
expectedResult.compilerOptions.configFilePath = configFileName; expectedResult.compilerOptions.configFilePath = configFileName;

View file

@ -1,5 +1,7 @@
/// <reference path="..\harness.ts" /> /// <reference path="..\harness.ts" />
/// <reference path="..\..\compiler\commandLineParser.ts" /> /// <reference path="..\..\compiler\commandLineParser.ts" />
/// <reference path="../compiler.ts" />
/// <reference path="../vfs.ts" />
namespace ts { namespace ts {
interface ExpectedResult { typeAcquisition: TypeAcquisition; errors: Diagnostic[]; } interface ExpectedResult { typeAcquisition: TypeAcquisition; errors: Diagnostic[]; }
@ -43,7 +45,7 @@ namespace ts {
const result = parseJsonText(configFileName, fileText); const result = parseJsonText(configFileName, fileText);
assert(!result.parseDiagnostics.length); assert(!result.parseDiagnostics.length);
assert(!!result.endOfFileToken); 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); const { typeAcquisition: actualTypeAcquisition, errors: actualParseErrors } = parseJsonSourceFileConfigFileContent(result, host, "/apath/", /*existingOptions*/ undefined, configFileName);
verifyAcquisition(actualTypeAcquisition, expectedResult); verifyAcquisition(actualTypeAcquisition, expectedResult);

View file

@ -1,107 +1,108 @@
/// <reference path="..\harness.ts" /> /// <reference path="../harness.ts" />
/// <reference path="..\virtualFileSystem.ts" /> /// <reference path="../compiler.ts" />
/// <reference path="../vfs.ts" />
namespace ts { namespace ts {
const caseInsensitiveBasePath = "c:/dev/"; const caseInsensitiveBasePath = "c:/dev/";
const caseInsensitiveTsconfigPath = "c:/dev/tsconfig.json"; const caseInsensitiveTsconfigPath = "c:/dev/tsconfig.json";
const caseInsensitiveHost = new Utils.MockParseConfigHost(caseInsensitiveBasePath, /*useCaseSensitiveFileNames*/ false, [ const caseInsensitiveHost = new fakes.ParseConfigHost(new vfs.FileSystem(/*ignoreCase*/ true, { cwd: caseInsensitiveBasePath, files: {
"c:/dev/a.ts", "c:/dev/a.ts": "",
"c:/dev/a.d.ts", "c:/dev/a.d.ts": "",
"c:/dev/a.js", "c:/dev/a.js": "",
"c:/dev/b.ts", "c:/dev/b.ts": "",
"c:/dev/b.js", "c:/dev/b.js": "",
"c:/dev/c.d.ts", "c:/dev/c.d.ts": "",
"c:/dev/z/a.ts", "c:/dev/z/a.ts": "",
"c:/dev/z/abz.ts", "c:/dev/z/abz.ts": "",
"c:/dev/z/aba.ts", "c:/dev/z/aba.ts": "",
"c:/dev/z/b.ts", "c:/dev/z/b.ts": "",
"c:/dev/z/bbz.ts", "c:/dev/z/bbz.ts": "",
"c:/dev/z/bba.ts", "c:/dev/z/bba.ts": "",
"c:/dev/x/a.ts", "c:/dev/x/a.ts": "",
"c:/dev/x/aa.ts", "c:/dev/x/aa.ts": "",
"c:/dev/x/b.ts", "c:/dev/x/b.ts": "",
"c:/dev/x/y/a.ts", "c:/dev/x/y/a.ts": "",
"c:/dev/x/y/b.ts", "c:/dev/x/y/b.ts": "",
"c:/dev/js/a.js", "c:/dev/js/a.js": "",
"c:/dev/js/b.js", "c:/dev/js/b.js": "",
"c:/dev/js/d.min.js", "c:/dev/js/d.min.js": "",
"c:/dev/js/ab.min.js", "c:/dev/js/ab.min.js": "",
"c:/ext/ext.ts", "c:/ext/ext.ts": "",
"c:/ext/b/a..b.ts" "c:/ext/b/a..b.ts": "",
]); }}));
const caseSensitiveBasePath = "/dev/"; const caseSensitiveBasePath = "/dev/";
const caseSensitiveHost = new Utils.MockParseConfigHost(caseSensitiveBasePath, /*useCaseSensitiveFileNames*/ true, [ const caseSensitiveHost = new fakes.ParseConfigHost(new vfs.FileSystem(/*ignoreCase*/ false, { cwd: caseSensitiveBasePath, files: {
"/dev/a.ts", "/dev/a.ts": "",
"/dev/a.d.ts", "/dev/a.d.ts": "",
"/dev/a.js", "/dev/a.js": "",
"/dev/b.ts", "/dev/b.ts": "",
"/dev/b.js", "/dev/b.js": "",
"/dev/A.ts", "/dev/A.ts": "",
"/dev/B.ts", "/dev/B.ts": "",
"/dev/c.d.ts", "/dev/c.d.ts": "",
"/dev/z/a.ts", "/dev/z/a.ts": "",
"/dev/z/abz.ts", "/dev/z/abz.ts": "",
"/dev/z/aba.ts", "/dev/z/aba.ts": "",
"/dev/z/b.ts", "/dev/z/b.ts": "",
"/dev/z/bbz.ts", "/dev/z/bbz.ts": "",
"/dev/z/bba.ts", "/dev/z/bba.ts": "",
"/dev/x/a.ts", "/dev/x/a.ts": "",
"/dev/x/b.ts", "/dev/x/b.ts": "",
"/dev/x/y/a.ts", "/dev/x/y/a.ts": "",
"/dev/x/y/b.ts", "/dev/x/y/b.ts": "",
"/dev/q/a/c/b/d.ts", "/dev/q/a/c/b/d.ts": "",
"/dev/js/a.js", "/dev/js/a.js": "",
"/dev/js/b.js", "/dev/js/b.js": "",
]); }}));
const caseInsensitiveMixedExtensionHost = new Utils.MockParseConfigHost(caseInsensitiveBasePath, /*useCaseSensitiveFileNames*/ false, [ const caseInsensitiveMixedExtensionHost = new fakes.ParseConfigHost(new vfs.FileSystem(/*ignoreCase*/ true, { cwd: caseInsensitiveBasePath, files: {
"c:/dev/a.ts", "c:/dev/a.ts": "",
"c:/dev/a.d.ts", "c:/dev/a.d.ts": "",
"c:/dev/a.js", "c:/dev/a.js": "",
"c:/dev/b.tsx", "c:/dev/b.tsx": "",
"c:/dev/b.d.ts", "c:/dev/b.d.ts": "",
"c:/dev/b.jsx", "c:/dev/b.jsx": "",
"c:/dev/c.tsx", "c:/dev/c.tsx": "",
"c:/dev/c.js", "c:/dev/c.js": "",
"c:/dev/d.js", "c:/dev/d.js": "",
"c:/dev/e.jsx", "c:/dev/e.jsx": "",
"c:/dev/f.other" "c:/dev/f.other": "",
]); }}));
const caseInsensitiveCommonFoldersHost = new Utils.MockParseConfigHost(caseInsensitiveBasePath, /*useCaseSensitiveFileNames*/ false, [ const caseInsensitiveCommonFoldersHost = new fakes.ParseConfigHost(new vfs.FileSystem(/*ignoreCase*/ true, { cwd: caseInsensitiveBasePath, files: {
"c:/dev/a.ts", "c:/dev/a.ts": "",
"c:/dev/a.d.ts", "c:/dev/a.d.ts": "",
"c:/dev/a.js", "c:/dev/a.js": "",
"c:/dev/b.ts", "c:/dev/b.ts": "",
"c:/dev/x/a.ts", "c:/dev/x/a.ts": "",
"c:/dev/node_modules/a.ts", "c:/dev/node_modules/a.ts": "",
"c:/dev/bower_components/a.ts", "c:/dev/bower_components/a.ts": "",
"c:/dev/jspm_packages/a.ts" "c:/dev/jspm_packages/a.ts": "",
]); }}));
const caseInsensitiveDottedFoldersHost = new Utils.MockParseConfigHost(caseInsensitiveBasePath, /*useCaseSensitiveFileNames*/ false, [ const caseInsensitiveDottedFoldersHost = new fakes.ParseConfigHost(new vfs.FileSystem(/*ignoreCase*/ true, { cwd: caseInsensitiveBasePath, files: {
"c:/dev/x/d.ts", "c:/dev/x/d.ts": "",
"c:/dev/x/y/d.ts", "c:/dev/x/y/d.ts": "",
"c:/dev/x/y/.e.ts", "c:/dev/x/y/.e.ts": "",
"c:/dev/x/.y/a.ts", "c:/dev/x/.y/a.ts": "",
"c:/dev/.z/.b.ts", "c:/dev/.z/.b.ts": "",
"c:/dev/.z/c.ts", "c:/dev/.z/c.ts": "",
"c:/dev/w/.u/e.ts", "c:/dev/w/.u/e.ts": "",
"c:/dev/g.min.js/.g/g.ts" "c:/dev/g.min.js/.g/g.ts": "",
]); }}));
const caseInsensitiveOrderingDiffersWithCaseHost = new Utils.MockParseConfigHost(caseInsensitiveBasePath, /*useCaseSensitiveFileNames*/ false, [ const caseInsensitiveOrderingDiffersWithCaseHost = new fakes.ParseConfigHost(new vfs.FileSystem(/*ignoreCase*/ true, { cwd: caseInsensitiveBasePath, files: {
"c:/dev/xylophone.ts", "c:/dev/xylophone.ts": "",
"c:/dev/Yosemite.ts", "c:/dev/Yosemite.ts": "",
"c:/dev/zebra.ts", "c:/dev/zebra.ts": "",
]); }}));
const caseSensitiveOrderingDiffersWithCaseHost = new Utils.MockParseConfigHost(caseSensitiveBasePath, /*useCaseSensitiveFileNames*/ true, [ const caseSensitiveOrderingDiffersWithCaseHost = new fakes.ParseConfigHost(new vfs.FileSystem(/*ignoreCase*/ false, { cwd: caseSensitiveBasePath, files: {
"/dev/xylophone.ts", "/dev/xylophone.ts": "",
"/dev/Yosemite.ts", "/dev/Yosemite.ts": "",
"/dev/zebra.ts", "/dev/zebra.ts": "",
]); }}));
function assertParsed(actual: ParsedCommandLine, expected: ParsedCommandLine): void { function assertParsed(actual: ParsedCommandLine, expected: ParsedCommandLine): void {
assert.deepEqual(actual.fileNames, expected.fileNames); assert.deepEqual(actual.fileNames, expected.fileNames);

View file

@ -1,5 +1,5 @@
/// <reference path="..\..\..\src\harness\harness.ts" /> /// <reference path="..\..\..\src\harness\harness.ts" />
/// <reference path="..\..\..\src\harness\virtualFileSystem.ts" /> /// <reference path="..\..\..\src\harness\vfs.ts" />
namespace ts { namespace ts {

View file

@ -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:/");
});
});

View file

@ -1,4 +1,6 @@
/// <reference path="..\harness.ts" /> /// <reference path="..\harness.ts" />
/// <reference path="../documents.ts" />
/// <reference path="../vfs.ts" />
namespace ts { namespace ts {
function verifyMissingFilePaths(missingPaths: ReadonlyArray<Path>, expected: ReadonlyArray<string>) { function verifyMissingFilePaths(missingPaths: ReadonlyArray<Path>, expected: ReadonlyArray<string>) {
@ -22,33 +24,25 @@ namespace ts {
const emptyFileName = "empty.ts"; const emptyFileName = "empty.ts";
const emptyFileRelativePath = "./" + emptyFileName; const emptyFileRelativePath = "./" + emptyFileName;
const emptyFile: Harness.Compiler.TestFile = { const emptyFile = new documents.TextDocument(emptyFileName, "");
unitName: emptyFileName,
content: ""
};
const referenceFileName = "reference.ts"; const referenceFileName = "reference.ts";
const referenceFileRelativePath = "./" + referenceFileName; const referenceFileRelativePath = "./" + referenceFileName;
const referenceFile: Harness.Compiler.TestFile = { const referenceFile = new documents.TextDocument(referenceFileName,
unitName: referenceFileName,
content:
"/// <reference path=\"d:/imaginary/nonexistent1.ts\"/>\n" + // Absolute "/// <reference path=\"d:/imaginary/nonexistent1.ts\"/>\n" + // Absolute
"/// <reference path=\"./nonexistent2.ts\"/>\n" + // Relative "/// <reference path=\"./nonexistent2.ts\"/>\n" + // Relative
"/// <reference path=\"nonexistent3.ts\"/>\n" + // Unqualified "/// <reference path=\"nonexistent3.ts\"/>\n" + // Unqualified
"/// <reference path=\"nonexistent4\"/>\n" // No extension "/// <reference path=\"nonexistent4\"/>\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 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", () => { it("handles no missing root files", () => {
const program = createProgram([emptyFileRelativePath], options, testCompilerHost); const program = createProgram([emptyFileRelativePath], options, testCompilerHost);
const missing = program.getMissingFilePaths(); const missing = program.getMissingFilePaths();

View file

@ -14,13 +14,12 @@ describe("Public APIs", () => {
}); });
it("should compile", () => { it("should compile", () => {
const testFile: Harness.Compiler.TestFile = { const fs = vfs.createFromFileSystem(Harness.IO, /*ignoreCase*/ false);
unitName: builtFile, fs.linkSync(`${vfs.builtFolder}/${fileName}`, `${vfs.srcFolder}/${fileName}`);
content: fileContent const sys = new fakes.System(fs);
}; const host = new fakes.CompilerHost(sys);
const inputFiles = [testFile]; const result = compiler.compileFiles(host, [`${vfs.srcFolder}/${fileName}`], {});
const output = Harness.Compiler.compileFiles(inputFiles, [], /*harnessSettings*/ undefined, /*options*/ {}, /*currentDirectory*/ undefined); assert(!result.diagnostics || !result.diagnostics.length, Harness.Compiler.minimalDiagnosticsToString(result.diagnostics, /*pretty*/ true));
assert(!output.result.errors || !output.result.errors.length, Harness.Compiler.minimalDiagnosticsToString(output.result.errors, /*pretty*/ true));
}); });
} }

View file

@ -4,17 +4,13 @@ namespace ts {
describe("Symbol Walker", () => { describe("Symbol Walker", () => {
function test(description: string, source: string, verifier: (file: SourceFile, checker: TypeChecker) => void) { function test(description: string, source: string, verifier: (file: SourceFile, checker: TypeChecker) => void) {
it(description, () => { it(description, () => {
let {result} = Harness.Compiler.compileFiles([{ const result = Harness.Compiler.compileFiles([{
unitName: "main.ts", unitName: "main.ts",
content: source content: source
}], [], {}, {}, "/"); }], [], {}, {}, "/");
let file = result.program.getSourceFile("main.ts"); const file = result.program.getSourceFile("main.ts");
let checker = result.program.getTypeChecker(); const checker = result.program.getTypeChecker();
verifier(file, checker); verifier(file, checker);
result = undefined;
file = undefined;
checker = undefined;
}); });
} }

View file

@ -1,5 +1,7 @@
/// <reference path="..\harness.ts" /> /// <reference path="..\harness.ts" />
/// <reference path="..\..\compiler\commandLineParser.ts" /> /// <reference path="..\..\compiler\commandLineParser.ts" />
/// <reference path="../compiler.ts" />
/// <reference path="../vfs.ts" />
namespace ts { namespace ts {
describe("parseConfigFileTextToJson", () => { describe("parseConfigFileTextToJson", () => {
@ -31,13 +33,15 @@ namespace ts {
function getParsedCommandJson(jsonText: string, configFileName: string, basePath: string, allFileList: string[]) { function getParsedCommandJson(jsonText: string, configFileName: string, basePath: string, allFileList: string[]) {
const parsed = parseConfigFileTextToJson(configFileName, jsonText); 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); return parseJsonConfigFileContent(parsed.config, host, basePath, /*existingOptions*/ undefined, configFileName);
} }
function getParsedCommandJsonNode(jsonText: string, configFileName: string, basePath: string, allFileList: string[]) { function getParsedCommandJsonNode(jsonText: string, configFileName: string, basePath: string, allFileList: string[]) {
const parsed = parseJsonText(configFileName, jsonText); 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); return parseJsonSourceFileConfigFileContent(parsed, host, basePath, /*existingOptions*/ undefined, configFileName);
} }

114
src/harness/utils.ts Normal file
View file

@ -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;
}
}

1306
src/harness/vfs.ts Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,222 +0,0 @@
/// <reference path="harness.ts" />
/// <reference path="..\compiler\commandLineParser.ts"/>
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 <VirtualDirectory[]>ts.filter(this.getFileSystemEntries(), entry => entry.isDirectory());
}
getFiles(): VirtualFile[] {
return <VirtualFile[]>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> | 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<string>, excludes: ReadonlyArray<string>, includes: ReadonlyArray<string>, depth: number) {
return ts.matchFiles(path, extensions, excludes, includes, this.useCaseSensitiveFileNames, this.currentDirectory, depth, (path: string) => this.getAccessibleFileSystemEntries(path));
}
}
}

127
src/harness/vpath.ts Normal file
View file

@ -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<string> = [".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.");
}
}

View file

@ -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); const project = this.findProject(response.projectName);
if (!project) { if (!project) {
return; return;

View file

@ -266,7 +266,7 @@ namespace ts.codefix {
return [global]; 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") { if (!baseUrl || preferences.importModuleSpecifierPreference === "relative") {
return [relativePath]; return [relativePath];
} }
@ -321,7 +321,7 @@ namespace ts.codefix {
1 < 2 = true 1 < 2 = true
In this case we should prefer using the relative path "../a" instead of the baseUrl path "foo/a". 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); const relativeFirst = getRelativePathNParents(relativePath) < getRelativePathNParents(pathFromSourceToBaseUrl);
return relativeFirst ? [relativePath, importRelativeToBaseUrl] : [importRelativeToBaseUrl, relativePath]; return relativeFirst ? [relativePath, importRelativeToBaseUrl] : [importRelativeToBaseUrl, relativePath];
}); });
@ -343,11 +343,12 @@ namespace ts.codefix {
} }
function getRelativePathNParents(relativePath: string): number { function getRelativePathNParents(relativePath: string): number {
let count = 0; const components = getPathComponents(relativePath);
for (let i = 0; i + 3 <= relativePath.length && relativePath.slice(i, i + 3) === "../"; i += 3) { if (components[0] || components.length === 1) return 0;
count++; 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 { function tryGetModuleNameFromAmbientModule(moduleSymbol: Symbol): string | undefined {
@ -389,7 +390,7 @@ namespace ts.codefix {
} }
const normalizedSourcePath = getPathRelativeToRootDirs(sourceDirectory, rootDirs, getCanonicalFileName); 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); return removeFileExtension(relativePath);
} }
@ -472,7 +473,7 @@ namespace ts.codefix {
return path.substring(parts.topLevelPackageNameIndex + 1); return path.substring(parts.topLevelPackageNameIndex + 1);
} }
else { else {
return getRelativePath(path, sourceDirectory, getCanonicalFileName); return ensurePathIsNonModuleName(getRelativePath(sourceDirectory, path, getCanonicalFileName));
} }
} }
} }

View file

@ -63,10 +63,10 @@ namespace ts {
function getPathUpdater(oldFilePath: string, newFilePath: string, host: LanguageServiceHost): (oldPath: string) => string | undefined { 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. // 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 => { return oldPath => {
if (!pathIsRelative(oldPath)) return; if (!pathIsRelative(oldPath)) return;
return ensurePathIsRelative(normalizePath(combinePaths(getDirectoryPath(oldPath), rel))); return ensurePathIsNonModuleName(normalizePath(combinePaths(getDirectoryPath(oldPath), rel)));
}; };
} }

View file

@ -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; * Remove the basename from the path. Note that we don't use the basename to filter completions;
* the client is responsible for refining completions. * the client is responsible for refining completions.
*/ */
if (!hasTrailingDirectorySeparator(fragment)) {
fragment = getDirectoryPath(fragment); fragment = getDirectoryPath(fragment);
}
if (fragment === "") { if (fragment === "") {
fragment = "." + directorySeparator; fragment = "." + directorySeparator;
@ -97,8 +99,9 @@ namespace ts.Completions.PathCompletions {
fragment = ensureTrailingDirectorySeparator(fragment); fragment = ensureTrailingDirectorySeparator(fragment);
const absolutePath = normalizeAndPreserveTrailingSlash(isRootedDiskPath(fragment) ? fragment : combinePaths(scriptPath, fragment)); // const absolutePath = normalizeAndPreserveTrailingSlash(isRootedDiskPath(fragment) ? fragment : combinePaths(scriptPath, fragment)); // TODO(rbuckton): should use resolvePaths
const baseDirectory = getDirectoryPath(absolutePath); const absolutePath = resolvePath(scriptPath, fragment);
const baseDirectory = hasTrailingDirectorySeparator(absolutePath) ? absolutePath : getDirectoryPath(absolutePath);
const ignoreCase = !(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames()); const ignoreCase = !(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames());
if (tryDirectoryExists(host, baseDirectory)) { 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)) { for (const ambientName of getAmbientModuleCompletions(fragment, fragmentDirectory, typeChecker)) {
result.push(nameAndKind(ambientName, ScriptElementKind.externalModuleName)); 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 // 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* // full directory component. For example: directory/path/of/prefix/base*
const normalizedPrefix = normalizeAndPreserveTrailingSlash(parsed.prefix); const normalizedPrefix = resolvePath(parsed.prefix);
const normalizedPrefixDirectory = getDirectoryPath(normalizedPrefix); const normalizedPrefixDirectory = hasTrailingDirectorySeparator(parsed.prefix) ? normalizedPrefix : getDirectoryPath(normalizedPrefix);
const normalizedPrefixBase = getBaseFileName(normalizedPrefix); const normalizedPrefixBase = hasTrailingDirectorySeparator(parsed.prefix) ? "" : getBaseFileName(normalizedPrefix);
const fragmentHasPath = containsSlash(fragment); 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 // 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); const normalizedSuffix = normalizePath(parsed.suffix);
// Need to normalize after combining: If we combinePaths("a", "../b"), we want "b" and not "a/../b". // 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; 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 * 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 * to determine if the caret is currently within the string literal and capture the literal fragment

View file

@ -1122,11 +1122,6 @@ namespace ts {
return false; 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 { export function isInReferenceComment(sourceFile: SourceFile, position: number): boolean {
return isInComment(sourceFile, position, /*tokenAtPosition*/ undefined, c => { return isInComment(sourceFile, position, /*tokenAtPosition*/ undefined, c => {
const commentText = sourceFile.text.substring(c.pos, c.end); const commentText = sourceFile.text.substring(c.pos, c.end);

View file

@ -38,6 +38,6 @@ _2["default"].aIndex;
"use strict"; "use strict";
exports.__esModule = true; exports.__esModule = true;
var __1 = require(".."); var __1 = require("..");
var _1 = require("../"); var __2 = require("../");
__1["default"].a; __1["default"].a;
_1["default"].aIndex; __2["default"].aIndex;

View file

@ -9,7 +9,7 @@
"File '/types/jquery/jquery.d.ts' exist - use it as a name resolution result.", "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'.", "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. ========", "======== 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'.", "Resolving with primary search path '/types'.",
"'package.json' does not have a 'typings' field.", "'package.json' does not have a 'typings' field.",
"'package.json' has 'types' field 'jquery.d.ts' that references '/types/jquery/jquery.d.ts'.", "'package.json' has 'types' field 'jquery.d.ts' that references '/types/jquery/jquery.d.ts'.",

View file

@ -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 TS1100: Invalid use of 'eval' in strict mode.
tests/cases/compiler/variableDeclarationInStrictMode1.ts(2,5): error TS2300: Duplicate identifier 'eval'. 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) ==== ==== tests/cases/compiler/variableDeclarationInStrictMode1.ts (2 errors) ====

View file

@ -5,7 +5,7 @@
//@allowSyntheticDefaultImports: true //@allowSyntheticDefaultImports: true
//@allowJs: true //@allowJs: true
//@jsx: react //@jsx: react
//@outDir: "build" //@outDir: build
//@filename: react.d.ts //@filename: react.d.ts
export = React; export = React;

View file

@ -1,7 +1,7 @@
// @noImplicitReferences: true // @noImplicitReferences: true
// @traceResolution: true // @traceResolution: true
// @typeRoots: /types // @typeRoots: /types
// @currentDirectory: test // @currentDirectory: /test
// package.json in a primary reference can refer to another file // package.json in a primary reference can refer to another file

View file

@ -1,3 +1,4 @@
// @lib: es2015
// @strict: true // @strict: true
// @target: es2015 // @target: es2015

View file

@ -21,7 +21,7 @@
// @Filename: my_typings/some-module/index.d.ts // @Filename: my_typings/some-module/index.d.ts
//// export var x = 9; //// 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("1", ["someFile2"], { isNewIdentifierLocation: true });
verify.completionsAt("2", [{ name: "some-module", replacementSpan: test.ranges()[0] }], { isNewIdentifierLocation: true }); verify.completionsAt("2", [{ name: "some-module", replacementSpan: test.ranges()[0] }], { isNewIdentifierLocation: true });
verify.completionsAt("3", ["fourslash"], { isNewIdentifierLocation: true }); verify.completionsAt("3", ["fourslash"], { isNewIdentifierLocation: true });

View file

@ -20,7 +20,7 @@
// @Filename: my_typings/some-module/index.d.ts // @Filename: my_typings/some-module/index.d.ts
//// export var x = 9; //// 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("1", ["someFile2"], { isNewIdentifierLocation: true });
verify.completionsAt("2", [{ name: "some-module", replacementSpan: test.ranges()[0] }], { isNewIdentifierLocation: true }); verify.completionsAt("2", [{ name: "some-module", replacementSpan: test.ranges()[0] }], { isNewIdentifierLocation: true });
verify.completionsAt("3", ["fourslash"], { isNewIdentifierLocation: true }); verify.completionsAt("3", ["fourslash"], { isNewIdentifierLocation: true });

View file

@ -20,7 +20,7 @@
// @Filename: my_typings/some-module/index.d.ts // @Filename: my_typings/some-module/index.d.ts
//// export var x = 9; //// 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("1", ["some-module"], { isNewIdentifierLocation: true });
verify.completionsAt("2", ["someOtherFile.ts"], { isNewIdentifierLocation: true }); verify.completionsAt("2", ["someOtherFile.ts"], { isNewIdentifierLocation: true });
verify.completionsAt("3", ["some-module"], { isNewIdentifierLocation: true }); verify.completionsAt("3", ["some-module"], { isNewIdentifierLocation: true });

View file

@ -42,6 +42,6 @@
verify.completions({ verify.completions({
at: test.markerNames(), at: test.markerNames(),
are: ["prefix", "prefix-only", "2test", "0test", "1test"], are: ["2test", "prefix", "prefix-only", "0test", "1test"],
isNewIdentifierLocation: true, isNewIdentifierLocation: true,
}); });

View file

@ -39,6 +39,6 @@ verify.completions(
}, },
{ {
at: kinds.map(k => `${k}2`), at: kinds.map(k => `${k}2`),
are: ["e1", "f1", "f2", "tests", "folder"], are: ["e1", "f1", "f2", "folder", "tests"],
isNewIdentifierLocation: true, isNewIdentifierLocation: true,
}); });

View file

@ -20,7 +20,7 @@
// @Filename: my_typings/some-module/index.d.ts // @Filename: my_typings/some-module/index.d.ts
//// export var x = 9; //// 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("1", ["someFile2"], { isNewIdentifierLocation: true });
verify.completionsAt("2", [{ name: "some-module", replacementSpan: test.ranges()[0] }], { isNewIdentifierLocation: true }); verify.completionsAt("2", [{ name: "some-module", replacementSpan: test.ranges()[0] }], { isNewIdentifierLocation: true });
verify.completionsAt("3", ["fourslash"], { isNewIdentifierLocation: true }); verify.completionsAt("3", ["fourslash"], { isNewIdentifierLocation: true });

View file

@ -16,6 +16,6 @@
const ranges = test.ranges(); const ranges = test.ranges();
const [r0, r1, r2] = 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 // Testing that it works with documentHighlights too
verify.rangesAreDocumentHighlights(); verify.rangesAreDocumentHighlights();

View file

@ -1,13 +1,13 @@
/// <reference path="fourslash.ts"/> /// <reference path="fourslash.ts"/>
// @Filename: test/my fil"e.ts // @Filename: test/my fil e.ts
////export class Bar { ////export class Bar {
//// public s: string; //// public s: string;
////} ////}
////export var x: number; ////export var x: number;
verify.navigationTree({ verify.navigationTree({
"text": "\"my fil\\\"e\"", "text": "\"my fil\\te\"",
"kind": "module", "kind": "module",
"childItems": [ "childItems": [
{ {
@ -32,7 +32,7 @@ verify.navigationTree({
verify.navigationBar([ verify.navigationBar([
{ {
"text": "\"my fil\\\"e\"", "text": "\"my fil\\te\"",
"kind": "module", "kind": "module",
"childItems": [ "childItems": [
{ {

View file

@ -21,8 +21,10 @@
<button id='runTestBtn' onclick='runTests()' type="button" class="btn">Run Tests</button> <button id='runTestBtn' onclick='runTests()' type="button" class="btn">Run Tests</button>
</div> </div>
<div id="mocha"></div> <div id="mocha"></div>
<script src="../node_modules/source-map-support/browser-source-map-support.js"></script>
<script>sourceMapSupport.install();</script>
<script src="../node_modules/mocha/mocha.js"></script> <script src="../node_modules/mocha/mocha.js"></script>
<script>mocha.setup('bdd')</script> <script>mocha.setup({ ui: 'bdd', timeout: 0 })</script>
<script src="../built/local/bundle.js"></script> <script src="../built/local/bundle.js"></script>
<script> <script>

File diff suppressed because it is too large Load diff