Initial support for globs in tsconfig.json

This commit is contained in:
Ron Buckton
2015-12-03 10:44:24 -08:00
parent 7d920c2aad
commit f9ae3e4f2b
14 changed files with 1086 additions and 87 deletions
+543 -28
View File
@@ -495,46 +495,51 @@ namespace ts {
};
function getFileNames(): string[] {
let fileNames: string[] = [];
let fileNames: string[];
if (hasProperty(json, "files")) {
if (json["files"] instanceof Array) {
fileNames = map(<string[]>json["files"], s => combinePaths(basePath, s));
if (isArray(json["files"])) {
fileNames = <string[]>json["files"];
}
else {
errors.push(createCompilerDiagnostic(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "files", "Array"));
}
}
else {
const filesSeen: Map<boolean> = {};
const exclude = json["exclude"] instanceof Array ? map(<string[]>json["exclude"], normalizeSlashes) : undefined;
const supportedExtensions = getSupportedExtensions(options);
Debug.assert(indexOf(supportedExtensions, ".ts") < indexOf(supportedExtensions, ".d.ts"), "Changed priority of extensions to pick");
// Get files of supported extensions in their order of resolution
for (const extension of supportedExtensions) {
const filesInDirWithExtension = host.readDirectory(basePath, extension, exclude);
for (const fileName of filesInDirWithExtension) {
// .ts extension would read the .d.ts extension files too but since .d.ts is lower priority extension,
// lets pick them when its turn comes up
if (extension === ".ts" && fileExtensionIs(fileName, ".d.ts")) {
continue;
}
let includeSpecs: string[];
if (hasProperty(json, "include")) {
if (isArray(json["include"])) {
includeSpecs = <string[]>json["include"];
}
else {
errors.push(createCompilerDiagnostic(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "include", "Array"));
}
}
// If this is one of the output extension (which would be .d.ts and .js if we are allowing compilation of js files)
// do not include this file if we included .ts or .tsx file with same base name as it could be output of the earlier compilation
if (extension === ".d.ts" || (options.allowJs && contains(supportedJavascriptExtensions, extension))) {
const baseName = fileName.substr(0, fileName.length - extension.length);
if (hasProperty(filesSeen, baseName + ".ts") || hasProperty(filesSeen, baseName + ".tsx")) {
continue;
}
}
let excludeSpecs: string[];
if (hasProperty(json, "exclude")) {
if (isArray(json["exclude"])) {
excludeSpecs = <string[]>json["exclude"];
}
else {
errors.push(createCompilerDiagnostic(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "exclude", "Array"));
}
}
filesSeen[fileName] = true;
fileNames.push(fileName);
if (fileNames === undefined && includeSpecs === undefined) {
includeSpecs = ["**/*.ts"];
if (options.jsx) {
includeSpecs.push("**/*.tsx");
}
if (options.allowJs) {
includeSpecs.push("**/*.js");
if (options.jsx) {
includeSpecs.push("**/*.jsx");
}
}
}
return fileNames;
return expandFiles(fileNames, includeSpecs, excludeSpecs, basePath, options, host, errors);
}
}
@@ -584,4 +589,514 @@ namespace ts {
return { options, errors };
}
// Simplified whitelist, forces escaping of any non-word (or digit), non-whitespace character.
const reservedCharacterPattern = /[^\w\s]/g;
const enum ExpandResult {
Ok,
Error
}
/**
* Expands an array of file specifications.
*
* @param fileNames The literal file names to include.
* @param includeSpecs The file specifications to expand.
* @param excludeSpecs The file specifications to exclude.
* @param basePath The base path for any relative file specifications.
* @param options Compiler options.
* @param host The host used to resolve files and directories.
* @param errors An array for diagnostic reporting.
*/
export function expandFiles(fileNames: string[], includeSpecs: string[], excludeSpecs: string[], basePath: string, options: CompilerOptions, host: ParseConfigHost, errors?: Diagnostic[]): string[] {
basePath = normalizePath(basePath);
basePath = removeTrailingDirectorySeparator(basePath);
const excludePattern = includeSpecs ? createExcludeRegularExpression(excludeSpecs, basePath, options, host, errors) : undefined;
const fileSet = createFileMap<Path>(host.useCaseSensitiveFileNames ? caseSensitiveKeyMapper : caseInsensitiveKeyMapper);
// include every literal file.
if (fileNames) {
for (const fileName of fileNames) {
const path = toPath(fileName, basePath, caseSensitiveKeyMapper);
if (!fileSet.contains(path)) {
fileSet.set(path, path);
}
}
}
// expand and include the provided files into the file set.
if (includeSpecs) {
for (let includeSpec of includeSpecs) {
includeSpec = normalizePath(includeSpec);
includeSpec = removeTrailingDirectorySeparator(includeSpec);
expandFileSpec(basePath, includeSpec, 0, excludePattern, options, host, errors, fileSet);
}
}
const output = fileSet.reduce(addFileToOutput, []);
return output;
}
/**
* Expands a file specification with wildcards.
*
* @param basePath The directory to expand.
* @param fileSpec The original file specification.
* @param start The starting offset in the file specification.
* @param excludePattern A pattern used to exclude a file specification.
* @param options Compiler options.
* @param host The host used to resolve files and directories.
* @param errors An array for diagnostic reporting.
* @param fileSet The set of matching files.
* @param isExpandingRecursiveDirectory A value indicating whether the file specification includes a recursive directory wildcard prior to the start of this segment.
*/
function expandFileSpec(basePath: string, fileSpec: string, start: number, excludePattern: RegExp, options: CompilerOptions, host: ParseConfigHost, errors: Diagnostic[], fileSet: FileMap<Path>, isExpandingRecursiveDirectory?: boolean): ExpandResult {
// Skip expansion if the base path matches an exclude pattern.
if (isExcludedPath(excludePattern, basePath)) {
return ExpandResult.Ok;
}
// Find the offset of the next wildcard in the file specification
let offset = indexOfWildcard(fileSpec, start);
if (offset < 0) {
// There were no more wildcards, so include the file.
const path = toPath(fileSpec.substring(start), basePath, caseSensitiveKeyMapper);
includeFile(path, excludePattern, options, host, fileSet);
return ExpandResult.Ok;
}
// Find the last directory separator before the wildcard to get the leading path.
offset = fileSpec.lastIndexOf(directorySeparator, offset);
if (offset > start) {
// The wildcard occurs in a later segment, include remaining path up to
// wildcard in prefix.
basePath = combinePaths(basePath, fileSpec.substring(start, offset));
// Skip this wildcard path if the base path now matches an exclude pattern.
if (isExcludedPath(excludePattern, basePath)) {
return ExpandResult.Ok;
}
start = offset + 1;
}
// Find the offset of the next directory separator to extract the wildcard path segment.
offset = getEndOfPathSegment(fileSpec, start);
// Check if the current offset is the beginning of a recursive directory pattern.
if (isRecursiveDirectoryWildcard(fileSpec, start, offset)) {
if (offset >= fileSpec.length) {
// If there is no file specification following the recursive directory pattern
// we cannot match any files, so we will ignore this pattern.
return ExpandResult.Ok;
}
// Stop expansion if a file specification contains more than one recursive directory pattern.
if (isExpandingRecursiveDirectory) {
if (errors) {
errors.push(createCompilerDiagnostic(Diagnostics.File_specification_cannot_contain_multiple_recursive_directory_wildcards_Asterisk_Asterisk_Colon_0, fileSpec));
}
return ExpandResult.Error;
}
// Expand the recursive directory pattern.
return expandRecursiveDirectory(basePath, fileSpec, offset + 1, excludePattern, options, host, errors, fileSet);
}
// Match the entries in the directory against the wildcard pattern.
const pattern = createRegularExpressionFromWildcard(fileSpec, start, offset, host);
// If there are no more directory separators (the offset is at the end of the file specification), then
// this must be a file.
if (offset >= fileSpec.length) {
const files = host.readFileNames(basePath);
for (const extension of getSupportedExtensions(options)) {
for (const file of files) {
if (fileExtensionIs(file, extension)) {
const path = toPath(file, basePath, caseSensitiveKeyMapper);
// .ts extension would read the .d.ts extension files too but since .d.ts is lower priority extension,
// lets pick them when its turn comes up.
if (extension === ".ts" && fileExtensionIs(file, ".d.ts")) {
continue;
}
// If this is one of the output extension (which would be .d.ts and .js if we are allowing compilation of js files)
// do not include this file if we included .ts or .tsx file with same base name as it could be output of the earlier compilation
if (extension === ".d.ts" || (options.allowJs && contains(supportedJavascriptExtensions, extension))) {
if (fileSet.contains(changeExtension(path, ".ts")) || fileSet.contains(changeExtension(path, ".tsx"))) {
continue;
}
}
// This wildcard has no further directory to process, so include the file.
includeFile(path, excludePattern, options, host, fileSet);
}
}
}
}
else {
const directories = host.readDirectoryNames(basePath);
for (const directory of directories) {
// If this was a directory, process the directory.
const path = toPath(directory, basePath, caseSensitiveKeyMapper);
if (expandFileSpec(path, fileSpec, offset + 1, excludePattern, options, host, errors, fileSet, isExpandingRecursiveDirectory) === ExpandResult.Error) {
return ExpandResult.Error;
}
}
}
return ExpandResult.Ok;
}
/**
* Expands a `**` recursive directory wildcard.
*
* @param basePath The directory to recursively expand.
* @param fileSpec The original file specification.
* @param start The starting offset in the file specification.
* @param excludePattern A pattern used to exclude a file specification.
* @param options Compiler options.
* @param host The host used to resolve files and directories.
* @param errors An array for diagnostic reporting.
* @param fileSet The set of matching files.
*/
function expandRecursiveDirectory(basePath: string, fileSpec: string, start: number, excludePattern: RegExp, options: CompilerOptions, host: ParseConfigHost, errors: Diagnostic[], fileSet: FileMap<Path>): ExpandResult {
// expand the non-recursive part of the file specification against the prefix path.
if (expandFileSpec(basePath, fileSpec, start, excludePattern, options, host, errors, fileSet, /*isExpandingRecursiveDirectory*/ true) === ExpandResult.Error) {
return ExpandResult.Error;
}
// Recursively expand each subdirectory.
const directories = host.readDirectoryNames(basePath);
for (const entry of directories) {
const path = combinePaths(basePath, entry);
if (expandRecursiveDirectory(path, fileSpec, start, excludePattern, options, host, errors, fileSet) === ExpandResult.Error) {
return ExpandResult.Error;
}
}
return ExpandResult.Ok;
}
/**
* Attempts to include a file in a file set.
*
* @param file The file to include.
* @param excludePattern A pattern used to exclude a file specification.
* @param options Compiler options.
* @param host The host used to resolve files and directories.
* @param fileSet The set of matching files.
*/
function includeFile(file: Path, excludePattern: RegExp, options: CompilerOptions, host: ParseConfigHost, fileSet: FileMap<Path>): void {
// Ignore the file if it should be excluded.
if (isExcludedPath(excludePattern, file)) {
return;
}
// Ignore the file if it doesn't exist.
if (!host.fileExists(file)) {
return;
}
// Ignore the file if it does not have a supported extension.
if (!options.allowNonTsExtensions && !isSupportedSourceFileName(file, options)) {
return;
}
if (!fileSet.contains(file)) {
fileSet.set(file, file);
}
}
/**
* Adds a file to an array of files.
*
* @param output The output array.
* @param file The file path.
*/
function addFileToOutput(output: string[], file: string) {
output.push(file);
return output;
}
/**
* Determines whether a path should be excluded.
*
* @param excludePattern A pattern used to exclude a file specification.
* @param path The path to test for exclusion.
*/
function isExcludedPath(excludePattern: RegExp, path: string) {
return excludePattern ? excludePattern.test(path) : false;
}
/**
* Creates a regular expression from a glob-style wildcard.
*
* @param fileSpec The file specification.
* @param start The starting offset in the file specification.
* @param end The end offset in the file specification.
* @param host The host used to resolve files and directories.
*/
function createRegularExpressionFromWildcard(fileSpec: string, start: number, end: number, host: ParseConfigHost): RegExp {
const pattern = createPatternFromWildcard(fileSpec, start, end);
return new RegExp("^" + pattern + "$", host.useCaseSensitiveFileNames ? "" : "i");
}
/**
* Creates a pattern from a wildcard segment.
*
* @param fileSpec The file specification.
* @param start The starting offset in the file specification.
* @param end The end offset in the file specification.
*/
function createPatternFromWildcard(fileSpec: string, start: number, end: number): string {
let pattern = "";
let offset = indexOfWildcard(fileSpec, start);
while (offset >= 0 && offset < end) {
if (offset > start) {
// Escape and append the non-wildcard portion to the regular expression
pattern += escapeRegularExpressionText(fileSpec, start, offset);
}
const charCode = fileSpec.charCodeAt(offset);
if (charCode === CharacterCodes.asterisk) {
// Append a multi-character (zero or more characters) pattern to the regular expression
pattern += "[^/]*";
}
else if (charCode === CharacterCodes.question) {
// Append a single-character (zero or one character) pattern to the regular expression
pattern += "[^/]";
}
start = offset + 1;
offset = indexOfWildcard(fileSpec, start);
}
// Escape and append any remaining non-wildcard portion.
if (start < end) {
pattern += escapeRegularExpressionText(fileSpec, start, end);
}
return pattern;
}
/**
* Creates a regular expression from a glob-style wildcard used to exclude a file.
*
* @param excludeSpecs The file specifications to exclude.
* @param basePath The prefix path.
* @param options Compiler options.
* @param host The host used to resolve files and directories.
* @param errors An array for diagnostic reporting.
*/
function createExcludeRegularExpression(excludeSpecs: string[], basePath: string, options: CompilerOptions, host: ParseConfigHost, errors: Diagnostic[]): RegExp {
// Ignore an empty exclusion list
if (!excludeSpecs || excludeSpecs.length === 0) {
return undefined;
}
basePath = escapeRegularExpressionText(basePath, 0, basePath.length);
let pattern = "";
for (const excludeSpec of excludeSpecs) {
const excludePattern = createExcludePattern(excludeSpec, basePath, options, host, errors);
if (excludePattern) {
if (pattern.length > 0) {
pattern += "|";
}
pattern += "(" + excludePattern + ")";
}
}
if (pattern.length > 0) {
return new RegExp("^(" + pattern + ")($|/)", host.useCaseSensitiveFileNames ? "" : "i");
}
return undefined;
}
/**
* Creates a pattern for used to exclude a file.
*
* @param excludeSpec The file specification to exclude.
* @param basePath The base path for the exclude pattern.
* @param options Compiler options.
* @param host The host used to resolve files and directories.
* @param errors An array for diagnostic reporting.
*/
function createExcludePattern(excludeSpec: string, basePath: string, options: CompilerOptions, host: ParseConfigHost, errors: Diagnostic[]): string {
if (!excludeSpec) {
return undefined;
}
excludeSpec = normalizePath(excludeSpec);
excludeSpec = removeTrailingDirectorySeparator(excludeSpec);
let pattern = isRootedDiskPath(excludeSpec) ? "" : basePath;
let hasRecursiveDirectoryWildcard = false;
let segmentStart = 0;
let segmentEnd = getEndOfPathSegment(excludeSpec, segmentStart);
while (segmentStart < segmentEnd) {
if (isRecursiveDirectoryWildcard(excludeSpec, segmentStart, segmentEnd)) {
if (hasRecursiveDirectoryWildcard) {
if (errors) {
errors.push(createCompilerDiagnostic(Diagnostics.File_specification_cannot_contain_multiple_recursive_directory_wildcards_Asterisk_Asterisk_Colon_0, excludeSpec));
}
return undefined;
}
// As an optimization, if the recursive directory is the last
// wildcard, or is followed by only `*` or `*.ts`, don't add the
// remaining pattern and exit the loop.
if (canElideRecursiveDirectorySegment(excludeSpec, segmentEnd, options, host)) {
break;
}
hasRecursiveDirectoryWildcard = true;
pattern += "(/.+)?";
}
else {
if (pattern) {
pattern += directorySeparator;
}
pattern += createPatternFromWildcard(excludeSpec, segmentStart, segmentEnd);
}
segmentStart = segmentEnd + 1;
segmentEnd = getEndOfPathSegment(excludeSpec, segmentStart);
}
return pattern;
}
/**
* Determines whether a recursive directory segment can be elided when
* building a regular expression to exclude a path.
*
* @param excludeSpec The file specification used to exclude a path.
* @param segmentEnd The end position of the recursive directory segment.
* @param options Compiler options.
* @param host The host used to resolve files and directories.
*/
function canElideRecursiveDirectorySegment(excludeSpec: string, segmentEnd: number, options: CompilerOptions, host: ParseConfigHost) {
// If there are no segments after this segment, the pattern for this segment may be elided.
if (segmentEnd + 1 >= excludeSpec.length) {
return true;
}
// If the following segment is a wildcard that may be elided, the pattern for this segment may be elided.
return canElideWildcardSegment(excludeSpec, segmentEnd + 1, options, host);
}
/**
* Determines whether a wildcard segment can be elided when building a
* regular expression to exclude a path.
*
* @param excludeSpec The file specification used to exclude a path.
* @param segmentStart The starting position of the segment.
* @param options Compiler options.
* @param host The host used to resolve files and directories.
*/
function canElideWildcardSegment(excludeSpec: string, segmentStart: number, options: CompilerOptions, host: ParseConfigHost) {
const charCode = excludeSpec.charCodeAt(segmentStart);
if (charCode === CharacterCodes.asterisk) {
const end = excludeSpec.length;
// If the segment consists only of `*`, we may elide this segment.
if (segmentStart + 1 === end) {
return true;
}
// If the segment consists only of `*.ts`, and we do not allow
// any other extensions for source files, we may elide this segment.
if (!options.allowNonTsExtensions && !options.jsx && !options.allowJs && segmentStart + 4 === end) {
const segment = excludeSpec.substr(segmentStart);
return fileExtensionIs(host.useCaseSensitiveFileNames ? segment : segment.toLowerCase(), ".ts");
}
}
return false;
}
/**
* Escape regular expression reserved tokens.
*
* @param text The text to escape.
* @param start The starting offset in the string.
* @param end The ending offset in the string.
*/
function escapeRegularExpressionText(text: string, start: number, end: number) {
return text.substring(start, end).replace(reservedCharacterPattern, "\\$&");
}
/**
* Determines whether the wildcard at the current offset is a recursive directory wildcard.
*
* @param fileSpec The file specification.
* @param segmentStart The starting offset of a segment in the file specification.
* @param segmentEnd The ending offset of a segment in the file specification.
*/
function isRecursiveDirectoryWildcard(fileSpec: string, segmentStart: number, segmentEnd: number) {
return segmentEnd - segmentStart === 2 &&
fileSpec.charCodeAt(segmentStart) === CharacterCodes.asterisk &&
fileSpec.charCodeAt(segmentStart + 1) === CharacterCodes.asterisk;
}
/**
* Gets the index of the next wildcard character in a file specification.
*
* @param fileSpec The file specification.
* @param start The starting offset in the file specification.
*/
function indexOfWildcard(fileSpec: string, start: number): number {
for (let i = start; i < fileSpec.length; ++i) {
const ch = fileSpec.charCodeAt(i);
if (ch === CharacterCodes.asterisk || ch === CharacterCodes.question) {
return i;
}
}
return -1;
}
/**
* Get the end position of a path segment, either the index of the next directory separator or
* the provided end position.
*
* @param fileSpec The file specification.
* @param segmentStart The start offset in the file specification.
*/
function getEndOfPathSegment(fileSpec: string, segmentStart: number): number {
const end = fileSpec.length;
if (segmentStart >= end) {
return end;
}
const offset = fileSpec.indexOf(directorySeparator, segmentStart);
return offset < 0 ? end : offset;
}
/**
* Gets a case sensitive key.
*
* @param key The original key.
*/
function caseSensitiveKeyMapper(key: string) {
return key;
}
/**
* Gets a case insensitive key.
*
* @param key The original key.
*/
function caseInsensitiveKeyMapper(key: string) {
return key.toLowerCase();
}
}
+70
View File
@@ -25,6 +25,7 @@ namespace ts {
contains,
remove,
forEachValue: forEachValueInMap,
reduce,
clear
};
@@ -34,6 +35,10 @@ namespace ts {
}
}
function reduce<U>(callback: (memo: U, value: T, key: Path) => U, initial: U) {
return reduceProperties(files, callback, initial);
}
// path should already be well-formed so it does not need to be normalized
function get(path: Path): T {
return files[toKey(path)];
@@ -490,6 +495,24 @@ namespace ts {
return a < b ? Comparison.LessThan : Comparison.GreaterThan;
}
export function compareStrings(a: string, b: string, ignoreCase?: boolean): Comparison {
if (a === b) return Comparison.EqualTo;
if (a === undefined) return Comparison.LessThan;
if (b === undefined) return Comparison.GreaterThan;
if (ignoreCase) {
if (String.prototype.localeCompare) {
const result = a.localeCompare(b, /*locales*/ undefined, { usage: "sort", sensitivity: "accent" });
return result < 0 ? Comparison.LessThan : result > 0 ? Comparison.GreaterThan : Comparison.EqualTo;
}
a = a.toUpperCase();
b = b.toUpperCase();
if (a === b) return Comparison.EqualTo;
}
return a < b ? Comparison.LessThan : Comparison.GreaterThan;
}
function getDiagnosticFileName(diagnostic: Diagnostic): string {
return diagnostic.file ? diagnostic.file.fileName : undefined;
}
@@ -756,6 +779,49 @@ namespace ts {
return path1 + directorySeparator + path2;
}
/**
* Removes a trailing directory separator from a path.
* @param path The path.
*/
export function removeTrailingDirectorySeparator(path: string) {
if (path.charAt(path.length - 1) === directorySeparator) {
return path.substr(0, path.length - 1);
}
return path;
}
/**
* Adds a trailing directory separator to a path, if it does not already have one.
* @param path The path.
*/
export function ensureTrailingDirectorySeparator(path: string) {
if (path.charAt(path.length - 1) !== directorySeparator) {
return path + directorySeparator;
}
return path;
}
export function comparePaths(a: string, b: string, currentDirectory: string, ignoreCase?: boolean) {
if (a === b) return Comparison.EqualTo;
if (a === undefined) return Comparison.LessThan;
if (b === undefined) return Comparison.GreaterThan;
a = removeTrailingDirectorySeparator(a);
b = removeTrailingDirectorySeparator(b);
const aComponents = getNormalizedPathComponents(a, currentDirectory);
const bComponents = getNormalizedPathComponents(b, currentDirectory);
const sharedLength = Math.min(aComponents.length, bComponents.length);
for (let i = 0; i < sharedLength; ++i) {
const result = compareStrings(aComponents[i], bComponents[i], ignoreCase);
if (result !== Comparison.EqualTo) {
return result;
}
}
return compareValues(aComponents.length, bComponents.length);
}
export function fileExtensionIs(path: string, extension: string): boolean {
const pathLen = path.length;
const extLen = extension.length;
@@ -794,6 +860,10 @@ namespace ts {
return path;
}
export function changeExtension<T extends string | Path>(path: T, newExtension: string): T {
return <T>(removeFileExtension(path) + newExtension);
}
const backslashOrDoubleQuote = /[\"\\]/g;
const escapedCharsRegExp = /[\u0000-\u001f\t\v\f\b\r\n\u2028\u2029\u0085]/g;
const escapedCharsMap: Map<string> = {
+4
View File
@@ -2020,6 +2020,10 @@
"category": "Error",
"code": 5009
},
"File specification cannot contain multiple recursive directory wildcards ('**'): '{0}'.": {
"category": "Error",
"code": 5011
},
"Cannot read file '{0}': {1}": {
"category": "Error",
"code": 5012
+43 -3
View File
@@ -17,6 +17,8 @@ namespace ts {
getExecutingFilePath(): string;
getCurrentDirectory(): string;
readDirectory(path: string, extension?: string, exclude?: string[]): string[];
readFileNames(path: string): string[];
readDirectoryNames(path: string): string[];
getMemoryUsage?(): number;
exit(exitCode?: number): void;
}
@@ -155,6 +157,16 @@ namespace ts {
}
}
function readFileNames(path: string): string[] {
const folder = fso.GetFolder(path || ".");
return getNames(folder.files);
}
function readDirectoryNames(path: string): string[] {
const folder = fso.GetFolder(path || ".");
return getNames(folder.directories);
}
return {
args,
newLine: "\r\n",
@@ -185,6 +197,8 @@ namespace ts {
return new ActiveXObject("WScript.Shell").CurrentDirectory;
},
readDirectory,
readFileNames,
readDirectoryNames,
exit(exitCode?: number): void {
try {
WScript.Quit(exitCode);
@@ -281,7 +295,7 @@ namespace ts {
// REVIEW: for now this implementation uses polling.
// The advantage of polling is that it works reliably
// on all os and with network mounted files.
// For 90 referenced files, the average time to detect
// For 90 referenced files, the average time to detect
// changes is 2*msInterval (by default 5 seconds).
// The overhead of this is .04 percent (1/2500) with
// average pause of < 1 millisecond (and max
@@ -381,6 +395,30 @@ namespace ts {
}
}
function readFileNames(path: string): string[] {
const entries = _fs.readdirSync(path || ".");
const files: string[] = [];
for (const entry of entries) {
const stat = _fs.statSync(combinePaths(path, entry));
if (stat.isFile()) {
files.push(entry);
}
}
return files.sort();
}
function readDirectoryNames(path: string): string[] {
const entries = _fs.readdirSync(path || ".");
const directories: string[] = [];
for (const entry of entries) {
const stat = _fs.statSync(combinePaths(path, entry));
if (stat.isDirectory()) {
directories.push(entry);
}
}
return directories.sort();
}
return {
args: process.argv.slice(2),
newLine: _os.EOL,
@@ -406,7 +444,7 @@ namespace ts {
};
},
watchDirectory: (path, callback, recursive) => {
// Node 4.0 `fs.watch` function supports the "recursive" option on both OSX and Windows
// Node 4.0 `fs.watch` function supports the "recursive" option on both OSX and Windows
// (ref: https://github.com/nodejs/node/pull/2649 and https://github.com/Microsoft/TypeScript/issues/4643)
return _fs.watch(
path,
@@ -426,7 +464,7 @@ namespace ts {
return _path.resolve(path);
},
fileExists(path: string): boolean {
return _fs.existsSync(path);
return _fs.existsSync(path) && _fs.statSync(path).isFile();
},
directoryExists(path: string) {
return _fs.existsSync(path) && _fs.statSync(path).isDirectory();
@@ -443,6 +481,8 @@ namespace ts {
return process.cwd();
},
readDirectory,
readFileNames,
readDirectoryNames,
getMemoryUsage() {
if (global.gc) {
global.gc();
+21
View File
@@ -14,6 +14,7 @@ namespace ts {
remove(fileName: Path): void;
forEachValue(f: (key: Path, v: T) => void): void;
reduce<U>(f: (memo: U, value: T, key: Path) => U, initial: U): U;
clear(): void;
}
@@ -1582,7 +1583,27 @@ namespace ts {
}
export interface ParseConfigHost {
useCaseSensitiveFileNames: boolean;
readDirectory(rootDir: string, extension: string, exclude: string[]): string[];
/**
* Gets a value indicating whether the specified path exists.
* @param path The path to test.
*/
fileExists(path: string): boolean;
/**
* Reads the files names in the directory.
* @param rootDir The directory path.
*/
readFileNames(rootDir: string): string[];
/**
* Reads the directory names in the directory.
* @param rootDir The directory path.
*/
readDirectoryNames(rootDir: string): string[];
}
export interface WriteFileCallback {
+3
View File
@@ -167,6 +167,9 @@ declare module chai {
module assert {
function equal(actual: any, expected: any, message?: string): void;
function notEqual(actual: any, expected: any, message?: string): void;
function deepEqual(actual: any, expected: any, message?: string): void;
function notDeepEqual(actual: any, expected: any, message?: string): void;
function lengthOf(object: any[], length: number, message?: string): void;
function isTrue(value: any, message?: string): void;
function isFalse(value: any, message?: string): void;
function isNull(value: any, message?: string): void;
+21 -7
View File
@@ -1,7 +1,7 @@
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -282,7 +282,7 @@ namespace Utils {
break;
case "parserContextFlags":
// Clear the flag that are produced by aggregating child values.. That is ephemeral
// Clear the flag that are produced by aggregating child values.. That is ephemeral
// data we don't care about in the dump. We only care what the parser set directly
// on the ast.
let value = n.parserContextFlags & ts.ParserContextFlags.ParserGeneratedFlags;
@@ -356,7 +356,7 @@ namespace Utils {
assert.equal(node1.kind, node2.kind, "node1.kind !== node2.kind");
assert.equal(node1.flags, node2.flags, "node1.flags !== node2.flags");
// call this on both nodes to ensure all propagated flags have been set (and thus can be
// call this on both nodes to ensure all propagated flags have been set (and thus can be
// compared).
assert.equal(ts.containsParseError(node1), ts.containsParseError(node2));
assert.equal(node1.parserContextFlags, node2.parserContextFlags, "node1.parserContextFlags !== node2.parserContextFlags");
@@ -436,6 +436,8 @@ namespace Harness {
getExecutingFilePath(): string;
exit(exitCode?: number): void;
readDirectory(path: string, extension?: string, exclude?: string[]): string[];
readDirectoryNames(path: string): string[];
readFileNames(path: string): string[];
}
export var IO: IO;
@@ -474,6 +476,8 @@ namespace Harness {
export const fileExists: typeof IO.fileExists = fso.FileExists;
export const log: typeof IO.log = global.WScript && global.WScript.StdOut.WriteLine;
export const readDirectory: typeof IO.readDirectory = (path, extension, exclude) => ts.sys.readDirectory(path, extension, exclude);
export const readDirectoryNames: typeof IO.readDirectoryNames = path => ts.sys.readDirectoryNames(path);
export const readFileNames: typeof IO.readFileNames = path => ts.sys.readFileNames(path);
export function createDirectory(path: string) {
if (directoryExists(path)) {
@@ -544,6 +548,8 @@ namespace Harness {
export const log: typeof IO.log = s => console.log(s);
export const readDirectory: typeof IO.readDirectory = (path, extension, exclude) => ts.sys.readDirectory(path, extension, exclude);
export const readDirectoryNames: typeof IO.readDirectoryNames = path => ts.sys.readDirectoryNames(path);
export const readFileNames: typeof IO.readFileNames = path => ts.sys.readFileNames(path);
export function createDirectory(path: string) {
if (!directoryExists(path)) {
@@ -752,6 +758,14 @@ namespace Harness {
export function readDirectory(path: string, extension?: string, exclude?: string[]) {
return listFiles(path).filter(f => !extension || ts.fileExtensionIs(f, extension));
}
export function readDirectoryNames(path: string): string[] {
return [];
}
export function readFileNames(path: string) {
return readDirectory(path);
}
}
}
@@ -777,7 +791,7 @@ namespace Harness {
(emittedFile: string, emittedLine: number, emittedColumn: number, sourceFile: string, sourceLine: number, sourceColumn: number, sourceName: string): void;
}
// Settings
// Settings
export let userSpecifiedRoot = "";
export let lightMode = false;
@@ -816,7 +830,7 @@ namespace Harness {
fileName: string,
sourceText: string,
languageVersion: ts.ScriptTarget) {
// We'll only assert invariants outside of light mode.
// We'll only assert invariants outside of light mode.
const shouldAssertInvariants = !Harness.lightMode;
// Only set the parent nodes if we're asserting invariants. We don't need them otherwise.
@@ -911,7 +925,7 @@ namespace Harness {
libFiles?: string;
}
// Additional options not already in ts.optionDeclarations
// Additional options not already in ts.optionDeclarations
const harnessOptionDeclarations: ts.CommandLineOption[] = [
{ name: "allowNonTsExtensions", type: "boolean" },
{ name: "useCaseSensitiveFileNames", type: "boolean" },
@@ -1149,7 +1163,7 @@ namespace Harness {
errLines.forEach(e => outputLines.push(e));
// do not count errors from lib.d.ts here, they are computed separately as numLibraryDiagnostics
// if lib.d.ts is explicitly included in input files and there are some errors in it (i.e. because of duplicate identifiers)
// if lib.d.ts is explicitly included in input files and there are some errors in it (i.e. because of duplicate identifiers)
// then they will be added twice thus triggering 'total errors' assertion with condition
// 'totalErrorsReportedInNonLibraryFiles + numLibraryDiagnostics + numTest262HarnessDiagnostics, diagnostics.length
+14 -1
View File
@@ -259,6 +259,12 @@ namespace Harness.LanguageService {
readDirectory(rootDir: string, extension: string): string {
throw new Error("NYI");
}
readDirectoryNames(path: string): string {
throw new Error("Not implemented.");
}
readFileNames(path: string): string {
throw new Error("Not implemented.");
}
fileExists(fileName: string) { return this.getScriptInfo(fileName) !== undefined; }
readFile(fileName: string) {
const snapshot = this.nativeHost.getScriptSnapshot(fileName);
@@ -572,6 +578,14 @@ namespace Harness.LanguageService {
throw new Error("Not implemented Yet.");
}
readDirectoryNames(path: string): string[] {
throw new Error("Not implemented.");
}
readFileNames(path: string): string[] {
throw new Error("Not implemented.");
}
watchFile(fileName: string, callback: (fileName: string) => void): ts.FileWatcher {
return { close() { } };
}
@@ -644,4 +658,3 @@ namespace Harness.LanguageService {
getPreProcessedFileInfo(fileName: string, fileContents: string): ts.PreProcessedFileInfo { throw new Error("getPreProcessedFileInfo is not available using the server interface."); }
}
}
+17 -2
View File
@@ -210,7 +210,14 @@ class ProjectRunner extends RunnerBase {
}
const configObject = result.config;
const configParseResult = ts.parseJsonConfigFileContent(configObject, { readDirectory }, ts.getDirectoryPath(configFileName), compilerOptions);
const configParseHost: ts.ParseConfigHost = {
useCaseSensitiveFileNames: Harness.IO.useCaseSensitiveFileNames(),
fileExists,
readDirectory,
readDirectoryNames,
readFileNames
};
const configParseResult = ts.parseJsonConfigFileContent(configObject, configParseHost, ts.getDirectoryPath(configFileName), compilerOptions);
if (configParseResult.errors.length > 0) {
return {
moduleKind,
@@ -237,7 +244,7 @@ class ProjectRunner extends RunnerBase {
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
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.Map<ts.CommandLineOption> = {};
@@ -278,6 +285,14 @@ class ProjectRunner extends RunnerBase {
return result;
}
function readDirectoryNames(path: string) {
return Harness.IO.readDirectoryNames(getFileNameInTheProjectTest(path));
}
function readFileNames(path: string) {
return Harness.IO.readFileNames(getFileNameInTheProjectTest(path));
}
function fileExists(fileName: string): boolean {
return Harness.IO.fileExists(getFileNameInTheProjectTest(fileName));
}
+8 -1
View File
@@ -76,7 +76,14 @@ namespace RWC {
if (tsconfigFile) {
const tsconfigFileContents = getHarnessCompilerInputUnit(tsconfigFile.path);
const parsedTsconfigFileContents = ts.parseConfigFileTextToJson(tsconfigFile.path, tsconfigFileContents.content);
const configParseResult = ts.parseJsonConfigFileContent(parsedTsconfigFileContents.config, Harness.IO, ts.getDirectoryPath(tsconfigFile.path));
const configParseHost: ts.ParseConfigHost = {
useCaseSensitiveFileNames: Harness.IO.useCaseSensitiveFileNames(),
fileExists: Harness.IO.fileExists,
readDirectory: Harness.IO.readDirectory,
readDirectoryNames: Harness.IO.readDirectoryNames,
readFileNames: Harness.IO.readFileNames,
};
const configParseResult = ts.parseJsonConfigFileContent(parsedTsconfigFileContents.config, configParseHost, ts.getDirectoryPath(tsconfigFile.path));
fileNames = configParseResult.fileNames;
opts.options = ts.extend(opts.options, configParseResult.options);
}
+53 -9
View File
@@ -60,7 +60,7 @@ namespace ts {
getNewLine?(): string;
getProjectVersion?(): string;
useCaseSensitiveFileNames?(): boolean;
getModuleResolutionsForFile?(fileName: string): string;
}
@@ -73,6 +73,9 @@ namespace ts {
* when enumerating the directory.
*/
readDirectory(rootDir: string, extension: string, exclude?: string): string;
readDirectoryNames?(rootDir: string): string;
readFileNames?(rootDir: string): string;
useCaseSensitiveFileNames?: boolean;
}
///
@@ -272,12 +275,12 @@ namespace ts {
private files: string[];
private loggingEnabled = false;
private tracingEnabled = false;
public resolveModuleNames: (moduleName: string[], containingFile: string) => ResolvedModule[];
constructor(private shimHost: LanguageServiceShimHost) {
// if shimHost is a COM object then property check will become method call with no arguments.
// 'in' does not have this effect.
// 'in' does not have this effect.
if ("getModuleResolutionsForFile" in this.shimHost) {
this.resolveModuleNames = (moduleNames: string[], containingFile: string) => {
let resolutionsInFile = <Map<string>>JSON.parse(this.shimHost.getModuleResolutionsForFile(containingFile));
@@ -407,7 +410,18 @@ namespace ts {
export class CoreServicesShimHostAdapter implements ParseConfigHost {
public useCaseSensitiveFileNames: boolean;
constructor(private shimHost: CoreServicesShimHost) {
if (typeof shimHost.useCaseSensitiveFileNames === "boolean") {
this.useCaseSensitiveFileNames = shimHost.useCaseSensitiveFileNames;
}
else if (sys) {
this.useCaseSensitiveFileNames = sys.useCaseSensitiveFileNames;
}
else {
this.useCaseSensitiveFileNames = true;
}
}
public readDirectory(rootDir: string, extension: string, exclude: string[]): string[] {
@@ -424,11 +438,41 @@ namespace ts {
}
return JSON.parse(encoded);
}
public readDirectoryNames(path: string): string[] {
if (this.shimHost.readDirectory) {
const encoded = this.shimHost.readDirectoryNames(path);
return JSON.parse(encoded);
}
if (sys) {
path = normalizePath(path);
path = ensureTrailingDirectorySeparator(path);
return sys.readDirectoryNames(path);
}
return [];
}
public readFileNames(path: string): string[] {
if (this.shimHost.readFileNames) {
const encoded = this.shimHost.readFileNames(path);
return JSON.parse(encoded);
}
if (sys) {
path = normalizePath(path);
path = ensureTrailingDirectorySeparator(path);
return sys.readFileNames(path);
}
return [];
}
public fileExists(fileName: string): boolean {
return this.shimHost.fileExists(fileName);
}
public readFile(fileName: string): string {
return this.shimHost.readFile(fileName);
}
@@ -940,7 +984,7 @@ namespace ts {
private forwardJSONCall(actionDescription: string, action: () => any): any {
return forwardJSONCall(this.logger, actionDescription, action, this.logPerformance);
}
public resolveModuleName(fileName: string, moduleName: string, compilerOptionsJson: string): string {
return this.forwardJSONCall(`resolveModuleName('${fileName}')`, () => {
let compilerOptions = <CompilerOptions>JSON.parse(compilerOptionsJson);
@@ -949,14 +993,14 @@ namespace ts {
resolvedFileName: result.resolvedModule ? result.resolvedModule.resolvedFileName: undefined,
failedLookupLocations: result.failedLookupLocations
};
});
});
}
public getPreProcessedFileInfo(fileName: string, sourceTextSnapshot: IScriptSnapshot): string {
return this.forwardJSONCall(
"getPreProcessedFileInfo('" + fileName + "')",
() => {
// for now treat files as JavaScript
// for now treat files as JavaScript
var result = preProcessFile(sourceTextSnapshot.getText(0, sourceTextSnapshot.getLength()), /* readImportFiles */ true, /* detectJavaScriptImports */ true);
var convertResult = {
referencedFiles: <IFileReference[]>[],
+24 -22
View File
@@ -39,6 +39,8 @@ module ts {
readDirectory: (path: string, extension?: string, exclude?: string[]): string[] => {
throw new Error("NYI");
},
readDirectoryNames: (path: string): string[] => { throw new Error("NYI"); },
readFileNames: (path: string): string[] => { throw new Error("NYI"); },
exit: (exitCode?: number) => {
},
watchFile: (path, callback) => {
@@ -69,8 +71,8 @@ module ts {
let projectService = new server.ProjectService(serverHost, logger);
let rootScriptInfo = projectService.openFile(rootFile, /* openedByClient */true);
let project = projectService.createInferredProject(rootScriptInfo);
project.setProjectOptions( {files: [rootScriptInfo.fileName], compilerOptions: {module: ts.ModuleKind.AMD} } );
return {
project.setProjectOptions( {files: [rootScriptInfo.fileName], compilerOptions: {module: ts.ModuleKind.AMD} } );
return {
project,
rootScriptInfo
};
@@ -87,22 +89,22 @@ module ts {
name: "c:/f1.ts",
content: `foo()`
};
let serverHost = createDefaultServerHost({ [root.name]: root, [imported.name]: imported });
let { project, rootScriptInfo } = createProject(root.name, serverHost);
// ensure that imported file was found
let diags = project.compilerService.languageService.getSemanticDiagnostics(imported.name);
assert.equal(diags.length, 1);
let originalFileExists = serverHost.fileExists;
{
// patch fileExists to make sure that disk is not touched
serverHost.fileExists = (fileName): boolean => {
assert.isTrue(false, "fileExists should not be called");
assert.isTrue(false, "fileExists should not be called");
return false;
};
let newContent = `import {x} from "f1"
var x: string = 1;`;
rootScriptInfo.editContent(0, rootScriptInfo.content.length, newContent);
@@ -123,7 +125,7 @@ module ts {
};
let newContent = `import {x} from "f2"`;
rootScriptInfo.editContent(0, rootScriptInfo.content.length, newContent);
try {
// trigger synchronization to make sure that LSHost will try to find 'f2' module on disk
project.compilerService.languageService.getSemanticDiagnostics(imported.name);
@@ -132,7 +134,7 @@ module ts {
catch(e) {
assert.isTrue(e.message.indexOf(`Could not find file: '${imported.name}'.`) === 0);
}
assert.isTrue(fileExistsIsCalled);
}
{
@@ -140,45 +142,45 @@ module ts {
serverHost.fileExists = (fileName): boolean => {
if (fileName === "lib.d.ts") {
return false;
}
}
fileExistsCalled = true;
assert.isTrue(fileName.indexOf('/f1.') !== -1);
return originalFileExists(fileName);
};
let newContent = `import {x} from "f1"`;
rootScriptInfo.editContent(0, rootScriptInfo.content.length, newContent);
project.compilerService.languageService.getSemanticDiagnostics(imported.name);
assert.isTrue(fileExistsCalled);
// setting compiler options discards module resolution cache
fileExistsCalled = false;
let opts = ts.clone(project.projectOptions);
opts.compilerOptions = ts.clone(opts.compilerOptions);
opts.compilerOptions.target = ts.ScriptTarget.ES5;
project.setProjectOptions(opts);
project.compilerService.languageService.getSemanticDiagnostics(imported.name);
assert.isTrue(fileExistsCalled);
}
});
it("loads missing files from disk", () => {
let root: File = {
name: 'c:/foo.ts',
content: `import {x} from "bar"`
};
let imported: File = {
name: 'c:/bar.d.ts',
content: `export var y = 1`
};
};
let fileMap: Map<File> = { [root.name]: root };
let serverHost = createDefaultServerHost(fileMap);
let originalFileExists = serverHost.fileExists;
let fileExistsCalledForBar = false;
serverHost.fileExists = fileName => {
if (fileName === "lib.d.ts") {
@@ -187,22 +189,22 @@ module ts {
if (!fileExistsCalledForBar) {
fileExistsCalledForBar = fileName.indexOf("/bar.") !== -1;
}
return originalFileExists(fileName);
};
let { project, rootScriptInfo } = createProject(root.name, serverHost);
let diags = project.compilerService.languageService.getSemanticDiagnostics(root.name);
assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called");
assert.isTrue(diags.length === 1, "one diagnostic expected");
assert.isTrue(typeof diags[0].messageText === "string" && ((<string>diags[0].messageText).indexOf("Cannot find module") === 0), "should be 'cannot find module' message");
// assert that import will success once file appear on disk
fileMap[imported.name] = imported;
fileExistsCalledForBar = false;
rootScriptInfo.editContent(0, rootScriptInfo.content.length, `import {y} from "bar"`)
diags = project.compilerService.languageService.getSemanticDiagnostics(root.name);
assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called");
assert.isTrue(diags.length === 0);
+249
View File
@@ -0,0 +1,249 @@
/// <reference path="..\..\..\src\harness\external\mocha.d.ts" />
/// <reference path="..\..\..\src\harness\harness.ts" />
describe("expandFiles", () => {
it("fail", () => {
assert.isTrue(false, "just checking");
});
const basePath = "c:/dev/";
const caseInsensitiveHost = createMockParseConfigHost(
basePath,
/*files*/ [
"c:/dev/a.ts",
"c:/dev/a.d.ts",
"c:/dev/a.js",
"c:/dev/b.ts",
"c:/dev/b.js",
"c:/dev/c.d.ts",
"c:/dev/z/a.ts",
"c:/dev/z/abz.ts",
"c:/dev/z/aba.ts",
"c:/dev/z/b.ts",
"c:/dev/z/bbz.ts",
"c:/dev/z/bba.ts",
"c:/dev/x/a.ts",
"c:/dev/x/aa.ts",
"c:/dev/x/b.ts",
"c:/dev/x/y/a.ts",
"c:/dev/x/y/b.ts"
],
/*ignoreCase*/ true);
const caseSensitiveHost = createMockParseConfigHost(
basePath,
/*files*/ [
"c:/dev/a.ts",
"c:/dev/a.d.ts",
"c:/dev/a.js",
"c:/dev/b.ts",
"c:/dev/b.js",
"c:/dev/A.ts",
"c:/dev/B.ts",
"c:/dev/c.d.ts",
"c:/dev/z/a.ts",
"c:/dev/z/abz.ts",
"c:/dev/z/aba.ts",
"c:/dev/z/b.ts",
"c:/dev/z/bbz.ts",
"c:/dev/z/bba.ts",
"c:/dev/x/a.ts",
"c:/dev/x/b.ts",
"c:/dev/x/y/a.ts",
"c:/dev/x/y/b.ts",
],
/*ignoreCase*/ false);
const expect = _chai.expect;
describe("with literal file list", () => {
it("without exclusions", () => {
const fileNames = ["a.ts", "b.ts"];
const results = ts.expandFiles(fileNames, /*includeSpecs*/ undefined, /*excludeSpecs*/ undefined, basePath, {}, caseInsensitiveHost);
assert.deepEqual(results, ["c:/dev/a.ts", "c:/dev/b.ts"]);
});
it("missing files are still present", () => {
const fileNames = ["z.ts", "x.ts"];
const results = ts.expandFiles(fileNames, /*includeSpecs*/ undefined, /*excludeSpecs*/ undefined, basePath, {}, caseInsensitiveHost);
assert.deepEqual(results, ["c:/dev/z.ts", "c:/dev/x.ts"]);
});
it("are not removed due to excludes", () => {
const fileNames = ["a.ts", "b.ts"];
const excludeSpecs = ["b.ts"];
const results = ts.expandFiles(fileNames, /*includeSpecs*/ undefined, excludeSpecs, basePath, {}, caseInsensitiveHost);
assert.deepEqual(results, ["c:/dev/a.ts", "c:/dev/b.ts"]);
});
});
describe("with literal include list", () => {
it("without exclusions", () => {
const includeSpecs = ["a.ts", "b.ts"];
const results = ts.expandFiles(/*fileNames*/ undefined, includeSpecs, /*excludeSpecs*/ undefined, basePath, {}, caseInsensitiveHost);
assert.deepEqual(results, ["c:/dev/a.ts", "c:/dev/b.ts"]);
});
it("with non .ts file extensions are excluded", () => {
const includeSpecs = ["a.js", "b.js"];
const results = ts.expandFiles(/*fileNames*/ undefined, includeSpecs, /*excludeSpecs*/ undefined, basePath, {}, caseInsensitiveHost);
assert.deepEqual(results, []);
});
it("with missing files are excluded", () => {
const includeSpecs = ["z.ts", "x.ts"];
const results = ts.expandFiles(/*fileNames*/ undefined, includeSpecs, /*excludeSpecs*/ undefined, basePath, {}, caseInsensitiveHost);
assert.deepEqual(results, []);
});
it("with literal excludes", () => {
const includeSpecs = ["a.ts", "b.ts"];
const excludeSpecs = ["b.ts"];
const results = ts.expandFiles(/*fileNames*/ undefined, includeSpecs, excludeSpecs, basePath, {}, caseInsensitiveHost);
assert.deepEqual(results, ["c:/dev/a.ts"]);
});
it("with wildcard excludes", () => {
const includeSpecs = ["a.ts", "b.ts", "z/a.ts", "z/abz.ts", "z/aba.ts", "x/b.ts"];
const excludeSpecs = ["*.ts", "z/??z.ts", "*/b.ts"];
const results = ts.expandFiles(/*fileNames*/ undefined, includeSpecs, excludeSpecs, basePath, {}, caseInsensitiveHost);
assert.deepEqual(results, ["c:/dev/z/a.ts", "c:/dev/z/aba.ts"]);
});
it("with recursive excludes", () => {
const includeSpecs = ["a.ts", "b.ts", "x/a.ts", "x/b.ts", "x/y/a.ts", "x/y/b.ts"];
const excludeSpecs = ["**/b.ts"];
const results = ts.expandFiles(/*fileNames*/ undefined, includeSpecs, excludeSpecs, basePath, {}, caseInsensitiveHost);
assert.deepEqual(results, ["c:/dev/a.ts", "c:/dev/x/a.ts", "c:/dev/x/y/a.ts"]);
});
it("with case sensitive exclude", () => {
const includeSpecs = ["B.ts"];
const excludeSpecs = ["**/b.ts"];
const results = ts.expandFiles(/*fileNames*/ undefined, includeSpecs, excludeSpecs, basePath, {}, caseSensitiveHost);
assert.deepEqual(results, ["c:/dev/B.ts"]);
});
});
describe("with wildcard include list", () => {
it("same named declarations are excluded", () => {
const includeSpecs = ["*.ts"];
const results = ts.expandFiles(/*fileNames*/ undefined, includeSpecs, /*excludeSpecs*/ undefined, basePath, {}, caseInsensitiveHost);
assert.deepEqual(results, ["c:/dev/a.ts", "c:/dev/b.ts", "c:/dev/c.d.ts"]);
});
it("`*` matches only ts files", () => {
const includeSpecs = ["*"];
const results = ts.expandFiles(/*fileNames*/ undefined, includeSpecs, /*excludeSpecs*/ undefined, basePath, {}, caseInsensitiveHost);
assert.deepEqual(results, ["c:/dev/a.ts", "c:/dev/b.ts", "c:/dev/c.d.ts"]);
});
it("`?` matches only a single character", () => {
const includeSpecs = ["x/?.ts"];
const results = ts.expandFiles(/*fileNames*/ undefined, includeSpecs, /*excludeSpecs*/ undefined, basePath, {}, caseInsensitiveHost);
assert.deepEqual(results, ["c:/dev/x/a.ts", "c:/dev/x/b.ts"]);
});
it("with recursive directory", () => {
const includeSpecs = ["**/a.ts"];
const results = ts.expandFiles(/*fileNames*/ undefined, includeSpecs, /*excludeSpecs*/ undefined, basePath, {}, caseInsensitiveHost);
assert.deepEqual(results, ["c:/dev/a.ts", "c:/dev/x/a.ts", "c:/dev/x/y/a.ts", "c:/dev/z/a.ts"]);
});
it("case sensitive", () => {
const includeSpecs = ["**/A.ts"];
const results = ts.expandFiles(/*fileNames*/ undefined, includeSpecs, /*excludeSpecs*/ undefined, basePath, {}, caseSensitiveHost);
assert.deepEqual(results, ["c:/dev/A.ts"]);
});
it("with missing files are excluded", () => {
const includeSpecs = ["*/z.ts"];
const results = ts.expandFiles(/*fileNames*/ undefined, includeSpecs, /*excludeSpecs*/ undefined, basePath, {}, caseInsensitiveHost);
assert.deepEqual(results, []);
});
it("always include literal files", () => {
const fileNames = ["a.ts"];
const includeSpecs = ["*/z.ts"];
const excludeSpecs = ["**/a.ts"];
const results = ts.expandFiles(fileNames, includeSpecs, excludeSpecs, basePath, {}, caseInsensitiveHost);
assert.deepEqual(results, ["c:/dev/a.ts"]);
});
});
function createMockParseConfigHost(basePath: string, files: string[], ignoreCase: boolean): ts.ParseConfigHost {
const fileSet: ts.Map<string> = {};
const directorySet: ts.Map<{ files: ts.Map<string>; directories: ts.Map<string>; }> = {};
files.sort((a, b) => ts.comparePaths(a, b, basePath, ignoreCase));
for (const file of files) {
addFile(ts.getNormalizedAbsolutePath(file, basePath));
}
return {
useCaseSensitiveFileNames: !ignoreCase,
fileExists,
readDirectory,
readFileNames,
readDirectoryNames
};
function fileExists(path: string): boolean {
const fileKey = ignoreCase ? path.toLowerCase() : path;
return ts.hasProperty(fileSet, fileKey);
}
function directoryExists(path: string): boolean {
const directoryKey = ignoreCase ? path.toLowerCase() : path;
return ts.hasProperty(directorySet, directoryKey);
}
function readDirectory(rootDir: string, extension?: string, exclude?: string[]): string[] {
throw new Error("Not implemented");
}
function readFileNames(path: string) {
const files = getDirectoryEntry(path).files;
const result: string[] = [];
ts.forEachKey(files, key => { result.push(key); });
result.sort((a, b) => ts.compareStrings(a, b, ignoreCase));
return result;
}
function readDirectoryNames(path: string) {
const directories = getDirectoryEntry(path).directories;
const result: string[] = [];
ts.forEachKey(directories, key => { result.push(key); });
result.sort((a, b) => ts.compareStrings(a, b, ignoreCase));
return result;
}
function getDirectoryEntry(path: string) {
path = ts.getNormalizedAbsolutePath(path, basePath);
path = ts.removeTrailingDirectorySeparator(path);
const directoryKey = ignoreCase ? path.toLowerCase() : path;
return ts.getProperty(directorySet, directoryKey);
}
function addFile(file: string) {
const fileKey = ignoreCase ? file.toLowerCase() : file;
if (!ts.hasProperty(fileSet, fileKey)) {
fileSet[fileKey] = file;
const name = ts.getBaseFileName(file);
const parent = ts.getDirectoryPath(file);
addToDirectory(parent, name, "file");
}
}
function addDirectory(directory: string) {
directory = ts.removeTrailingDirectorySeparator(directory);
const directoryKey = ignoreCase ? directory.toLowerCase() : directory;
if (!ts.hasProperty(directorySet, directoryKey)) {
directorySet[directoryKey] = { files: {}, directories: {} };
const name = ts.getBaseFileName(directory);
const parent = ts.getDirectoryPath(directory);
if (parent !== directory) {
addToDirectory(parent, name, "directory");
}
}
}
function addToDirectory(directory: string, entry: string, type: "file" | "directory") {
addDirectory(directory);
directory = ts.removeTrailingDirectorySeparator(directory);
const directoryKey = ignoreCase ? directory.toLowerCase() : directory;
const entryKey = ignoreCase ? entry.toLowerCase() : entry;
const directoryEntry = directorySet[directoryKey];
const entries = type === "file" ? directoryEntry.files : directoryEntry.directories;
if (!ts.hasProperty(entries, entryKey)) {
entries[entryKey] = entry;
}
}
}
});
+16 -14
View File
@@ -16,6 +16,8 @@ namespace ts.server {
getExecutingFilePath(): string { return void 0; },
getCurrentDirectory(): string { return void 0; },
readDirectory(): string[] { return []; },
readDirectoryNames(): string[] { return []; },
readFileNames(): string[] { return []; },
exit(): void {}
};
const mockLogger: Logger = {
@@ -28,7 +30,7 @@ namespace ts.server {
endGroup(): void {},
msg(s: string, type?: string): void {},
};
describe("the Session class", () => {
let session: Session;
let lastSent: protocol.Message;
@@ -204,7 +206,7 @@ namespace ts.server {
.to.throw(`Protocol handler already exists for command "${command}"`);
});
});
describe("event", () => {
it("can format event responses and send them", () => {
const evt = "notify-test";
@@ -315,7 +317,7 @@ namespace ts.server {
responseRequired: true
}));
}
send(msg: protocol.Message) {
this.client.handle(msg);
}
@@ -323,7 +325,7 @@ namespace ts.server {
enqueue(msg: protocol.Request) {
this.queue.unshift(msg);
}
handleRequest(msg: protocol.Request) {
let response: protocol.Response;
try {
@@ -345,7 +347,7 @@ namespace ts.server {
}
}
}
class InProcClient {
private server: InProcSession;
private seq = 0;
@@ -379,7 +381,7 @@ namespace ts.server {
connect(session: InProcSession): void {
this.server = session;
}
execute(command: string, args: any, callback: (resp: protocol.Response) => void): void {
if (!this.server) {
return;
@@ -394,7 +396,7 @@ namespace ts.server {
this.callbacks[this.seq] = callback;
}
};
it("can be constructed and respond to commands", (done) => {
const cli = new InProcClient();
const session = new InProcSession(cli);
@@ -402,23 +404,23 @@ namespace ts.server {
data: true
};
const toEvent = {
data: false
data: false
};
let responses = 0;
// Connect the client
cli.connect(session);
// Add an event handler
cli.on("testevent", (eventinfo) => {
expect(eventinfo).to.equal(toEvent);
responses++;
expect(responses).to.equal(1);
});
// Trigger said event from the server
session.event(toEvent, "testevent");
// Queue an echo command
cli.execute("echo", toEcho, (resp) => {
assert(resp.success, resp.message);
@@ -426,7 +428,7 @@ namespace ts.server {
expect(responses).to.equal(2);
expect(resp.body).to.deep.equal(toEcho);
});
// Queue a configure command
cli.execute("configure", {
hostInfo: "unit test",
@@ -436,10 +438,10 @@ namespace ts.server {
}, (resp) => {
assert(resp.success, resp.message);
responses++;
expect(responses).to.equal(3);
expect(responses).to.equal(3);
done();
});
// Consume the queue and trigger the callbacks
session.consumeQueue();
});