From f9ae3e4f2b529a2f59a0fd0f9dae1bb01c94f9a7 Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Thu, 3 Dec 2015 10:44:24 -0800 Subject: [PATCH] Initial support for globs in tsconfig.json --- src/compiler/commandLineParser.ts | 571 +++++++++++++++++- src/compiler/core.ts | 70 +++ src/compiler/diagnosticMessages.json | 4 + src/compiler/sys.ts | 46 +- src/compiler/types.ts | 21 + src/harness/external/chai.d.ts | 3 + src/harness/harness.ts | 28 +- src/harness/harnessLanguageService.ts | 15 +- src/harness/projectsRunner.ts | 19 +- src/harness/rwcRunner.ts | 9 +- src/services/shims.ts | 62 +- .../cases/unittests/cachingInServerLSHost.ts | 46 +- tests/cases/unittests/expandFiles.ts | 249 ++++++++ tests/cases/unittests/session.ts | 30 +- 14 files changed, 1086 insertions(+), 87 deletions(-) create mode 100644 tests/cases/unittests/expandFiles.ts diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 034b7022e82..f850ac00dba 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -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(json["files"], s => combinePaths(basePath, s)); + if (isArray(json["files"])) { + fileNames = json["files"]; } else { errors.push(createCompilerDiagnostic(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "files", "Array")); } } - else { - const filesSeen: Map = {}; - const exclude = json["exclude"] instanceof Array ? map(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 = 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 = 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(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, 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): 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): 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(); + } } diff --git a/src/compiler/core.ts b/src/compiler/core.ts index cae7bd82103..aae4b54b5c7 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -25,6 +25,7 @@ namespace ts { contains, remove, forEachValue: forEachValueInMap, + reduce, clear }; @@ -34,6 +35,10 @@ namespace ts { } } + function reduce(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(path: T, newExtension: string): T { + return (removeFileExtension(path) + newExtension); + } + const backslashOrDoubleQuote = /[\"\\]/g; const escapedCharsRegExp = /[\u0000-\u001f\t\v\f\b\r\n\u2028\u2029\u0085]/g; const escapedCharsMap: Map = { diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 0c19f6d8247..6a1d0e5ce65 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -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 diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 3a90ca42fa1..fc8e90ac38f 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -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(); diff --git a/src/compiler/types.ts b/src/compiler/types.ts index dbd0322aa18..ffd9ddc206d 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -14,6 +14,7 @@ namespace ts { remove(fileName: Path): void; forEachValue(f: (key: Path, v: T) => void): void; + reduce(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 { diff --git a/src/harness/external/chai.d.ts b/src/harness/external/chai.d.ts index 814de75e7b2..fc25980d3a0 100644 --- a/src/harness/external/chai.d.ts +++ b/src/harness/external/chai.d.ts @@ -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; diff --git a/src/harness/harness.ts b/src/harness/harness.ts index 15f3813e54d..d8ccc512343 100644 --- a/src/harness/harness.ts +++ b/src/harness/harness.ts @@ -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 diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index 3c7814df562..e12105e3393 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -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."); } } } - \ No newline at end of file diff --git a/src/harness/projectsRunner.ts b/src/harness/projectsRunner.ts index 52102663457..655c2f07c2b 100644 --- a/src/harness/projectsRunner.ts +++ b/src/harness/projectsRunner.ts @@ -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 = {}; @@ -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)); } diff --git a/src/harness/rwcRunner.ts b/src/harness/rwcRunner.ts index ce570a7d6ad..5b4e2a7a3d9 100644 --- a/src/harness/rwcRunner.ts +++ b/src/harness/rwcRunner.ts @@ -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); } diff --git a/src/services/shims.ts b/src/services/shims.ts index 6b656ea2738..4c35f5aaf1b 100644 --- a/src/services/shims.ts +++ b/src/services/shims.ts @@ -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 = >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 = 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: [], diff --git a/tests/cases/unittests/cachingInServerLSHost.ts b/tests/cases/unittests/cachingInServerLSHost.ts index 88b44a693b9..f0a138c27a1 100644 --- a/tests/cases/unittests/cachingInServerLSHost.ts +++ b/tests/cases/unittests/cachingInServerLSHost.ts @@ -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 = { [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" && ((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); diff --git a/tests/cases/unittests/expandFiles.ts b/tests/cases/unittests/expandFiles.ts new file mode 100644 index 00000000000..a9d12a73260 --- /dev/null +++ b/tests/cases/unittests/expandFiles.ts @@ -0,0 +1,249 @@ +/// +/// + +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 = {}; + const directorySet: ts.Map<{ files: ts.Map; directories: ts.Map; }> = {}; + + 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; + } + } + } +}); + diff --git a/tests/cases/unittests/session.ts b/tests/cases/unittests/session.ts index c1dfd508942..8e35baa3910 100644 --- a/tests/cases/unittests/session.ts +++ b/tests/cases/unittests/session.ts @@ -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(); });