From 30575dbd7cc4ac62c044e6d1eef77de3affd11ce Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Mon, 7 Dec 2015 14:58:13 -0800 Subject: [PATCH] Added caching, more tests --- Jakefile.js | 3 +- src/compiler/commandLineParser.ts | 484 +++++++++++++++++------ src/compiler/core.ts | 88 ++++- src/compiler/diagnosticMessages.json | 4 + src/compiler/types.ts | 20 +- src/harness/external/chai.d.ts | 6 +- src/harness/harnessLanguageService.ts | 1 + src/harness/projectsRunner.ts | 5 + src/harness/rwcRunner.ts | 1 + src/server/editorServices.ts | 57 ++- src/services/shims.ts | 13 + tests/cases/unittests/expandFiles.ts | 538 ++++++++++++++++++-------- 12 files changed, 928 insertions(+), 292 deletions(-) diff --git a/Jakefile.js b/Jakefile.js index 6243ce8ff97..a3f1d31f268 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -160,7 +160,8 @@ var harnessSources = harnessCoreSources.concat([ "reuseProgramStructure.ts", "cachingInServerLSHost.ts", "moduleResolution.ts", - "tsconfigParsing.ts" + "tsconfigParsing.ts", + "expandFiles.ts" ].map(function (f) { return path.join(unittestsDirectory, f); })).concat([ diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index f850ac00dba..6499ec6327f 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -488,13 +488,15 @@ namespace ts { const { options: optionsFromJsonConfigFile, errors } = convertCompilerOptionsFromJson(json["compilerOptions"], basePath); const options = extend(existingOptions, optionsFromJsonConfigFile); + const { fileNames, wildcardDirectories } = getFileNames(); return { options, - fileNames: getFileNames(), - errors + fileNames, + errors, + wildcardDirectories }; - function getFileNames(): string[] { + function getFileNames(): ExpandResult { let fileNames: string[]; if (hasProperty(json, "files")) { if (isArray(json["files"])) { @@ -526,17 +528,7 @@ namespace ts { } 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"); - } - } + includeSpecs = ["**/*"]; } return expandFiles(fileNames, includeSpecs, excludeSpecs, basePath, options, host, errors); @@ -593,11 +585,45 @@ namespace ts { // Simplified whitelist, forces escaping of any non-word (or digit), non-whitespace character. const reservedCharacterPattern = /[^\w\s]/g; - const enum ExpandResult { + const enum ExpansionState { Ok, Error } + interface ExpansionContext { + /** A pattern used to exclude a file specification. */ + excludePattern: RegExp; + /** Compiler options. */ + options: CompilerOptions; + /** The host used to resolve files and directories. */ + host: ParseConfigHost; + /** Errors to report. */ + errors: Diagnostic[]; + /** The set of literal files. */ + literalFiles: FileMap; + /** The set of files matching a wildcard. */ + wildcardFiles: FileMap; + /** Directories to be watched. */ + wildcardDirectories: FileMap; + /** Supported extensions. */ + supportedExtensions: string[]; + /** + * Path cache, used to reduce calls to the file system. `true` indicates a file exists, + * `false` indicates a file or directory does not exist. A DirectoryResult + * indicates the file and subdirectory names in a directory. */ + cache: FileMap; + } + + const enum FileSystemEntryKind { + File, + Directory + } + + interface DirectoryResult { + files?: string[]; + directories?: string[]; + } + /** * Expands an array of file specifications. * @@ -609,62 +635,118 @@ namespace ts { * @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[] { + export function expandFiles(fileNames: string[], includeSpecs: string[], excludeSpecs: string[], basePath: string, options: CompilerOptions, host: ParseConfigHost, errors?: Diagnostic[]): ExpandResult { basePath = normalizePath(basePath); basePath = removeTrailingDirectorySeparator(basePath); + // The exclude spec list is converted into a regular expression, which allows us to quickly + // test whether a file or directory should be excluded before recursively traversing the + // file system. const excludePattern = includeSpecs ? createExcludeRegularExpression(excludeSpecs, basePath, options, host, errors) : undefined; - const fileSet = createFileMap(host.useCaseSensitiveFileNames ? caseSensitiveKeyMapper : caseInsensitiveKeyMapper); + const keyMapper = host.useCaseSensitiveFileNames ? caseSensitiveKeyMapper : caseInsensitiveKeyMapper; - // include every literal file. + // Literal file names (provided via the "files" array in tsconfig.json) are stored in a + // file map with a possibly invariant key. We use this map later when when including + // wildcard paths. + const literalFiles = createFileMap(keyMapper); + + // Wildcard paths (provided via the "includes" array in tscofnig.json) are stored in a + // file map with a possibly invariant key. We use this map to store paths matched + // via wildcard, and to handle extension priority. + const wildcardFiles = createFileMap(keyMapper); + + // Wildcard directories (provided as part of a wildcard path) are stored in a + // file map that marks whether it was a regular wildcard match (with a `*` or `?` token), + // or a recursive directory. This information is used by filesystem watchers to monitor for + // new entries in these paths. + const wildcardDirectories = createFileMap(keyMapper); + + // To reduce the overhead of disk I/O (and marshalling to managed code when hosted in + // Visual Studio), file system queries are cached during the expansion session. + // If present, a cache entry can be one of three values: + // - A `false` value indicates the file or directory did not exist. + // - A `true` value indicates the path is a file and it exists. + // - An object value indicates the path is a directory and exists. The object may have + // zero, one, or both of the following properties: + // - A "files" array, which contains the file names in the directory. + // - A "directories" array, which contains the subdirectory names in the directory. + const cache = createFileMap(keyMapper); + + // Rather than requery this for each file and filespec, we querythe supported extensions + // once and store it on the expansion context. + const supportedExtensions = getSupportedExtensions(options); + + // The expansion context holds references to shared information for the various expansion + // operations to reduce the overhead of closures. + const context: ExpansionContext = { + options, + host, + errors, + excludePattern, + literalFiles, + wildcardFiles, + wildcardDirectories, + supportedExtensions, + cache + }; + + // Literal files are always included verbatim. An "include" or "exclude" specification cannot + // remove a literal file. if (fileNames) { for (const fileName of fileNames) { const path = toPath(fileName, basePath, caseSensitiveKeyMapper); - if (!fileSet.contains(path)) { - fileSet.set(path, path); + if (!literalFiles.contains(path)) { + literalFiles.set(path, path); } } } - // expand and include the provided files into the file set. + // Each "include" specification is expanded and matching files are added. if (includeSpecs) { for (let includeSpec of includeSpecs) { includeSpec = normalizePath(includeSpec); includeSpec = removeTrailingDirectorySeparator(includeSpec); - expandFileSpec(basePath, includeSpec, 0, excludePattern, options, host, errors, fileSet); + expandFileSpec(includeSpec, basePath, 0, context); } } - const output = fileSet.reduce(addFileToOutput, []); - return output; + return { + fileNames: wildcardFiles.reduce(addFileToOutput, literalFiles.reduce(addFileToOutput, [])), + wildcardDirectories: wildcardDirectories.reduce>(addDirectoryToOutput, {}), + }; } /** * Expands a file specification with wildcards. * - * @param basePath The directory to expand. * @param fileSpec The original file specification. + * @param basePath The directory to expand. This path must exist. * @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 context The expansion context. * @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 { + function expandFileSpec(fileSpec: string, basePath: Path, start: number, context: ExpansionContext, isExpandingRecursiveDirectory?: boolean): ExpansionState { + // A file specification must always point to a file. As a result, we always assume the + // path segment following the last directory separator points to a file. The only + // exception is when the final path segment is the recursive directory pattern "**", in + // which case we report an error. + const { host, options, errors, wildcardFiles, wildcardDirectories, excludePattern, cache, supportedExtensions } = context; + // Skip expansion if the base path matches an exclude pattern. - if (isExcludedPath(excludePattern, basePath)) { - return ExpandResult.Ok; + if (isExcludedPath(basePath, excludePattern)) { + return ExpansionState.Ok; } - // Find the offset of the next wildcard in the file specification + // Find the offset of the next wildcard in the file specification. If there are no more + // wildcards, we can include the file if it exists and isn't excluded. 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; + if (!isExcludedPath(path, excludePattern) && pathExists(path, FileSystemEntryKind.File, context)) { + includeFile(path, context, /*wildcardHasExtension*/ true); + } + + return ExpansionState.Ok; } // Find the last directory separator before the wildcard to get the leading path. @@ -672,11 +754,11 @@ namespace ts { 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)); + basePath = toPath(fileSpec.substring(start, offset), basePath, caseSensitiveKeyMapper); // Skip this wildcard path if the base path now matches an exclude pattern. - if (isExcludedPath(excludePattern, basePath)) { - return ExpandResult.Ok; + if (isExcludedPath(basePath, excludePattern) || !pathExists(basePath, FileSystemEntryKind.Directory, context)) { + return ExpansionState.Ok; } start = offset + 1; @@ -687,23 +769,34 @@ namespace ts { // 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; + return ExpansionState.Error; } + if (offset >= fileSpec.length) { + // If there is no file specification following the recursive directory pattern + // then we report an error as we cannot match any files. + if (errors) { + errors.push(createCompilerDiagnostic(Diagnostics.File_specification_cannot_end_in_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, fileSpec)); + } + + return ExpansionState.Error; + } + + // Keep track of the recursive wildcard directory + wildcardDirectories.set(basePath, WatchDirectoryFlags.Recursive); + // Expand the recursive directory pattern. - return expandRecursiveDirectory(basePath, fileSpec, offset + 1, excludePattern, options, host, errors, fileSet); + return expandRecursiveDirectory(fileSpec, basePath, offset + 1, context); + } + + if (!isExpandingRecursiveDirectory) { + wildcardDirectories.set(basePath, WatchDirectoryFlags.None); } // Match the entries in the directory against the wildcard pattern. @@ -712,103 +805,224 @@ namespace ts { // 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); + const wildcardHasExtension = fileSegmentHasExtension(fileSpec, start); + const fileNames = readDirectory(basePath, FileSystemEntryKind.File, context); + for (const fileName of fileNames) { + // Skip the file if it doesn't match the pattern. + if (!pattern.test(fileName)) { + continue; + } - // .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; - } + const path = toPath(fileName, basePath, caseSensitiveKeyMapper); - // 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; - } - } + // If we have excluded this path, we should skip the file. + if (isExcludedPath(path, excludePattern)) { + continue; + } - // This wildcard has no further directory to process, so include the file. - includeFile(path, excludePattern, options, host, fileSet); + // This wildcard has no further directory to process, so include the file. + includeFile(path, context, wildcardHasExtension); + } + } + else { + const directoryNames = readDirectory(basePath, FileSystemEntryKind.Directory, context); + for (const directoryName of directoryNames) { + if (pattern.test(directoryName)) { + const newBasePath = toPath(directoryName, basePath, caseSensitiveKeyMapper); + + // Expand the entries in this directory. + if (expandFileSpec(fileSpec, newBasePath, offset + 1, context, isExpandingRecursiveDirectory) === ExpansionState.Error) { + return ExpansionState.Error; } } } } - 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; + return ExpansionState.Ok; } /** * Expands a `**` recursive directory wildcard. * - * @param basePath The directory to recursively expand. * @param fileSpec The original file specification. + * @param basePath The directory to recursively expand. * @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 context The expansion context. */ - 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; + function expandRecursiveDirectory(fileSpec: string, basePath: Path, start: number, context: ExpansionContext): ExpansionState { + // Skip the directory if it is excluded. + if (isExcludedPath(basePath, context.excludePattern)) { + return ExpansionState.Ok; + } + + // Expand the non-recursive part of the file specification against the prefix path. + if (expandFileSpec(fileSpec, basePath, start, context, /*isExpandingRecursiveDirectory*/ true) === ExpansionState.Error) { + return ExpansionState.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; + const directoryNames = readDirectory(basePath, FileSystemEntryKind.Directory, context); + for (const directoryName of directoryNames) { + const newBasePath = toPath(directoryName, basePath, caseSensitiveKeyMapper); + if (expandRecursiveDirectory(fileSpec, newBasePath, start, context) === ExpansionState.Error) { + return ExpansionState.Error; } } - return ExpandResult.Ok; + return ExpansionState.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. + * @param context The expansion context. + * @param wildcardHasExtension A value indicating whether the wildcard supplied an explicit extension. */ - 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; - } + function includeFile(file: Path, context: ExpansionContext, wildcardHasExtension: boolean): void { + const { options, literalFiles, wildcardFiles, excludePattern, supportedExtensions } = context; // Ignore the file if it does not have a supported extension. - if (!options.allowNonTsExtensions && !isSupportedSourceFileName(file, options)) { + if ((!wildcardHasExtension || !options.allowNonTsExtensions) && !isSupportedSourceFileName(file, options)) { return; } - if (!fileSet.contains(file)) { - fileSet.set(file, file); + // If we have already included a literal or wildcard path with a + // higher priority extension, we should skip this file. + // + // This handles cases where we may encounter both .ts and + // .d.ts (or .js if "allowJs" is enabled) in the same + // directory when they are compilation outputs. + const extensionPriority = getExtensionPriority(file, supportedExtensions); + if (hasFileWithHigherPriorityExtension(file, extensionPriority, context)) { + return; + } + + // We may have included a wildcard path with a lower priority + // extension due to the user-defined order of entries in the + // "include" array. If there is a lower priority extension in the + // same directory, we should remove it. + removeWildcardFilesWithLowerPriorityExtension(file, extensionPriority, context); + + if (!literalFiles.contains(file) && !wildcardFiles.contains(file)) { + wildcardFiles.set(file, file); + } + } + + /** + * Tests whether a path exists and is a specific kind of item. Results are + * cached for performance. + * + * @param path The path to tests. + * @param kind The kind of file system entry to find. + * @param context The expansion context. + */ + function pathExists(path: Path, kind: FileSystemEntryKind, context: ExpansionContext) { + const { cache, host } = context; + const entry = cache.get(path); + if (entry === false) { + // If the entry is strictly `false` then the path doesn`t exist, regardless of its kind. + return false; + } + else if (entry === true) { + // If the entry is strictly `true` then a file exists at this path. + return kind === FileSystemEntryKind.File; + } + else if (typeof entry === "object") { + // If the entry is an object, then a directory exists at this path. + return kind === FileSystemEntryKind.Directory; + } + else { + // The entry does not exist in the cache, so we need to check the host. + if (kind === FileSystemEntryKind.File) { + const result = host.fileExists(path); + cache.set(path, result); + return result; + } + else if (kind === FileSystemEntryKind.Directory) { + const result = host.directoryExists(path); + cache.set(path, result ? {} : false); + return result; + } + } + + return false; + } + + /** + * Reads the contents of a directory for a specific kind of item. Results are + * cached for performance. + * + * @param basePath The path to the directory. The path must already exist. + * @param kind The kind of file system entry to find. + * @param context The expansion context. + */ + function readDirectory(basePath: Path, kind: FileSystemEntryKind, context: ExpansionContext) { + const { cache, host } = context; + + let entry = cache.get(basePath); + if (entry === undefined) { + entry = {}; + cache.set(basePath, entry); + } + + if (typeof entry === "object") { + if (kind === FileSystemEntryKind.File) { + if (entry.files === undefined) { + entry.files = host.readFileNames(basePath); + } + + return entry.files; + } + else if (kind === FileSystemEntryKind.Directory) { + if (entry.directories === undefined) { + entry.directories = host.readDirectoryNames(basePath); + } + + return entry.directories; + } + } + + return []; + } + + /** + * Determines whether a literal or wildcard file has already been included that has a higher + * extension priority. + * + * @param file The path to the file. + * @param extensionPriority The priority of the extension. + * @param context The expansion context. + */ + function hasFileWithHigherPriorityExtension(file: Path, extensionPriority: ExtensionPriority, context: ExpansionContext) { + const { literalFiles, wildcardFiles, supportedExtensions } = context; + const adjustedExtensionPriority = adjustExtensionPriority(extensionPriority); + for (let i = ExtensionPriority.Highest; i < adjustedExtensionPriority; ++i) { + const higherPriorityExtension = supportedExtensions[i]; + const higherPriorityPath = changeExtension(file, higherPriorityExtension); + if (literalFiles.contains(higherPriorityPath) || wildcardFiles.contains(higherPriorityPath)) { + return true; + } + } + + return false; + } + + /** + * Removes files included via wildcard expansion with a lower extension priority that have + * already been included. + * + * @param file The path to the file. + * @param extensionPriority The priority of the extension. + * @param context The expansion context. + */ + function removeWildcardFilesWithLowerPriorityExtension(file: Path, extensionPriority: ExtensionPriority, context: ExpansionContext) { + const { wildcardFiles, supportedExtensions } = context; + const nextExtensionPriority = getNextLowestExtensionPriority(extensionPriority); + for (let i = nextExtensionPriority; i < supportedExtensions.length; ++i) { + const lowerPriorityExtension = supportedExtensions[i]; + const lowerPriorityPath = changeExtension(file, lowerPriorityExtension); + wildcardFiles.remove(lowerPriorityPath); } } @@ -823,16 +1037,48 @@ namespace ts { return output; } + /** + * Adds a watched directory to an output map. + * + * @param output The output map. + * @param flags The directory flags. + * @param directory The directory path. + */ + function addDirectoryToOutput(output: Map, flags: WatchDirectoryFlags, directory: string) { + output[directory] = flags; + 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. + * @param excludePattern A pattern used to exclude a file specification. */ - function isExcludedPath(excludePattern: RegExp, path: string) { + function isExcludedPath(path: string, excludePattern: RegExp) { return excludePattern ? excludePattern.test(path) : false; } + /** + * Determines whether a file segment contains a valid extension. + * + * @param fileSpec The file specification. + * @param segmentStart The offset to the start of the file segment in the specification. + */ + function fileSegmentHasExtension(fileSpec: string, segmentStart: number) { + // if the final path segment does not have a . token, the file does not have an extension. + if (fileSpec.indexOf(".", segmentStart) === -1) { + return false; + } + + // if the extension for the final path segment is (".*"), then the file does not have an extension. + if (fileExtensionIs(fileSpec, ".*")) { + return false; + } + + return true; + } + /** * Creates a regular expression from a glob-style wildcard. * @@ -858,17 +1104,17 @@ namespace ts { let offset = indexOfWildcard(fileSpec, start); while (offset >= 0 && offset < end) { if (offset > start) { - // Escape and append the non-wildcard portion to the regular expression + // 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 += "[^/]*"; + // 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 + // Append a single-character pattern to the regular expression. pattern += "[^/]"; } @@ -1054,8 +1300,8 @@ namespace ts { * @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) { + function indexOfWildcard(fileSpec: string, start: number, end: number = fileSpec.length): number { + for (let i = start; i < end; ++i) { const ch = fileSpec.charCodeAt(i); if (ch === CharacterCodes.asterisk || ch === CharacterCodes.question) { return i; diff --git a/src/compiler/core.ts b/src/compiler/core.ts index aae4b54b5c7..998766e8d9d 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -26,7 +26,8 @@ namespace ts { remove, forEachValue: forEachValueInMap, reduce, - clear + clear, + mergeFrom }; function forEachValueInMap(f: (key: Path, value: T) => void) { @@ -61,6 +62,16 @@ namespace ts { files = {}; } + function mergeFrom(other: FileMap) { + other.forEachValue(mergeFromOther); + } + + function mergeFromOther(key: Path, value: T) { + if (!contains(key)) { + set(key, value); + } + } + function toKey(path: Path): string { return keyMapper ? keyMapper(path) : path; } @@ -822,6 +833,28 @@ namespace ts { return compareValues(aComponents.length, bComponents.length); } + export function containsPath(parent: string, child: string, currentDirectory: string, ignoreCase?: boolean) { + if (parent === undefined || child === undefined) return false; + if (parent === child) return true; + parent = removeTrailingDirectorySeparator(parent); + child = removeTrailingDirectorySeparator(child); + if (parent === child) return true; + const parentComponents = getNormalizedPathComponents(parent, currentDirectory); + const childComponents = getNormalizedPathComponents(child, currentDirectory); + if (childComponents.length < parentComponents.length) { + return false; + } + + for (let i = 0; i < parentComponents.length; ++i) { + const result = compareStrings(parentComponents[i], childComponents[i], ignoreCase); + if (result !== Comparison.EqualTo) { + return false; + } + } + + return true; + } + export function fileExtensionIs(path: string, extension: string): boolean { const pathLen = path.length; const extLen = extension.length; @@ -850,6 +883,59 @@ namespace ts { return false; } + /** + * Extension boundaries by priority. Lower numbers indicate higher priorities, and are + * aligned to the offset of the highest priority extension in the + * allSupportedExtensions array. + */ + export const enum ExtensionPriority { + TypeScriptFiles = 0, + DeclarationAndJavaScriptFiles = 2, + Limit = 5, + + Highest = TypeScriptFiles, + Lowest = DeclarationAndJavaScriptFiles, + } + + export function getExtensionPriority(path: string, supportedExtensions: string[]): ExtensionPriority { + for (let i = supportedExtensions.length - 1; i >= 0; i--) { + if (fileExtensionIs(path, supportedExtensions[i])) { + return adjustExtensionPriority(i); + } + } + + // If its not in the list of supported extensions, this is likely a + // TypeScript file with a non-ts extension + return ExtensionPriority.Highest; + } + + /** + * Adjusts an extension priority to be the highest priority within the same range. + */ + export function adjustExtensionPriority(extensionPriority: ExtensionPriority): ExtensionPriority { + if (extensionPriority < ExtensionPriority.DeclarationAndJavaScriptFiles) { + return ExtensionPriority.TypeScriptFiles; + } + else if (extensionPriority < ExtensionPriority.Limit) { + return ExtensionPriority.DeclarationAndJavaScriptFiles; + } + else { + return ExtensionPriority.Limit; + } + } + + /** + * Gets the next lowest extension priority for a given priority. + */ + export function getNextLowestExtensionPriority(extensionPriority: ExtensionPriority): ExtensionPriority { + if (extensionPriority < ExtensionPriority.DeclarationAndJavaScriptFiles) { + return ExtensionPriority.DeclarationAndJavaScriptFiles; + } + else { + return ExtensionPriority.Limit; + } + } + const extensionsToRemove = [".d.ts", ".ts", ".js", ".tsx", ".jsx"]; export function removeFileExtension(path: string): string { for (const ext of extensionsToRemove) { diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 6a1d0e5ce65..c607bace4ef 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -2020,6 +2020,10 @@ "category": "Error", "code": 5009 }, + "File specification cannot end in a recursive directory wildcard ('**'): '{0}'.": { + "category": "Error", + "code": 5010 + }, "File specification cannot contain multiple recursive directory wildcards ('**'): '{0}'.": { "category": "Error", "code": 5011 diff --git a/src/compiler/types.ts b/src/compiler/types.ts index ffd9ddc206d..3d16f70d049 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -15,6 +15,7 @@ namespace ts { forEachValue(f: (key: Path, v: T) => void): void; reduce(f: (memo: U, value: T, key: Path) => U, initial: U): U; + mergeFrom(other: FileMap): void; clear(): void; } @@ -1588,11 +1589,17 @@ namespace ts { readDirectory(rootDir: string, extension: string, exclude: string[]): string[]; /** - * Gets a value indicating whether the specified path exists. + * Gets a value indicating whether the specified path exists and is a file. * @param path The path to test. */ fileExists(path: string): boolean; + /** + * Gets a value indicating whether the specified path exists and is a directory. + * @param path The path to test. + */ + directoryExists(path: string): boolean; + /** * Reads the files names in the directory. * @param rootDir The directory path. @@ -2460,6 +2467,17 @@ namespace ts { options: CompilerOptions; fileNames: string[]; errors: Diagnostic[]; + wildcardDirectories?: Map; + } + + export const enum WatchDirectoryFlags { + None = 0, + Recursive = 1 << 0, + } + + export interface ExpandResult { + fileNames: string[]; + wildcardDirectories: Map; } /* @internal */ diff --git a/src/harness/external/chai.d.ts b/src/harness/external/chai.d.ts index fc25980d3a0..fba0024d254 100644 --- a/src/harness/external/chai.d.ts +++ b/src/harness/external/chai.d.ts @@ -167,12 +167,14 @@ 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 deepEqual(actual: T, expected: T, message?: string): void; + function notDeepEqual(actual: T, expected: T, 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; function isNotNull(value: any, message?: string): void; + function isUndefined(value: any, message?: string): void; + function isDefined(value: any, message?: string): void; } } \ No newline at end of file diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index e12105e3393..521d17cfc8f 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -266,6 +266,7 @@ namespace Harness.LanguageService { throw new Error("Not implemented."); } fileExists(fileName: string) { return this.getScriptInfo(fileName) !== undefined; } + directoryExists(directoryName: string) { return false; } readFile(fileName: string) { const snapshot = this.nativeHost.getScriptSnapshot(fileName); return snapshot && snapshot.getText(0, snapshot.getLength()); diff --git a/src/harness/projectsRunner.ts b/src/harness/projectsRunner.ts index 655c2f07c2b..76c7952428a 100644 --- a/src/harness/projectsRunner.ts +++ b/src/harness/projectsRunner.ts @@ -213,6 +213,7 @@ class ProjectRunner extends RunnerBase { const configParseHost: ts.ParseConfigHost = { useCaseSensitiveFileNames: Harness.IO.useCaseSensitiveFileNames(), fileExists, + directoryExists, readDirectory, readDirectoryNames, readFileNames @@ -297,6 +298,10 @@ class ProjectRunner extends RunnerBase { return Harness.IO.fileExists(getFileNameInTheProjectTest(fileName)); } + function directoryExists(directoryName: string): boolean { + return Harness.IO.directoryExists(getFileNameInTheProjectTest(directoryName)); + } + function getSourceFileText(fileName: string): string { let text: string = undefined; try { diff --git a/src/harness/rwcRunner.ts b/src/harness/rwcRunner.ts index 5b4e2a7a3d9..826f6b67260 100644 --- a/src/harness/rwcRunner.ts +++ b/src/harness/rwcRunner.ts @@ -79,6 +79,7 @@ namespace RWC { const configParseHost: ts.ParseConfigHost = { useCaseSensitiveFileNames: Harness.IO.useCaseSensitiveFileNames(), fileExists: Harness.IO.fileExists, + directoryExists: Harness.IO.directoryExists, readDirectory: Harness.IO.readDirectory, readDirectoryNames: Harness.IO.readDirectoryNames, readFileNames: Harness.IO.readFileNames, diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 0305b7595c2..0e1c1c97f03 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -119,7 +119,7 @@ namespace ts.server { if (!resolution) { const existingResolution = currentResolutionsInFile && ts.lookUp(currentResolutionsInFile, moduleName); if (moduleResolutionIsValid(existingResolution)) { - // ok, it is safe to use existing module resolution results + // ok, it is safe to use existing module resolution results resolution = existingResolution; } else { @@ -144,8 +144,8 @@ namespace ts.server { } if (resolution.resolvedModule) { - // TODO: consider checking failedLookupLocations - // TODO: use lastCheckTime to track expiration for module name resolution + // TODO: consider checking failedLookupLocations + // TODO: use lastCheckTime to track expiration for module name resolution return true; } @@ -354,6 +354,7 @@ namespace ts.server { export interface ProjectOptions { // these fields can be present in the project file files?: string[]; + wildcardDirectories?: ts.Map; compilerOptions?: ts.CompilerOptions; } @@ -362,6 +363,7 @@ namespace ts.server { projectFilename: string; projectFileWatcher: FileWatcher; directoryWatcher: FileWatcher; + directoriesWatchedForWildcards: Map; // Used to keep track of what directories are watched for this project directoriesWatchedForTsconfig: string[] = []; program: ts.Program; @@ -510,7 +512,7 @@ namespace ts.server { openFileRootsConfigured: ScriptInfo[] = []; // a path to directory watcher map that detects added tsconfig files directoryWatchersForTsconfig: ts.Map = {}; - // count of how many projects are using the directory watcher. If the + // count of how many projects are using the directory watcher. If the // number becomes 0 for a watcher, then we should close it. directoryWatchersRefCount: ts.Map = {}; hostConfiguration: HostConfiguration; @@ -590,11 +592,11 @@ namespace ts.server { // We check if the project file list has changed. If so, we update the project. if (!arrayIsEqualTo(currentRootFiles && currentRootFiles.sort(), newRootFiles && newRootFiles.sort())) { // For configured projects, the change is made outside the tsconfig file, and - // it is not likely to affect the project for other files opened by the client. We can + // it is not likely to affect the project for other files opened by the client. We can // just update the current project. this.updateConfiguredProject(project); - // Call updateProjectStructure to clean up inferred projects we may have + // Call updateProjectStructure to clean up inferred projects we may have // created for the new files this.updateProjectStructure(); } @@ -739,6 +741,8 @@ namespace ts.server { if (project.isConfiguredProject()) { project.projectFileWatcher.close(); project.directoryWatcher.close(); + forEachValue(project.directoriesWatchedForWildcards, watcher => { watcher.close(); }); + delete project.directoriesWatchedForWildcards; this.configuredProjects = copyListRemovingItem(project, this.configuredProjects); } else { @@ -816,8 +820,8 @@ namespace ts.server { * @param info The file that has been closed or newly configured */ closeOpenFile(info: ScriptInfo) { - // Closing file should trigger re-reading the file content from disk. This is - // because the user may chose to discard the buffer content before saving + // Closing file should trigger re-reading the file content from disk. This is + // because the user may chose to discard the buffer content before saving // to the disk, and the server's version of the file can be out of sync. info.svc.reloadFromFile(info.fileName); @@ -915,8 +919,8 @@ namespace ts.server { } /** - * This function is to update the project structure for every projects. - * It is called on the premise that all the configured projects are + * This function is to update the project structure for every projects. + * It is called on the premise that all the configured projects are * up to date. */ updateProjectStructure() { @@ -970,7 +974,7 @@ namespace ts.server { if (rootFile.defaultProject && rootFile.defaultProject.isConfiguredProject()) { // If the root file has already been added into a configured project, - // meaning the original inferred project is gone already. + // meaning the original inferred project is gone already. if (!rootedProject.isConfiguredProject()) { this.removeProject(rootedProject); } @@ -1075,9 +1079,9 @@ namespace ts.server { } /** - * This function tries to search for a tsconfig.json for the given file. If we found it, + * This function tries to search for a tsconfig.json for the given file. If we found it, * we first detect if there is already a configured project created for it: if so, we re-read - * the tsconfig file content and update the project; otherwise we create a new one. + * the tsconfig file content and update the project; otherwise we create a new one. */ openOrUpdateConfiguredProjectForFile(fileName: string) { const searchPath = ts.normalizePath(getDirectoryPath(fileName)); @@ -1215,7 +1219,8 @@ namespace ts.server { else { const projectOptions: ProjectOptions = { files: parsedCommandLine.fileNames, - compilerOptions: parsedCommandLine.options + wildcardDirectories: parsedCommandLine.wildcardDirectories, + compilerOptions: parsedCommandLine.options, }; return { succeeded: true, projectOptions }; } @@ -1241,12 +1246,30 @@ namespace ts.server { } project.finishGraph(); project.projectFileWatcher = this.host.watchFile(configFilename, _ => this.watchedProjectConfigFileChanged(project)); - this.log("Add recursive watcher for: " + ts.getDirectoryPath(configFilename)); + + const configDirectoryPath = ts.getDirectoryPath(configFilename); + + this.log("Add recursive watcher for: " + configDirectoryPath); project.directoryWatcher = this.host.watchDirectory( - ts.getDirectoryPath(configFilename), + configDirectoryPath, path => this.directoryWatchedForSourceFilesChanged(project, path), /*recursive*/ true ); + + project.directoriesWatchedForWildcards = reduceProperties(projectOptions.wildcardDirectories, (watchers, flag, directory) => { + if (comparePaths(configDirectoryPath, directory, ".", !this.host.useCaseSensitiveFileNames) !== Comparison.EqualTo) { + const recursive = (flag & WatchDirectoryFlags.Recursive) !== 0; + this.log(`Add ${ recursive ? "recursive " : ""}watcher for: ${directory}`); + watchers[directory] = this.host.watchDirectory( + directory, + path => this.directoryWatchedForSourceFilesChanged(project, path), + recursive + ); + } + + return watchers; + }, >{}); + return { success: true, project: project }; } } @@ -1280,7 +1303,7 @@ namespace ts.server { info = this.openFile(fileName, /*openedByClient*/ false); } else { - // if the root file was opened by client, it would belong to either + // if the root file was opened by client, it would belong to either // openFileRoots or openFileReferenced. if (info.isOpen) { if (this.openFileRoots.indexOf(info) >= 0) { diff --git a/src/services/shims.ts b/src/services/shims.ts index 4c35f5aaf1b..a7f85d24452 100644 --- a/src/services/shims.ts +++ b/src/services/shims.ts @@ -75,6 +75,7 @@ namespace ts { readDirectory(rootDir: string, extension: string, exclude?: string): string; readDirectoryNames?(rootDir: string): string; readFileNames?(rootDir: string): string; + directoryExists?(path: string): boolean; useCaseSensitiveFileNames?: boolean; } @@ -473,6 +474,18 @@ namespace ts { return this.shimHost.fileExists(fileName); } + public directoryExists(directoryName: string): boolean { + if (this.shimHost.directoryExists) { + return this.shimHost.directoryExists(directoryName); + } + + if (sys) { + return sys.directoryExists(directoryName); + } + + return false; + } + public readFile(fileName: string): string { return this.shimHost.readFile(fileName); } diff --git a/tests/cases/unittests/expandFiles.ts b/tests/cases/unittests/expandFiles.ts index a9d12a73260..3aac938f178 100644 --- a/tests/cases/unittests/expandFiles.ts +++ b/tests/cases/unittests/expandFiles.ts @@ -1,164 +1,395 @@ /// /// -describe("expandFiles", () => { - it("fail", () => { - assert.isTrue(false, "just checking"); - }); +namespace ts { + const caseInsensitiveBasePath = "c:/dev/"; + const caseInsensitiveHost = createMockParseConfigHost(/*ignoreCase*/ true, caseInsensitiveBasePath, [ + "a.ts", + "a.d.ts", + "a.js", + "b.ts", + "b.js", + "c.d.ts", + "z/a.ts", + "z/abz.ts", + "z/aba.ts", + "z/b.ts", + "z/bbz.ts", + "z/bba.ts", + "x/a.ts", + "x/aa.ts", + "x/b.ts", + "x/y/a.ts", + "x/y/b.ts", + "js/a.js", + "js/b.js", + ]); - 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 caseSensitiveBasePath = "/dev/"; + const caseSensitiveHost = createMockParseConfigHost(/*ignoreCase*/ false, caseSensitiveBasePath, [ + "a.ts", + "a.d.ts", + "a.js", + "b.ts", + "b.js", + "A.ts", + "B.ts", + "c.d.ts", + "z/a.ts", + "z/abz.ts", + "z/aba.ts", + "z/b.ts", + "z/bbz.ts", + "z/bba.ts", + "x/a.ts", + "x/b.ts", + "x/y/a.ts", + "x/y/b.ts", + "js/a.js", + "js/b.js", + ]); - 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 caseInsensitiveMixedExtensionHost = createMockParseConfigHost(/*ignoreCase*/ true, caseInsensitiveBasePath, [ + "a.ts", + "a.d.ts", + "a.js", + "b.tsx", + "b.d.ts", + "b.jsx", + "c.tsx", + "c.js", + "d.js", + "e.jsx", + "f.other" + ]); - 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"]); + describe("expandFiles", () => { + describe("with literal file list", () => { + it("without exclusions", () => { + const fileNames = ["a.ts", "b.ts"]; + const expected: ts.ExpandResult = { + fileNames: ["c:/dev/a.ts", "c:/dev/b.ts"], + wildcardDirectories: {}, + }; + const actual = ts.expandFiles(fileNames, /*includeSpecs*/ undefined, /*excludeSpecs*/ undefined, caseInsensitiveBasePath, {}, caseInsensitiveHost); + assert.deepEqual(actual, expected); + }); + it("missing files are still present", () => { + const fileNames = ["z.ts", "x.ts"]; + const expected: ts.ExpandResult = { + fileNames: ["c:/dev/z.ts", "c:/dev/x.ts"], + wildcardDirectories: {}, + }; + const actual = ts.expandFiles(fileNames, /*includeSpecs*/ undefined, /*excludeSpecs*/ undefined, caseInsensitiveBasePath, {}, caseInsensitiveHost); + assert.deepEqual(actual, expected); + }); + it("are not removed due to excludes", () => { + const fileNames = ["a.ts", "b.ts"]; + const excludeSpecs = ["b.ts"]; + const expected: ts.ExpandResult = { + fileNames: ["c:/dev/a.ts", "c:/dev/b.ts"], + wildcardDirectories: {}, + }; + const actual = ts.expandFiles(fileNames, /*includeSpecs*/ undefined, excludeSpecs, caseInsensitiveBasePath, {}, caseInsensitiveHost); + assert.deepEqual(actual, expected); + }); }); - 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"]); + + describe("with literal include list", () => { + it("without exclusions", () => { + const includeSpecs = ["a.ts", "b.ts"]; + const expected: ts.ExpandResult = { + fileNames: ["c:/dev/a.ts", "c:/dev/b.ts"], + wildcardDirectories: {}, + }; + const actual = ts.expandFiles(/*fileNames*/ undefined, includeSpecs, /*excludeSpecs*/ undefined, caseInsensitiveBasePath, {}, caseInsensitiveHost); + assert.deepEqual(actual, expected); + }); + it("with non .ts file extensions are excluded", () => { + const includeSpecs = ["a.js", "b.js"]; + const expected: ts.ExpandResult = { + fileNames: [], + wildcardDirectories: {}, + }; + const actual = ts.expandFiles(/*fileNames*/ undefined, includeSpecs, /*excludeSpecs*/ undefined, caseInsensitiveBasePath, {}, caseInsensitiveHost); + assert.deepEqual(actual, expected); + }); + it("with missing files are excluded", () => { + const includeSpecs = ["z.ts", "x.ts"]; + const expected: ts.ExpandResult = { + fileNames: [], + wildcardDirectories: {}, + }; + const actual = ts.expandFiles(/*fileNames*/ undefined, includeSpecs, /*excludeSpecs*/ undefined, caseInsensitiveBasePath, {}, caseInsensitiveHost); + assert.deepEqual(actual, expected); + }); + it("with literal excludes", () => { + const includeSpecs = ["a.ts", "b.ts"]; + const excludeSpecs = ["b.ts"]; + const expected: ts.ExpandResult = { + fileNames: ["c:/dev/a.ts"], + wildcardDirectories: {}, + }; + const actual = ts.expandFiles(/*fileNames*/ undefined, includeSpecs, excludeSpecs, caseInsensitiveBasePath, {}, caseInsensitiveHost); + assert.deepEqual(actual, expected); + }); + 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 expected: ts.ExpandResult = { + fileNames: ["c:/dev/z/a.ts", "c:/dev/z/aba.ts"], + wildcardDirectories: {}, + }; + const actual = ts.expandFiles(/*fileNames*/ undefined, includeSpecs, excludeSpecs, caseInsensitiveBasePath, {}, caseInsensitiveHost); + assert.deepEqual(actual, expected); + }); + 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 expected: ts.ExpandResult = { + fileNames: ["c:/dev/a.ts", "c:/dev/x/a.ts", "c:/dev/x/y/a.ts"], + wildcardDirectories: {}, + }; + const actual = ts.expandFiles(/*fileNames*/ undefined, includeSpecs, excludeSpecs, caseInsensitiveBasePath, {}, caseInsensitiveHost); + assert.deepEqual(actual, expected); + }); + it("with case sensitive exclude", () => { + const includeSpecs = ["B.ts"]; + const excludeSpecs = ["**/b.ts"]; + const expected: ts.ExpandResult = { + fileNames: ["/dev/B.ts"], + wildcardDirectories: {}, + }; + const actual = ts.expandFiles(/*fileNames*/ undefined, includeSpecs, excludeSpecs, caseSensitiveBasePath, {}, caseSensitiveHost); + assert.deepEqual(actual, expected); + }); }); - 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 wildcard include list", () => { + it("same named declarations are excluded", () => { + const includeSpecs = ["*.ts"]; + const expected: ts.ExpandResult = { + fileNames: ["c:/dev/a.ts", "c:/dev/b.ts", "c:/dev/c.d.ts"], + wildcardDirectories: { + "c:/dev": ts.WatchDirectoryFlags.None + }, + }; + const actual = ts.expandFiles(/*fileNames*/ undefined, includeSpecs, /*excludeSpecs*/ undefined, caseInsensitiveBasePath, {}, caseInsensitiveHost); + assert.deepEqual(actual, expected); + }); + it("`*` matches only ts files", () => { + const includeSpecs = ["*"]; + const expected: ts.ExpandResult = { + fileNames: ["c:/dev/a.ts", "c:/dev/b.ts", "c:/dev/c.d.ts"], + wildcardDirectories: { + "c:/dev": ts.WatchDirectoryFlags.None + }, + }; + const actual = ts.expandFiles(/*fileNames*/ undefined, includeSpecs, /*excludeSpecs*/ undefined, caseInsensitiveBasePath, {}, caseInsensitiveHost); + assert.deepEqual(actual, expected); + }); + it("`?` matches only a single character", () => { + const includeSpecs = ["x/?.ts"]; + const expected: ts.ExpandResult = { + fileNames: ["c:/dev/x/a.ts", "c:/dev/x/b.ts"], + wildcardDirectories: { + "c:/dev/x": ts.WatchDirectoryFlags.None + }, + }; + const actual = ts.expandFiles(/*fileNames*/ undefined, includeSpecs, /*excludeSpecs*/ undefined, caseInsensitiveBasePath, {}, caseInsensitiveHost); + assert.deepEqual(actual, expected); + }); + it("with recursive directory", () => { + const includeSpecs = ["**/a.ts"]; + const expected: ts.ExpandResult = { + fileNames: ["c:/dev/a.ts", "c:/dev/x/a.ts", "c:/dev/x/y/a.ts", "c:/dev/z/a.ts"], + wildcardDirectories: { + "c:/dev": ts.WatchDirectoryFlags.Recursive + }, + }; + const actual = ts.expandFiles(/*fileNames*/ undefined, includeSpecs, /*excludeSpecs*/ undefined, caseInsensitiveBasePath, {}, caseInsensitiveHost); + assert.deepEqual(actual, expected); + }); + it("case sensitive", () => { + const includeSpecs = ["**/A.ts"]; + const expected: ts.ExpandResult = { + fileNames: ["/dev/A.ts"], + wildcardDirectories: { + "/dev": ts.WatchDirectoryFlags.Recursive + }, + }; + const actual = ts.expandFiles(/*fileNames*/ undefined, includeSpecs, /*excludeSpecs*/ undefined, caseSensitiveBasePath, {}, caseSensitiveHost); + assert.deepEqual(actual, expected); + }); + it("with missing files are excluded", () => { + const includeSpecs = ["*/z.ts"]; + const expected: ts.ExpandResult = { + fileNames: [], + wildcardDirectories: { + "c:/dev": ts.WatchDirectoryFlags.None + }, + }; + const actual = ts.expandFiles(/*fileNames*/ undefined, includeSpecs, /*excludeSpecs*/ undefined, caseInsensitiveBasePath, {}, caseInsensitiveHost); + assert.deepEqual(actual, expected); + }); + it("always include literal files", () => { + const fileNames = ["a.ts"]; + const includeSpecs = ["*/z.ts"]; + const excludeSpecs = ["**/a.ts"]; + const expected: ts.ExpandResult = { + fileNames: ["c:/dev/a.ts"], + wildcardDirectories: { + "c:/dev": ts.WatchDirectoryFlags.None + }, + }; + const actual = ts.expandFiles(fileNames, includeSpecs, excludeSpecs, caseInsensitiveBasePath, {}, caseInsensitiveHost); + assert.deepEqual(actual, expected); + }); + it("exclude folders", () => { + const includeSpecs = ["**/*"]; + const excludeSpecs = ["z", "x"]; + const expected: ts.ExpandResult = { + fileNames: [ + "c:/dev/a.ts", + "c:/dev/b.ts", + "c:/dev/c.d.ts" + ], + wildcardDirectories: { + "c:/dev": ts.WatchDirectoryFlags.Recursive + } + }; + const actual = ts.expandFiles(/*fileNames*/ undefined, includeSpecs, excludeSpecs, caseInsensitiveBasePath, {}, caseInsensitiveHost); + assert.deepEqual(actual, expected); + }); + it("exclude .js files when allowJs=false", () => { + const includeSpecs = ["js/*"]; + const expected: ts.ExpandResult = { + fileNames: [], + wildcardDirectories: { + "c:/dev/js": ts.WatchDirectoryFlags.None + } + }; + const actual = ts.expandFiles(/*fileNames*/ undefined, includeSpecs, /*excludeSpecs*/ undefined, caseInsensitiveBasePath, {}, caseInsensitiveHost); + assert.deepEqual(actual, expected); + }); + it("include .js files when allowJs=true", () => { + const includeSpecs = ["js/*"]; + const expected: ts.ExpandResult = { + fileNames: ["c:/dev/js/a.js", "c:/dev/js/b.js"], + wildcardDirectories: { + "c:/dev/js": ts.WatchDirectoryFlags.None + } + }; + const actual = ts.expandFiles(/*fileNames*/ undefined, includeSpecs, /*excludeSpecs*/ undefined, caseInsensitiveBasePath, { allowJs: true }, caseInsensitiveHost); + assert.deepEqual(actual, expected); + }); + }); + + describe("when called from parseJsonConfigFileContent", () => { + it("with jsx=none, allowJs=false", () => { + const json: any = { + "compilerOptions": { + "jsx": "none", + "allowJs": false + } + }; + const expected: ts.ExpandResult = { + fileNames: [ + "c:/dev/a.ts", + "c:/dev/b.tsx", + "c:/dev/c.tsx", + ], + wildcardDirectories: { + "c:/dev": ts.WatchDirectoryFlags.Recursive + } + }; + const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveMixedExtensionHost, caseInsensitiveBasePath); + assert.deepEqual(actual.fileNames, expected.fileNames); + assert.deepEqual(actual.wildcardDirectories, expected.wildcardDirectories); + }); + it("with jsx=preserve, allowJs=false", () => { + const json: any = { + "compilerOptions": { + "jsx": "preserve", + "allowJs": false + } + }; + const expected: ts.ExpandResult = { + fileNames: [ + "c:/dev/a.ts", + "c:/dev/b.tsx", + "c:/dev/c.tsx", + ], + wildcardDirectories: { + "c:/dev": ts.WatchDirectoryFlags.Recursive + } + }; + const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveMixedExtensionHost, caseInsensitiveBasePath); + assert.deepEqual(actual.fileNames, expected.fileNames); + assert.deepEqual(actual.wildcardDirectories, expected.wildcardDirectories); + }); + it("with jsx=none, allowJs=true", () => { + const json: any = { + "compilerOptions": { + "jsx": "none", + "allowJs": true + } + }; + const expected: ts.ExpandResult = { + fileNames: [ + "c:/dev/a.ts", + "c:/dev/b.tsx", + "c:/dev/c.tsx", + "c:/dev/d.js", + "c:/dev/e.jsx", + ], + wildcardDirectories: { + "c:/dev": ts.WatchDirectoryFlags.Recursive + } + }; + const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveMixedExtensionHost, caseInsensitiveBasePath); + assert.deepEqual(actual.fileNames, expected.fileNames); + assert.deepEqual(actual.wildcardDirectories, expected.wildcardDirectories); + }); + it("with jsx=preserve, allowJs=true", () => { + const json: any = { + "compilerOptions": { + "jsx": "preserve", + "allowJs": true + } + }; + const expected: ts.ExpandResult = { + fileNames: [ + "c:/dev/a.ts", + "c:/dev/b.tsx", + "c:/dev/c.tsx", + "c:/dev/d.js", + "c:/dev/e.jsx", + ], + wildcardDirectories: { + "c:/dev": ts.WatchDirectoryFlags.Recursive + } + }; + const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveMixedExtensionHost, caseInsensitiveBasePath); + assert.deepEqual(actual.fileNames, expected.fileNames); + assert.deepEqual(actual.wildcardDirectories, expected.wildcardDirectories); + }); }); }); - 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"]); - }); - }); + interface DirectoryEntry { + files: ts.Map; + directories: ts.Map; + } - 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"]); - }); - }); + interface TestParseConfigHost extends ts.ParseConfigHost { + basePath: string; + } - function createMockParseConfigHost(basePath: string, files: string[], ignoreCase: boolean): ts.ParseConfigHost { + function createMockParseConfigHost(ignoreCase: boolean, basePath: string, files: string[]): TestParseConfigHost { const fileSet: ts.Map = {}; - const directorySet: ts.Map<{ files: ts.Map; directories: ts.Map; }> = {}; + const directorySet: ts.Map = {}; + const emptyDirectory: DirectoryEntry = { files: {}, directories: {} }; files.sort((a, b) => ts.comparePaths(a, b, basePath, ignoreCase)); for (const file of files) { @@ -167,18 +398,24 @@ describe("expandFiles", () => { return { useCaseSensitiveFileNames: !ignoreCase, + basePath, fileExists, + directoryExists, readDirectory, readFileNames, readDirectoryNames }; function fileExists(path: string): boolean { + path = ts.getNormalizedAbsolutePath(path, basePath); + path = ts.removeTrailingDirectorySeparator(path); const fileKey = ignoreCase ? path.toLowerCase() : path; return ts.hasProperty(fileSet, fileKey); } function directoryExists(path: string): boolean { + path = ts.getNormalizedAbsolutePath(path, basePath); + path = ts.removeTrailingDirectorySeparator(path); const directoryKey = ignoreCase ? path.toLowerCase() : path; return ts.hasProperty(directorySet, directoryKey); } @@ -188,7 +425,7 @@ describe("expandFiles", () => { } function readFileNames(path: string) { - const files = getDirectoryEntry(path).files; + const { files } = getDirectoryEntry(path) || emptyDirectory; const result: string[] = []; ts.forEachKey(files, key => { result.push(key); }); result.sort((a, b) => ts.compareStrings(a, b, ignoreCase)); @@ -196,7 +433,7 @@ describe("expandFiles", () => { } function readDirectoryNames(path: string) { - const directories = getDirectoryEntry(path).directories; + const { directories } = getDirectoryEntry(path); // || emptyDirectory; const result: string[] = []; ts.forEachKey(directories, key => { result.push(key); }); result.sort((a, b) => ts.compareStrings(a, b, ignoreCase)); @@ -245,5 +482,4 @@ describe("expandFiles", () => { } } } -}); - +} \ No newline at end of file