From fa3b8427d0f986b8777b5ba0a579c3e22ef5cd30 Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Tue, 19 May 2015 15:33:15 -0700 Subject: [PATCH 01/11] Added glob pattern matching for paths in tsconfig.json --- src/compiler/commandLineParser.ts | 358 +++++++++++++++++- src/compiler/core.ts | 56 +++ .../diagnosticInformationMap.generated.ts | 1 + src/compiler/diagnosticMessages.json | 4 + src/compiler/sys.ts | 12 + src/compiler/tsc.ts | 2 +- src/compiler/types.ts | 22 +- src/harness/harnessLanguageService.ts | 8 + src/services/shims.ts | 46 ++- 9 files changed, 500 insertions(+), 9 deletions(-) diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 7a589fe61cb3b..cfc130fe76bae 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -339,10 +339,11 @@ module ts { * file to. e.g. outDir */ export function parseConfigFile(json: any, host: ParseConfigHost, basePath: string): ParsedCommandLine { - var errors: Diagnostic[] = []; - + let errors: Diagnostic[] = []; + let options = getCompilerOptions(); + return { - options: getCompilerOptions(), + options, fileNames: getFiles(), errors }; @@ -390,13 +391,20 @@ module ts { } function getFiles(): string[] { - var files: string[] = []; if (hasProperty(json, "files")) { if (json["files"] instanceof Array) { - var files = map(json["files"], s => combinePaths(basePath, s)); + var files = json["files"]; + + var exclude: string[]; + if (hasProperty(json, "exclude") && json["exclude"] instanceof Array) { + exclude = json["exclude"]; + } + + return expandFiles(files, exclude); } } else { + var files: string[] = []; var sysFiles = host.readDirectory(basePath, ".ts"); for (var i = 0; i < sysFiles.length; i++) { var name = sysFiles[i]; @@ -404,8 +412,346 @@ module ts { files.push(name); } } + return files; + } + } + + /** + * Expands an array of file specifications. + * @param files The file specifications to expand. + * @param exclude Any file specifications to exclude. + */ + function expandFiles(files: string[], exclude: string[]): string[] { + // Used to verify the file specification does not have multiple recursive directory wildcards. + let invalidRecursiveWildcardPattern = /(^|\/)\*\*\/(.*\/)?\*\*(\/|$)/; + + // Used to parse glob-style wildcards, this pattern matches the following tokens: + // + // `*` - Matches zero or more characters. This token, if matched, will be + // stored in capture group $1. + // `?` - Matches zero or one character. This token, if matched, will be + // stored in capture group $2. + // `[` RangeCharacters `]` - Matches a zero or one character in the specified range of + // characters, where RangeCharacters consists of any character not + // in the set { `*`, `?`, `]` }. These tokens, if matched, will be + // stored in capture group $3. + // ReservedCharacters - Where ReservedCharacters are tokens with special meaning in + // the regular expression grammar that need to be escaped. + // This includes characters in the set { `+`, `.`, `^`, `$`, `(`, `)`, + // `{`, `}`, `[`, `]`, `|` }. These tokens, if matched, will be stored + // in capture group $4. + // DirectorySeparators - Characters in the set { `/`, `\` }. + let wildcardParserPattern = /(\*)|(\?)|(\[[^?*\]]*\])|([+.^$(){}\[\]|])/g; + + // Used to parse glob-style wildcards for exclude paths, this pattern matches the following tokens: + // + // `**` - Matches a recursive directory, but only when found in the following + // context: + // - As the whole wildcard. + // - At the start of the wildcard, followed by `/`. + // - At the end of the wildcard, preceeded by `/`. + // - Anywhere in the pattern if preceeded and followed by `/`. + // These tokens, if matched, will be stored in capture group $1. + // the pattern and followed by a directory separator + // `*` - Matches zero or more characters. This token, if matched, will be + // stored in capture group $2. + // `?` - Matches zero or one character. This token, if matched, will be + // stored in capture group $3. + // `[` RangeCharacters `]` - Matches a zero or one character in the specified range of + // characters, where RangeCharacters consists of any character not + // in the set { `*`, `?`, `]` }. These tokens, if matched, will be + // stored in capture group $4. + // ReservedCharacters - Where ReservedCharacters are tokens with special meaning in + // the regular expression grammar that need to be escaped. + // This includes characters in the set { `+`, `.`, `^`, `$`, `(`, `)`, + // `{`, `}`, `[`, `]`, `|` }. These tokens, if matched, will be stored + // in capture group $5. + let excludeWildcardParserPattern = /((?:^|\/)\*\*(?:\/|$))|(\*)|(\?)|(\[[^?*\/\]]*\])|([+.^$(){}\[\]|])/g; + + // Used to parse a glob-style character range, this pattern matches the following tokens: + // + // `[` - The opening bracket of the range, if at the start of the string. + // This token, if matched, will be stored in capture group $1. + // `!` - Indicates the range should match any character except those listed in + // the range, if found immediately following the opening bracket of the + // range. This token, if matched, will be stored in capture group $2. + // ReservedCharacters - Where ReservedCharacters are tokens with special meaning in the + // regular expression grammar that need to be escaped. This includes + // characters in the set { `^`, `[` }. These tokens, if matched, + // will be stored in capture group $3. + // `]` - Matches the closing bracket of the range, if at the end of the string. + // This token, if matched, will be stored in capture group $4. + let rangeParserPattern = /(^\[)(!)?|([\^\[])|(\]$)/g; + + // Used to match characters with special meaning in a regular expression. + let regExpReservedCharacterPattern = /[?*+.^$(){}\[\]|]/; + + let prefix = normalizePath(basePath); + let ignoreCase = !host.useCaseSensitiveFileNames; + let fileSet: Map = {}; + + // populate the exclusion list + let excludePattern = createExcludePattern(prefix, exclude); + + // expand and include the provided files into the file set. + for (let fileSpec of files) { + let normalizedFileSpec = normalizePath(fileSpec); + normalizedFileSpec = removeTrailingDirectorySeparator(normalizedFileSpec); + if (invalidRecursiveWildcardPattern.test(normalizedFileSpec)) { + errors.push(createCompilerDiagnostic(Diagnostics.File_specification_cannot_contain_multiple_recursive_directory_wildcards_Asterisk_Asterisk_Colon_0, fileSpec)) + } + else { + expandDirectory(prefix, normalizedFileSpec, 0); + } + } + + let result: string[] = []; + forEachValue(fileSet, value => { result.push(value); }); + return result; + + /** + * Expands a wildcard portion of a file specification. + * @param prefix The prefix path. + * @param fileSpec The file specification. + * @param offset The offset in the file specification. + */ + function expandWildcard(prefix: string, fileSpec: string, offset: number): void { + // find the offset of the next directory separator to extract the wildcard path segment. + let endOffset = fileSpec.indexOf(directorySeparator, offset); + let endOfLine = endOffset < 0; + let wildcard = fileSpec.substring(offset, endOfLine ? fileSpec.length : endOffset); + if (wildcard === "**") { + // If the path contains only "**", then this is a recursive directory pattern. + if (endOfLine) { + // If there is no file specification following the recursive directory pattern, + // we cannot match any files, so we will ignore this pattern + return; + } + + // expand the recursive directory pattern + expandRecursiveDirectory(prefix, fileSpec, endOffset + 1); + } + else { + // match the entries in the directory against the wildcard pattern. + let entries = host.readDirectoryFlat(prefix); + let pattern = createPatternFromWildcard(wildcard); + for (let entry of entries) { + // skip the entry if it does not match the pattern. + if (!pattern.test(entry)) { + continue; + } + + let path = combinePaths(prefix, entry); + if (endOfLine) { + // This wildcard has no further directory so process the match. + includeFile(path); + } + else if (host.directoryExists(path)) { + // If this was a directory, process the directory. + expandDirectory(path, fileSpec, endOffset + 1); + } + } + } + } + + /** + * Expands a `**` recursive directory wildcard. + * @param prefix The directory to recursively expand + * @param fileSpec The original file specification + * @param offset The offset into the file specification + */ + function expandRecursiveDirectory(prefix: string, fileSpec: string, offset: number): void { + // expand the non-recursive part of the file specification against the prefix path. + expandDirectory(prefix, fileSpec, offset); + + // recursively expand each subdirectory. + let entries = host.readDirectoryFlat(prefix); + for (let entry of entries) { + let path = combinePaths(prefix, entry); + if (host.directoryExists(path)) { + expandRecursiveDirectory(path, fileSpec, offset); + } + } + } + + /** + * Expands a directory with wildcards. + * @param prefix The directory to expand. + * @param fileSpec The original file specification. + * @param offset The offset into the file specification. + */ + function expandDirectory(prefix: string, fileSpec: string, offset: number): void { + // find the offset of the next wildcard in the file specification + let wildcardOffset = indexOfWildcard(fileSpec, offset); + if (wildcardOffset < 0) { + // There were no more wildcards, so process the match if the file exists and was not excluded. + let path = combinePaths(prefix, fileSpec.substring(offset)); + includeFile(path); + return; + } + + // find the last directory separator before the wildcard to get the leading path. + let endOffset = fileSpec.lastIndexOf(directorySeparator, wildcardOffset); + if (endOffset > offset) { + // wildcard occurs in a later segment, include remaining path up to wildcard in prefix + prefix = combinePaths(prefix, fileSpec.substring(offset, endOffset)); + offset = endOffset + 1; + } + + // Expand the wildcard portion of the path. + expandWildcard(prefix, fileSpec, offset); + } + + /** + * Includes a file in a file set. + * @param file The file to include. + */ + function includeFile(file: string): void { + let key = ignoreCase ? file.toLowerCase() : file; + // ignore the file if we've already matched it. + if (hasProperty(fileSet, key)) { + return; + } + + // ignore the file if it matches an exclude pattern + if (excludePattern && excludePattern.test(file)) { + return; + } + + // ignore the file if it doesn't exist or is a directory + if (!host.fileExists(file) || host.directoryExists(file)) { + return; + } + + // ignore the file if it does not have a supported extension + if (!options.allowNonTsExtensions && !hasSupportedFileExtension(file)) { + return; + } + + fileSet[key] = file; + } + + /** + * Gets the index of the next wildcard character in a file specification. + * @param fileSpec The file specification. + * @param start The offset at which to start the search. + */ + function indexOfWildcard(fileSpec: string, start: number): number { + const len = fileSpec.length; + for (let i = start; i < len; ++i) { + let charCode = fileSpec.charCodeAt(i); + if (charCode === 42 /*asterisk*/ || + charCode === 63 /*question*/ || + charCode === 91 /*openBracket*/) { + return i; + } + } + return -1; + } + + /** + * Creates a regular expression from a glob-style wildcard. + * @param wildcard The wildcard + */ + function createPatternFromWildcard(wildcard: string): RegExp { + return new RegExp("^" + wildcard.replace(wildcardParserPattern, wildcardTokenReplacer) + "$", ignoreCase ? "i" : ""); + } + + /** + * Creates a regular expression from a glob-style wildcard used to exclude a file. + * @param prefix The prefix path + * @param exclude The file specifications to exclude. + */ + function createExcludePattern(prefix: string, exclude: string[]): RegExp { + // ignore an empty exclusion list + if (!exclude || exclude.length === 0) { + return undefined; + } + + prefix = regExpEscape(prefix); + + let pattern = ""; + for (let excludeSpec of exclude) { + // skip empty entries. + if (!excludeSpec) continue; + + let normalizedExcludeSpec = normalizePath(excludeSpec); + if (invalidRecursiveWildcardPattern.test(normalizedExcludeSpec)) { + errors.push(createCompilerDiagnostic(Diagnostics.File_specification_cannot_contain_multiple_recursive_directory_wildcards_Asterisk_Asterisk_Colon_0, excludeSpec)); + } + else { + if (pattern) { + pattern += "|"; + } + + normalizedExcludeSpec = normalizedExcludeSpec.replace(excludeWildcardParserPattern, excludeWildcardTokenReplacer); + pattern += "(" + combinePaths(prefix, normalizedExcludeSpec) + ")"; + } + } + + if (pattern) { + return new RegExp("^(" + pattern + ")$", ignoreCase ? "i" : ""); + } + + return undefined; + } + + /** + * Replaces a wildcard token with an encoded regular expression pattern. + * @param raw The raw matched string + * @param multiChar The match for the multi-character wildcard token ('*'). + * @param singleChar The match for the single-character wildcard token ('?'). + * @param range The match for a character-range wildcard token ('[a-z]'). + * @param meta A token that needs to be encoded in a regular expression. + */ + function wildcardTokenReplacer(raw: string, multiChar: string, singleChar: string, range: string, meta: string) { + if (multiChar) return ".*"; + if (singleChar) return ".?"; + if (range) return range.replace(rangeParserPattern, rangeTokenReplacer) + if (meta) return "\\" + meta; + return raw; + } + + /** + * Replaces a wildcard token with an encoded regular expression pattern. + * @param raw The raw matched string + * @param recursive The match for the recursive directory wildcard token ('**'). Capture group $1 + * @param multiChar The match for the multi-character wildcard token ('*'). Capture group $2 + * @param singleChar The match for the single-character wildcard token ('?'). Capture group $3 + * @param range The match for a character-range wildcard token ('[a-z]'). Capture group $4 + * @param meta A token that needs to be encoded in a regular expression. Capture group $5 + */ + function excludeWildcardTokenReplacer(raw: string, recursive: string, multiChar: string, singleChar: string, range: string, meta: string) { + if (recursive) return "(^|/)(.*(/|$))?"; + if (multiChar) return ".*"; + if (singleChar) return ".?"; + if (range) return range.replace(rangeParserPattern, rangeTokenReplacer) + if (meta) return "\\" + meta; + return raw; + } + + /** + * Replaces a range token with an encoded regular expression pattern. + * @param raw The raw matched string. + * @param open The opening bracket of the range ('['). + * @param not The negation token for a range ('!'). + * @param meta A token that needs to be encoded in a regular expression. + * @param close The closing bracket of the range (']'). + */ + function rangeTokenReplacer(raw: string, open: string, not: string, meta: string, close: string) { + if (open) return not ? "[^" : "["; + if (meta) return "\\" + meta; + if (close) return "]"; + return raw; + } + + /** + * Escape regular expression reserved tokens. + * @param text The text to escape. + */ + function regExpEscape(text: string) { + return text.replace(regExpReservedCharacterPattern, token => "\\" + token); } - return files; } } } diff --git a/src/compiler/core.ts b/src/compiler/core.ts index ef7c892be9d31..416e1d0fddd7f 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -380,6 +380,19 @@ module ts { if (b === undefined) return Comparison.GreaterThan; return a < b ? Comparison.LessThan : Comparison.GreaterThan; } + + export function compareStrings(a: string, b: string, ignoreCase?: boolean) { + if (a === b) return Comparison.EqualTo; + if (a === undefined) return Comparison.LessThan; + if (b === undefined) return Comparison.GreaterThan; + if (ignoreCase) { + a = a.toLowerCase(); + b = b.toLowerCase(); + 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; @@ -643,7 +656,46 @@ module ts { if (path1.charAt(path1.length - 1) === directorySeparator) return path1 + path2; 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 compareNormalizedPaths(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; + let aComponents = getNormalizedPathComponents(a, currentDirectory); + let bComponents = getNormalizedPathComponents(b, currentDirectory); + let sharedLength = Math.min(aComponents.length, bComponents.length); + for (let i = 0; i < sharedLength; ++i) { + let result = compareStrings(aComponents[i], bComponents[i], ignoreCase); + if (result) return result; + } + + return compareValues(aComponents.length, bComponents.length); + } + export function fileExtensionIs(path: string, extension: string): boolean { let pathLen = path.length; let extLen = extension.length; @@ -654,6 +706,10 @@ module ts { * List of supported extensions in order of file resolution precedence. */ export const supportedExtensions = [".ts", ".d.ts"]; + + export function hasSupportedFileExtension(file: string) { + return forEach(supportedExtensions, extension => fileExtensionIs(file, extension)); + } const extensionsToRemove = [".d.ts", ".ts", ".js"]; export function removeFileExtension(path: string): string { diff --git a/src/compiler/diagnosticInformationMap.generated.ts b/src/compiler/diagnosticInformationMap.generated.ts index 2a8527168464a..8fd58e4fe4d3c 100644 --- a/src/compiler/diagnosticInformationMap.generated.ts +++ b/src/compiler/diagnosticInformationMap.generated.ts @@ -438,6 +438,7 @@ module ts { Loop_contains_block_scoped_variable_0_referenced_by_a_function_in_the_loop_This_is_only_supported_in_ECMAScript_6_or_higher: { code: 4091, category: DiagnosticCategory.Error, key: "Loop contains block-scoped variable '{0}' referenced by a function in the loop. This is only supported in ECMAScript 6 or higher." }, The_current_host_does_not_support_the_0_option: { code: 5001, category: DiagnosticCategory.Error, key: "The current host does not support the '{0}' option." }, Cannot_find_the_common_subdirectory_path_for_the_input_files: { code: 5009, category: DiagnosticCategory.Error, key: "Cannot find the common subdirectory path for the input files." }, + File_specification_cannot_contain_multiple_recursive_directory_wildcards_Asterisk_Asterisk_Colon_0: { code: 5011, category: DiagnosticCategory.Error, key: "File specification cannot contain multiple recursive directory wildcards ('**'): '{0}'." }, Cannot_read_file_0_Colon_1: { code: 5012, category: DiagnosticCategory.Error, key: "Cannot read file '{0}': {1}" }, Unsupported_file_encoding: { code: 5013, category: DiagnosticCategory.Error, key: "Unsupported file encoding." }, Failed_to_parse_file_0_Colon_1: { code: 5014, category: DiagnosticCategory.Error, key: "Failed to parse file '{0}': {1}." }, diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index cf1645b140724..a5459e974bcfc 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -1741,6 +1741,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 f9daf52c5f28d..5afec57d4da64 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -16,6 +16,7 @@ module ts { getExecutingFilePath(): string; getCurrentDirectory(): string; readDirectory(path: string, extension?: string): string[]; + readDirectoryFlat(path: string): string[]; getMemoryUsage?(): number; exit(exitCode?: number): void; } @@ -135,6 +136,11 @@ module ts { } } } + + function readDirectoryFlat(path: string): string[] { + var folder = fso.GetFolder(path || "."); + return [...getNames(folder.files), ...getNames(folder.subfolders)]; + } return { args, @@ -166,6 +172,7 @@ module ts { return new ActiveXObject("WScript.Shell").CurrentDirectory; }, readDirectory, + readDirectoryFlat, exit(exitCode?: number): void { try { WScript.Quit(exitCode); @@ -246,6 +253,10 @@ module ts { } } } + + function readDirectoryFlat(path: string): string[] { + return _fs.readdirSync(path || ".").sort(); + } return { args: process.argv.slice(2), @@ -294,6 +305,7 @@ module ts { return process.cwd(); }, readDirectory, + readDirectoryFlat, getMemoryUsage() { if (global.gc) { global.gc(); diff --git a/src/compiler/tsc.ts b/src/compiler/tsc.ts index 15cc665e35177..24a9283482573 100644 --- a/src/compiler/tsc.ts +++ b/src/compiler/tsc.ts @@ -242,7 +242,7 @@ module ts { setCachedProgram(compileResult.program); reportDiagnostic(createCompilerDiagnostic(Diagnostics.Compilation_complete_Watching_for_file_changes)); } - + function getSourceFile(fileName: string, languageVersion: ScriptTarget, onError ?: (message: string) => void) { // Return existing SourceFile object if one is available if (cachedProgram) { diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 86e680ca8b047..98ad1f3588b9a 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -1027,7 +1027,7 @@ module ts { // This field should never be used directly to obtain line map, use getLineMap function instead. /* @internal */ lineMap: number[]; } - + export interface ScriptReferenceHost { getCompilerOptions(): CompilerOptions; getSourceFile(fileName: string): SourceFile; @@ -1035,7 +1035,27 @@ module ts { } export interface ParseConfigHost { + useCaseSensitiveFileNames: boolean; + readDirectory(rootDir: string, extension: string): string[]; + + /** + * Gets a value indicating whether the specified path exists. + * @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 in the directory. + * @param path The directory path. + */ + readDirectoryFlat(path: string): string[]; } export interface WriteFileCallback { diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index 0e0b8d829183d..2c87f2e6b93f1 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -232,6 +232,10 @@ module Harness.LanguageService { readDirectory(rootDir: string, extension: string): string { throw new Error("NYI"); } + + readDirectoryFlat(path: string): string { + throw new Error("NYI"); + } log(s: string): void { this.nativeHost.log(s); } trace(s: string): void { this.nativeHost.trace(s); } @@ -537,6 +541,10 @@ module Harness.LanguageService { throw new Error("Not implemented Yet."); } + readDirectoryFlat(path: string): string[] { + throw new Error("Not implemented Yet."); + } + watchFile(fileName: string, callback: (fileName: string) => void): ts.FileWatcher { return { close() { } }; } diff --git a/src/services/shims.ts b/src/services/shims.ts index 27c70ddc878a4..7de930cce319a 100644 --- a/src/services/shims.ts +++ b/src/services/shims.ts @@ -62,6 +62,10 @@ module ts { export interface CoreServicesShimHost extends Logger { /** Returns a JSON-encoded value of the type: string[] */ readDirectory(rootDir: string, extension: string): string; + readDirectoryFlat?(path: string): string; + useCaseSensitiveFileNames?: boolean; + fileExists?(path: string): boolean; + directoryExists?(path: string): boolean; } /// @@ -335,13 +339,53 @@ module 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): string[] { var encoded = this.shimHost.readDirectory(rootDir, extension); return JSON.parse(encoded); } + + public readDirectoryFlat(path: string): string[] { + path = normalizePath(path); + path = ensureTrailingDirectorySeparator(path); + if (this.shimHost.readDirectoryFlat) { + var encoded = this.shimHost.readDirectoryFlat(path); + return JSON.parse(encoded); + } + + return sys ? sys.readDirectoryFlat(path): []; + } + + public fileExists(path: string): boolean { + path = normalizePath(path); + if (this.shimHost.fileExists) { + return this.shimHost.fileExists(path); + } + + return sys ? sys.fileExists(path) : undefined; + } + + public directoryExists(path: string): boolean { + path = normalizePath(path); + if (this.shimHost.directoryExists) { + return this.shimHost.directoryExists(path); + } + + return sys ? sys.directoryExists(path) : undefined; + } } function simpleForwardCall(logger: Logger, actionDescription: string, action: () => any, noPerfLogging: boolean): any { From 00a840aa4d5e24443c82194700df36ee50031230 Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Tue, 19 May 2015 15:47:59 -0700 Subject: [PATCH 02/11] Support for wildcard exclude when 'files' is missing in tsconfig --- src/compiler/commandLineParser.ts | 57 +++++++++++++++++++------------ 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index cfc130fe76bae..add4384bd4cfa 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -344,7 +344,7 @@ module ts { return { options, - fileNames: getFiles(), + fileNames: getFileNames(), errors }; @@ -390,29 +390,31 @@ module ts { return options; } - function getFiles(): string[] { + function getFileNames(): string[] { + var exclude = json["exclude"] instanceof Array ? json["exclude"] : undefined; if (hasProperty(json, "files")) { if (json["files"] instanceof Array) { - var files = json["files"]; - - var exclude: string[]; - if (hasProperty(json, "exclude") && json["exclude"] instanceof Array) { - exclude = json["exclude"]; - } - - return expandFiles(files, exclude); + var fileNames = json["files"]; + return expandFiles(fileNames, exclude, /*literalFiles*/ false); } + + return []; } else { - var files: string[] = []; + var fileNames: string[] = []; var sysFiles = host.readDirectory(basePath, ".ts"); for (var i = 0; i < sysFiles.length; i++) { var name = sysFiles[i]; if (!fileExtensionIs(name, ".d.ts") || !contains(sysFiles, name.substr(0, name.length - 5) + ".ts")) { - files.push(name); + fileNames.push(name); } } - return files; + + if (!exclude) { + return fileNames; + } + + return expandFiles(fileNames, exclude, /*literalFiles*/ true); } } @@ -420,8 +422,9 @@ module ts { * Expands an array of file specifications. * @param files The file specifications to expand. * @param exclude Any file specifications to exclude. + * @param literalFiles A value indicating whether the files array are literal files and not wildcards. */ - function expandFiles(files: string[], exclude: string[]): string[] { + function expandFiles(files: string[], exclude: string[], literalFiles: boolean): string[] { // Used to verify the file specification does not have multiple recursive directory wildcards. let invalidRecursiveWildcardPattern = /(^|\/)\*\*\/(.*\/)?\*\*(\/|$)/; @@ -493,15 +496,25 @@ module ts { // populate the exclusion list let excludePattern = createExcludePattern(prefix, exclude); - // expand and include the provided files into the file set. - for (let fileSpec of files) { - let normalizedFileSpec = normalizePath(fileSpec); - normalizedFileSpec = removeTrailingDirectorySeparator(normalizedFileSpec); - if (invalidRecursiveWildcardPattern.test(normalizedFileSpec)) { - errors.push(createCompilerDiagnostic(Diagnostics.File_specification_cannot_contain_multiple_recursive_directory_wildcards_Asterisk_Asterisk_Colon_0, fileSpec)) + if (literalFiles) { + // process each file spec as a literal file entry. + for (let fileSpec of files) { + let normalizedFileSpec = normalizePath(fileSpec); + normalizedFileSpec = combinePaths(basePath, normalizedFileSpec); + includeFile(normalizedFileSpec); } - else { - expandDirectory(prefix, normalizedFileSpec, 0); + } + else { + // expand and include the provided files into the file set. + for (let fileSpec of files) { + let normalizedFileSpec = normalizePath(fileSpec); + normalizedFileSpec = removeTrailingDirectorySeparator(normalizedFileSpec); + if (invalidRecursiveWildcardPattern.test(normalizedFileSpec)) { + errors.push(createCompilerDiagnostic(Diagnostics.File_specification_cannot_contain_multiple_recursive_directory_wildcards_Asterisk_Asterisk_Colon_0, fileSpec)) + } + else { + expandDirectory(prefix, normalizedFileSpec, 0); + } } } From 326e2598409ceb65aa562d9b262caffb0f494469 Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Tue, 19 May 2015 15:49:14 -0700 Subject: [PATCH 03/11] Fixed typo in comment --- src/compiler/commandLineParser.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index add4384bd4cfa..a3457413fb609 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -455,7 +455,6 @@ module ts { // - At the end of the wildcard, preceeded by `/`. // - Anywhere in the pattern if preceeded and followed by `/`. // These tokens, if matched, will be stored in capture group $1. - // the pattern and followed by a directory separator // `*` - Matches zero or more characters. This token, if matched, will be // stored in capture group $2. // `?` - Matches zero or one character. This token, if matched, will be From 198ff551692ee53c933a64d53a5b1771aaf7f7fc Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Tue, 19 May 2015 17:41:38 -0700 Subject: [PATCH 04/11] Cleanup, rewrote possibly confusing regular expressions. --- src/compiler/commandLineParser.ts | 522 +++++++++++++++++------------- 1 file changed, 297 insertions(+), 225 deletions(-) diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index a3457413fb609..e3a7f86c184ba 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -331,6 +331,11 @@ module ts { return { error: createCompilerDiagnostic(Diagnostics.Failed_to_parse_file_0_Colon_1, fileName, e.message) }; } } + + const enum ExpandResult { + Ok, + Error + } /** * Parse the contents of a config file (tsconfig.json). @@ -424,146 +429,104 @@ module ts { * @param exclude Any file specifications to exclude. * @param literalFiles A value indicating whether the files array are literal files and not wildcards. */ - function expandFiles(files: string[], exclude: string[], literalFiles: boolean): string[] { - // Used to verify the file specification does not have multiple recursive directory wildcards. - let invalidRecursiveWildcardPattern = /(^|\/)\*\*\/(.*\/)?\*\*(\/|$)/; - - // Used to parse glob-style wildcards, this pattern matches the following tokens: - // - // `*` - Matches zero or more characters. This token, if matched, will be - // stored in capture group $1. - // `?` - Matches zero or one character. This token, if matched, will be - // stored in capture group $2. - // `[` RangeCharacters `]` - Matches a zero or one character in the specified range of - // characters, where RangeCharacters consists of any character not - // in the set { `*`, `?`, `]` }. These tokens, if matched, will be - // stored in capture group $3. - // ReservedCharacters - Where ReservedCharacters are tokens with special meaning in - // the regular expression grammar that need to be escaped. - // This includes characters in the set { `+`, `.`, `^`, `$`, `(`, `)`, - // `{`, `}`, `[`, `]`, `|` }. These tokens, if matched, will be stored - // in capture group $4. - // DirectorySeparators - Characters in the set { `/`, `\` }. - let wildcardParserPattern = /(\*)|(\?)|(\[[^?*\]]*\])|([+.^$(){}\[\]|])/g; - - // Used to parse glob-style wildcards for exclude paths, this pattern matches the following tokens: - // - // `**` - Matches a recursive directory, but only when found in the following - // context: - // - As the whole wildcard. - // - At the start of the wildcard, followed by `/`. - // - At the end of the wildcard, preceeded by `/`. - // - Anywhere in the pattern if preceeded and followed by `/`. - // These tokens, if matched, will be stored in capture group $1. - // `*` - Matches zero or more characters. This token, if matched, will be - // stored in capture group $2. - // `?` - Matches zero or one character. This token, if matched, will be - // stored in capture group $3. - // `[` RangeCharacters `]` - Matches a zero or one character in the specified range of - // characters, where RangeCharacters consists of any character not - // in the set { `*`, `?`, `]` }. These tokens, if matched, will be - // stored in capture group $4. - // ReservedCharacters - Where ReservedCharacters are tokens with special meaning in - // the regular expression grammar that need to be escaped. - // This includes characters in the set { `+`, `.`, `^`, `$`, `(`, `)`, - // `{`, `}`, `[`, `]`, `|` }. These tokens, if matched, will be stored - // in capture group $5. - let excludeWildcardParserPattern = /((?:^|\/)\*\*(?:\/|$))|(\*)|(\?)|(\[[^?*\/\]]*\])|([+.^$(){}\[\]|])/g; - - // Used to parse a glob-style character range, this pattern matches the following tokens: - // - // `[` - The opening bracket of the range, if at the start of the string. - // This token, if matched, will be stored in capture group $1. - // `!` - Indicates the range should match any character except those listed in - // the range, if found immediately following the opening bracket of the - // range. This token, if matched, will be stored in capture group $2. - // ReservedCharacters - Where ReservedCharacters are tokens with special meaning in the - // regular expression grammar that need to be escaped. This includes - // characters in the set { `^`, `[` }. These tokens, if matched, - // will be stored in capture group $3. - // `]` - Matches the closing bracket of the range, if at the end of the string. - // This token, if matched, will be stored in capture group $4. - let rangeParserPattern = /(^\[)(!)?|([\^\[])|(\]$)/g; - - // Used to match characters with special meaning in a regular expression. - let regExpReservedCharacterPattern = /[?*+.^$(){}\[\]|]/; - - let prefix = normalizePath(basePath); + function expandFiles(fileSpecs: string[], excludeSpecs: string[], literalFiles: boolean): string[] { + let normalizedBasePath = normalizePath(basePath); let ignoreCase = !host.useCaseSensitiveFileNames; - let fileSet: Map = {}; + let isExpandingRecursiveDirectory = false; + let fileSet: StringSet = {}; + let output: string[] = []; - // populate the exclusion list - let excludePattern = createExcludePattern(prefix, exclude); + // populate the file exclusion pattern + let excludePattern = createExcludeRegularExpression(normalizedBasePath, excludeSpecs); - if (literalFiles) { - // process each file spec as a literal file entry. - for (let fileSpec of files) { - let normalizedFileSpec = normalizePath(fileSpec); + // expand and include the provided files into the file set. + for (let fileSpec of fileSpecs) { + let normalizedFileSpec = normalizePath(fileSpec); + if (literalFiles) { normalizedFileSpec = combinePaths(basePath, normalizedFileSpec); includeFile(normalizedFileSpec); } - } - else { - // expand and include the provided files into the file set. - for (let fileSpec of files) { - let normalizedFileSpec = normalizePath(fileSpec); + else { normalizedFileSpec = removeTrailingDirectorySeparator(normalizedFileSpec); - if (invalidRecursiveWildcardPattern.test(normalizedFileSpec)) { - errors.push(createCompilerDiagnostic(Diagnostics.File_specification_cannot_contain_multiple_recursive_directory_wildcards_Asterisk_Asterisk_Colon_0, fileSpec)) - } - else { - expandDirectory(prefix, normalizedFileSpec, 0); - } + expandDirectory(normalizedBasePath, normalizedFileSpec, 0); } } - let result: string[] = []; - forEachValue(fileSet, value => { result.push(value); }); - return result; + return output; + /** + * Expands a directory with wildcards. + * @param prefix The directory to expand. + * @param fileSpec The original file specification. + * @param offset The offset into the file specification. + */ + function expandDirectory(basePath: string, fileSpec: string, start: number): ExpandResult { + // 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 process the match if the file exists and was not excluded. + let path = combinePaths(basePath, fileSpec.substring(start)); + includeFile(path); + return ExpandResult.Ok; + } + + // find the last directory separator before the wildcard to get the leading path. + let end = fileSpec.lastIndexOf(directorySeparator, offset); + if (end > start) { + // wildcard occurs in a later segment, include remaining path up to wildcard in prefix + basePath = combinePaths(basePath, fileSpec.substring(start, end)); + start = end + 1; + } + + // Expand the wildcard portion of the path. + return expandWildcard(basePath, fileSpec, start); + } + /** * Expands a wildcard portion of a file specification. * @param prefix The prefix path. * @param fileSpec The file specification. * @param offset The offset in the file specification. */ - function expandWildcard(prefix: string, fileSpec: string, offset: number): void { + function expandWildcard(basePath: string, fileSpec: string, start: number): ExpandResult { // find the offset of the next directory separator to extract the wildcard path segment. - let endOffset = fileSpec.indexOf(directorySeparator, offset); - let endOfLine = endOffset < 0; - let wildcard = fileSpec.substring(offset, endOfLine ? fileSpec.length : endOffset); - if (wildcard === "**") { + let offset = fileSpec.indexOf(directorySeparator, start); + let isEndOfLine = offset < 0; + if (isRecursiveDirectoryWildcard(fileSpec, start)) { // If the path contains only "**", then this is a recursive directory pattern. - if (endOfLine) { - // If there is no file specification following the recursive directory pattern, - // we cannot match any files, so we will ignore this pattern - return; + if (isEndOfLine) { + // 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; } - + // expand the recursive directory pattern - expandRecursiveDirectory(prefix, fileSpec, endOffset + 1); + return expandRecursiveDirectory(basePath, fileSpec, offset + 1); } - else { - // match the entries in the directory against the wildcard pattern. - let entries = host.readDirectoryFlat(prefix); - let pattern = createPatternFromWildcard(wildcard); - for (let entry of entries) { - // skip the entry if it does not match the pattern. - if (!pattern.test(entry)) { - continue; - } - - let path = combinePaths(prefix, entry); - if (endOfLine) { - // This wildcard has no further directory so process the match. - includeFile(path); - } - else if (host.directoryExists(path)) { - // If this was a directory, process the directory. - expandDirectory(path, fileSpec, endOffset + 1); + + // match the entries in the directory against the wildcard pattern. + let entries = host.readDirectoryFlat(basePath); + let pattern = createRegularExpressionFromWildcard(fileSpec, start, isEndOfLine ? fileSpec.length : offset); + for (let entry of entries) { + // skip the entry if it does not match the pattern. + if (!pattern.test(entry)) { + continue; + } + + let path = combinePaths(basePath, entry); + if (isEndOfLine) { + // This wildcard has no further directory so process the match. + includeFile(path); + } + else if (host.directoryExists(path)) { + // If this was a directory, process the directory. + if (expandDirectory(path, fileSpec, offset + 1) === ExpandResult.Error) { + return ExpandResult.Error; } } } + + return ExpandResult.Ok; } /** @@ -572,46 +535,32 @@ module ts { * @param fileSpec The original file specification * @param offset The offset into the file specification */ - function expandRecursiveDirectory(prefix: string, fileSpec: string, offset: number): void { + function expandRecursiveDirectory(basePath: string, fileSpec: string, start: number): ExpandResult { + if (isExpandingRecursiveDirectory) { + errors.push(createCompilerDiagnostic(Diagnostics.File_specification_cannot_contain_multiple_recursive_directory_wildcards_Asterisk_Asterisk_Colon_0, fileSpec)) + return ExpandResult.Error; + } + // expand the non-recursive part of the file specification against the prefix path. - expandDirectory(prefix, fileSpec, offset); + isExpandingRecursiveDirectory = true; + let result = expandDirectory(basePath, fileSpec, start); + isExpandingRecursiveDirectory = false; - // recursively expand each subdirectory. - let entries = host.readDirectoryFlat(prefix); - for (let entry of entries) { - let path = combinePaths(prefix, entry); - if (host.directoryExists(path)) { - expandRecursiveDirectory(path, fileSpec, offset); + if (result !== ExpandResult.Error) { + // recursively expand each subdirectory. + let entries = host.readDirectoryFlat(basePath); + for (let entry of entries) { + let path = combinePaths(basePath, entry); + if (host.directoryExists(path)) { + if (expandRecursiveDirectory(path, fileSpec, start) === ExpandResult.Error) { + result = ExpandResult.Error; + break; + } + } } } - } - - /** - * Expands a directory with wildcards. - * @param prefix The directory to expand. - * @param fileSpec The original file specification. - * @param offset The offset into the file specification. - */ - function expandDirectory(prefix: string, fileSpec: string, offset: number): void { - // find the offset of the next wildcard in the file specification - let wildcardOffset = indexOfWildcard(fileSpec, offset); - if (wildcardOffset < 0) { - // There were no more wildcards, so process the match if the file exists and was not excluded. - let path = combinePaths(prefix, fileSpec.substring(offset)); - includeFile(path); - return; - } - - // find the last directory separator before the wildcard to get the leading path. - let endOffset = fileSpec.lastIndexOf(directorySeparator, wildcardOffset); - if (endOffset > offset) { - // wildcard occurs in a later segment, include remaining path up to wildcard in prefix - prefix = combinePaths(prefix, fileSpec.substring(offset, endOffset)); - offset = endOffset + 1; - } - - // Expand the wildcard portion of the path. - expandWildcard(prefix, fileSpec, offset); + + return result; } /** @@ -640,33 +589,75 @@ module ts { return; } - fileSet[key] = file; + fileSet[key] = true; + output.push(file); } /** - * Gets the index of the next wildcard character in a file specification. - * @param fileSpec The file specification. - * @param start The offset at which to start the search. + * Creates a regular expression from a glob-style wildcard. + * @param wildcard The wildcard */ - function indexOfWildcard(fileSpec: string, start: number): number { - const len = fileSpec.length; - for (let i = start; i < len; ++i) { - let charCode = fileSpec.charCodeAt(i); - if (charCode === 42 /*asterisk*/ || - charCode === 63 /*question*/ || - charCode === 91 /*openBracket*/) { - return i; - } - } - return -1; + function createRegularExpressionFromWildcard(fileSpec: string, start: number, end: number): RegExp { + let pattern = createPatternFromWildcard(fileSpec, start, end); + return new RegExp("^" + pattern + "$", ignoreCase ? "i" : ""); } - + /** - * Creates a regular expression from a glob-style wildcard. - * @param wildcard The wildcard + * Creates a pattern from a wildcard segment */ - function createPatternFromWildcard(wildcard: string): RegExp { - return new RegExp("^" + wildcard.replace(wildcardParserPattern, wildcardTokenReplacer) + "$", ignoreCase ? "i" : ""); + 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 += escapePatternForRegularExpression(fileSpec, start, offset); + } + + let 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 (one character) pattern to the regular expression + pattern += "."; + } + else if (charCode === CharacterCodes.openBracket) { + // Append a character range (one character) pattern to the regular expression + pattern += "["; + + // If the next character is an exclamation token, append a caret (^) to negate the + // character range and advance the start of the range by one. + start = offset + 1; + charCode = fileSpec.charCodeAt(start); + if (charCode === CharacterCodes.exclamation) { + pattern += "^"; + start++; + } + + // Find the end of the character range. If it can't be found, fix up the range + // to the end of the wildcard + offset = fileSpec.indexOf(']', start); + if (offset < 0 || offset > end) { + offset = end; + } + + // Escape and append the character range + pattern += escapePatternForRegularExpression(fileSpec, start, offset); + pattern += "]"; + } + + start = offset + 1; + offset = indexOfWildcard(fileSpec, start); + } + + // Escape and append any remaining non-wildcard portion. + if (start < end) { + pattern += escapePatternForRegularExpression(fileSpec, start, end); + } + + return pattern; } /** @@ -674,95 +665,176 @@ module ts { * @param prefix The prefix path * @param exclude The file specifications to exclude. */ - function createExcludePattern(prefix: string, exclude: string[]): RegExp { + function createExcludeRegularExpression(basePath: string, excludeSpecs: string[]): RegExp { // ignore an empty exclusion list - if (!exclude || exclude.length === 0) { + if (!excludeSpecs || excludeSpecs.length === 0) { return undefined; } - prefix = regExpEscape(prefix); + basePath = escapePatternForRegularExpression(basePath, 0, basePath.length); let pattern = ""; - for (let excludeSpec of exclude) { + for (let excludeSpec of excludeSpecs) { // skip empty entries. - if (!excludeSpec) continue; + if (!excludeSpec) { + continue; + } let normalizedExcludeSpec = normalizePath(excludeSpec); - if (invalidRecursiveWildcardPattern.test(normalizedExcludeSpec)) { - errors.push(createCompilerDiagnostic(Diagnostics.File_specification_cannot_contain_multiple_recursive_directory_wildcards_Asterisk_Asterisk_Colon_0, excludeSpec)); - } - else { + let excludePattern = createExcludePattern(normalizedExcludeSpec); + if (excludePattern) { if (pattern) { pattern += "|"; } - normalizedExcludeSpec = normalizedExcludeSpec.replace(excludeWildcardParserPattern, excludeWildcardTokenReplacer); - pattern += "(" + combinePaths(prefix, normalizedExcludeSpec) + ")"; + pattern += "(" + combinePaths(basePath, excludePattern) + ")"; } } - + if (pattern) { return new RegExp("^(" + pattern + ")$", ignoreCase ? "i" : ""); } return undefined; } + + /** + * Creates a pattern for the excludePattern regular expression used to exclude a file. + * @param exclude The file specification to exclude. + */ + function createExcludePattern(fileSpec: string): string { + let hasRecursiveDirectoryWildcard = false; + let pattern = ""; + let start = 0; + let end = fileSpec.length; + let offset = fileSpec.indexOf(directorySeparator, start); + while (offset >= 0 && offset < end) { + if (isRecursiveDirectoryWildcard(fileSpec, start)) { + if (hasRecursiveDirectoryWildcard) { + errors.push(createCompilerDiagnostic(Diagnostics.File_specification_cannot_contain_multiple_recursive_directory_wildcards_Asterisk_Asterisk_Colon_0, fileSpec)); + return undefined; + } + + hasRecursiveDirectoryWildcard = true; + } + + if (offset > start) { + pattern += createPatternFromWildcard(fileSpec, start, offset); + } + + pattern += fileSpec.charAt(offset); + start = offset + 1; + offset = fileSpec.indexOf(directorySeparator, start); + } + + if (start < end) { + pattern += createPatternFromWildcard(fileSpec, start, end); + } + + return pattern; + } /** - * Replaces a wildcard token with an encoded regular expression pattern. - * @param raw The raw matched string - * @param multiChar The match for the multi-character wildcard token ('*'). - * @param singleChar The match for the single-character wildcard token ('?'). - * @param range The match for a character-range wildcard token ('[a-z]'). - * @param meta A token that needs to be encoded in a regular expression. + * Escape regular expression reserved tokens. + * @param text The text to escape. */ - function wildcardTokenReplacer(raw: string, multiChar: string, singleChar: string, range: string, meta: string) { - if (multiChar) return ".*"; - if (singleChar) return ".?"; - if (range) return range.replace(rangeParserPattern, rangeTokenReplacer) - if (meta) return "\\" + meta; - return raw; + function escapePatternForRegularExpression(text: string, start: number, end: number) { + let offset = indexOfReservedCharacter(text, start); + if (offset < 0) { + return text; + } + + let escaped: string = ""; + while (offset >= 0 && offset < end) { + if (offset > start) { + escaped += text.substring(start, offset); + } + + escaped += "\\"; + escaped += text.charAt(offset); + start = offset + 1; + offset = indexOfReservedCharacter(text, start); + } + + if (start < end) { + escaped += text.substring(start, end); + } + + return escaped; } /** - * Replaces a wildcard token with an encoded regular expression pattern. - * @param raw The raw matched string - * @param recursive The match for the recursive directory wildcard token ('**'). Capture group $1 - * @param multiChar The match for the multi-character wildcard token ('*'). Capture group $2 - * @param singleChar The match for the single-character wildcard token ('?'). Capture group $3 - * @param range The match for a character-range wildcard token ('[a-z]'). Capture group $4 - * @param meta A token that needs to be encoded in a regular expression. Capture group $5 + * Determines whether the wildcard at the current offset is a recursive directory wildcard. + * @param fileSpec The file specification. + * @param offset The offset into the file specification. */ - function excludeWildcardTokenReplacer(raw: string, recursive: string, multiChar: string, singleChar: string, range: string, meta: string) { - if (recursive) return "(^|/)(.*(/|$))?"; - if (multiChar) return ".*"; - if (singleChar) return ".?"; - if (range) return range.replace(rangeParserPattern, rangeTokenReplacer) - if (meta) return "\\" + meta; - return raw; + function isRecursiveDirectoryWildcard(fileSpec: string, offset: number) { + if (offset + 2 <= fileSpec.length && + fileSpec.charCodeAt(offset) === CharacterCodes.asterisk && + fileSpec.charCodeAt(offset + 1) === CharacterCodes.asterisk && + isDirectorySeparatorOrEndOfLine(fileSpec, offset + 2)) { + return true; + } + + return false; } - + /** - * Replaces a range token with an encoded regular expression pattern. - * @param raw The raw matched string. - * @param open The opening bracket of the range ('['). - * @param not The negation token for a range ('!'). - * @param meta A token that needs to be encoded in a regular expression. - * @param close The closing bracket of the range (']'). + * Determines whether the provided offset points to a directory separator or the end of the line. + * @param fileSpec The file specification. + * @param offset The offset into the file specification. */ - function rangeTokenReplacer(raw: string, open: string, not: string, meta: string, close: string) { - if (open) return not ? "[^" : "["; - if (meta) return "\\" + meta; - if (close) return "]"; - return raw; + function isDirectorySeparatorOrEndOfLine(fileSpec: string, offset: number) { + return offset === fileSpec.length || (offset < fileSpec.length && fileSpec.charCodeAt(offset) === CharacterCodes.slash); } /** - * Escape regular expression reserved tokens. - * @param text The text to escape. + * Gets the index of the next wildcard character in a file specification. + * @param fileSpec The file specification. + * @param start The offset at which to start the search. + */ + function indexOfWildcard(fileSpec: string, start: number): number { + const len = fileSpec.length; + for (let i = start; i < len; ++i) { + let charCode = fileSpec.charCodeAt(i); + if (charCode === CharacterCodes.asterisk || + charCode === CharacterCodes.question /*question*/ || + charCode === CharacterCodes.openBracket) { + return i; + } + } + + return -1; + } + + /** + * Gets the index of the next reserved character in a regular expression. + * @param text The text to search. + * @param startOffset The offset at which to start the search. */ - function regExpEscape(text: string) { - return text.replace(regExpReservedCharacterPattern, token => "\\" + token); + function indexOfReservedCharacter(text: string, start: number) { + const len = text.length; + for (let i = start; i < len; ++i) { + let charCode = text.charCodeAt(i); + switch (charCode) { + case CharacterCodes.question: + case CharacterCodes.asterisk: + case CharacterCodes.plus: + case CharacterCodes.dot: + case CharacterCodes.caret: + case CharacterCodes.$: + case CharacterCodes.openParen: + case CharacterCodes.closeParen: + case CharacterCodes.openBrace: + case CharacterCodes.closeBrace: + case CharacterCodes.openBracket: + case CharacterCodes.closeBracket: + case CharacterCodes.bar: + return i; + } + } + + return -1; } } } From c19dc26474cdf7fb23ef3761f220a70f6947d4ee Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Tue, 19 May 2015 17:48:24 -0700 Subject: [PATCH 05/11] Cleanup, comments --- src/compiler/commandLineParser.ts | 83 ++++++++++++++++--------------- 1 file changed, 44 insertions(+), 39 deletions(-) diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index e3a7f86c184ba..4d454ed246a31 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -398,12 +398,8 @@ module ts { function getFileNames(): string[] { var exclude = json["exclude"] instanceof Array ? json["exclude"] : undefined; if (hasProperty(json, "files")) { - if (json["files"] instanceof Array) { - var fileNames = json["files"]; - return expandFiles(fileNames, exclude, /*literalFiles*/ false); - } - - return []; + var fileNames = json["files"] instanceof Array ? json["files"] : undefined; + return expandFiles(fileNames, exclude, /*literalFiles*/ false); } else { var fileNames: string[] = []; @@ -425,16 +421,20 @@ module ts { /** * Expands an array of file specifications. - * @param files The file specifications to expand. - * @param exclude Any file specifications to exclude. - * @param literalFiles A value indicating whether the files array are literal files and not wildcards. + * @param fileSpecs The file specifications to expand. + * @param excludeSpecs Any file specifications to exclude. + * @param literalFiles A value indicating whether the fileSpecs array contains only literal file names and not wildcards. */ function expandFiles(fileSpecs: string[], excludeSpecs: string[], literalFiles: boolean): string[] { + let output: string[] = []; + if (!fileSpecs) { + return output; + } + let normalizedBasePath = normalizePath(basePath); let ignoreCase = !host.useCaseSensitiveFileNames; let isExpandingRecursiveDirectory = false; let fileSet: StringSet = {}; - let output: string[] = []; // populate the file exclusion pattern let excludePattern = createExcludeRegularExpression(normalizedBasePath, excludeSpecs); @@ -456,9 +456,9 @@ module ts { /** * Expands a directory with wildcards. - * @param prefix The directory to expand. + * @param basePath The directory to expand. * @param fileSpec The original file specification. - * @param offset The offset into the file specification. + * @param start The starting offset in the file specification. */ function expandDirectory(basePath: string, fileSpec: string, start: number): ExpandResult { // find the offset of the next wildcard in the file specification @@ -484,9 +484,9 @@ module ts { /** * Expands a wildcard portion of a file specification. - * @param prefix The prefix path. + * @param basePath The prefix path. * @param fileSpec The file specification. - * @param offset The offset in the file specification. + * @param start The starting offset in the file specification. */ function expandWildcard(basePath: string, fileSpec: string, start: number): ExpandResult { // find the offset of the next directory separator to extract the wildcard path segment. @@ -515,7 +515,7 @@ module ts { let path = combinePaths(basePath, entry); if (isEndOfLine) { - // This wildcard has no further directory so process the match. + // This wildcard has no further directory to process, so include the file. includeFile(path); } else if (host.directoryExists(path)) { @@ -531,9 +531,9 @@ module ts { /** * Expands a `**` recursive directory wildcard. - * @param prefix The directory to recursively expand - * @param fileSpec The original file specification - * @param offset The offset into the file specification + * @param basePath The directory to recursively expand. + * @param fileSpec The original file specification. + * @param start The starting offset in the file specification. */ function expandRecursiveDirectory(basePath: string, fileSpec: string, start: number): ExpandResult { if (isExpandingRecursiveDirectory) { @@ -547,7 +547,7 @@ module ts { isExpandingRecursiveDirectory = false; if (result !== ExpandResult.Error) { - // recursively expand each subdirectory. + // Recursively expand each subdirectory. let entries = host.readDirectoryFlat(basePath); for (let entry of entries) { let path = combinePaths(basePath, entry); @@ -595,7 +595,9 @@ module ts { /** * Creates a regular expression from a glob-style wildcard. - * @param wildcard The wildcard + * @param fileSpec The file specification. + * @param start The starting offset in the file specification. + * @param end The end offset in the file specification. */ function createRegularExpressionFromWildcard(fileSpec: string, start: number, end: number): RegExp { let pattern = createPatternFromWildcard(fileSpec, start, end); @@ -604,6 +606,9 @@ module ts { /** * 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 = ""; @@ -611,7 +616,7 @@ module ts { while (offset >= 0 && offset < end) { if (offset > start) { // Escape and append the non-wildcard portion to the regular expression - pattern += escapePatternForRegularExpression(fileSpec, start, offset); + pattern += escapeRegularExpressionText(fileSpec, start, offset); } let charCode = fileSpec.charCodeAt(offset); @@ -644,7 +649,7 @@ module ts { } // Escape and append the character range - pattern += escapePatternForRegularExpression(fileSpec, start, offset); + pattern += escapeRegularExpressionText(fileSpec, start, offset); pattern += "]"; } @@ -654,7 +659,7 @@ module ts { // Escape and append any remaining non-wildcard portion. if (start < end) { - pattern += escapePatternForRegularExpression(fileSpec, start, end); + pattern += escapeRegularExpressionText(fileSpec, start, end); } return pattern; @@ -662,8 +667,8 @@ module ts { /** * Creates a regular expression from a glob-style wildcard used to exclude a file. - * @param prefix The prefix path - * @param exclude The file specifications to exclude. + * @param basePath The prefix path + * @param excludeSpecs The file specifications to exclude. */ function createExcludeRegularExpression(basePath: string, excludeSpecs: string[]): RegExp { // ignore an empty exclusion list @@ -671,7 +676,7 @@ module ts { return undefined; } - basePath = escapePatternForRegularExpression(basePath, 0, basePath.length); + basePath = escapeRegularExpressionText(basePath, 0, basePath.length); let pattern = ""; for (let excludeSpec of excludeSpecs) { @@ -686,7 +691,7 @@ module ts { if (pattern) { pattern += "|"; } - + pattern += "(" + combinePaths(basePath, excludePattern) + ")"; } } @@ -700,18 +705,18 @@ module ts { /** * Creates a pattern for the excludePattern regular expression used to exclude a file. - * @param exclude The file specification to exclude. + * @param excludeSpec The file specification to exclude. */ - function createExcludePattern(fileSpec: string): string { + function createExcludePattern(excludeSpec: string): string { let hasRecursiveDirectoryWildcard = false; let pattern = ""; let start = 0; - let end = fileSpec.length; - let offset = fileSpec.indexOf(directorySeparator, start); + let end = excludeSpec.length; + let offset = excludeSpec.indexOf(directorySeparator, start); while (offset >= 0 && offset < end) { - if (isRecursiveDirectoryWildcard(fileSpec, start)) { + if (isRecursiveDirectoryWildcard(excludeSpec, start)) { if (hasRecursiveDirectoryWildcard) { - errors.push(createCompilerDiagnostic(Diagnostics.File_specification_cannot_contain_multiple_recursive_directory_wildcards_Asterisk_Asterisk_Colon_0, fileSpec)); + errors.push(createCompilerDiagnostic(Diagnostics.File_specification_cannot_contain_multiple_recursive_directory_wildcards_Asterisk_Asterisk_Colon_0, excludeSpec)); return undefined; } @@ -719,16 +724,16 @@ module ts { } if (offset > start) { - pattern += createPatternFromWildcard(fileSpec, start, offset); + pattern += createPatternFromWildcard(excludeSpec, start, offset); } - pattern += fileSpec.charAt(offset); + pattern += excludeSpec.charAt(offset); start = offset + 1; - offset = fileSpec.indexOf(directorySeparator, start); + offset = excludeSpec.indexOf(directorySeparator, start); } if (start < end) { - pattern += createPatternFromWildcard(fileSpec, start, end); + pattern += createPatternFromWildcard(excludeSpec, start, end); } return pattern; @@ -738,7 +743,7 @@ module ts { * Escape regular expression reserved tokens. * @param text The text to escape. */ - function escapePatternForRegularExpression(text: string, start: number, end: number) { + function escapeRegularExpressionText(text: string, start: number, end: number) { let offset = indexOfReservedCharacter(text, start); if (offset < 0) { return text; @@ -810,7 +815,7 @@ module ts { /** * Gets the index of the next reserved character in a regular expression. * @param text The text to search. - * @param startOffset The offset at which to start the search. + * @param start The offset at which to start the search. */ function indexOfReservedCharacter(text: string, start: number) { const len = text.length; From 44ee64f4fdc5c365910c6eff5967bc05fe087d85 Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Tue, 19 May 2015 17:50:24 -0700 Subject: [PATCH 06/11] Minor edit for readability --- src/compiler/commandLineParser.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 4d454ed246a31..9ff965a0b8faf 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -506,7 +506,8 @@ module ts { // match the entries in the directory against the wildcard pattern. let entries = host.readDirectoryFlat(basePath); - let pattern = createRegularExpressionFromWildcard(fileSpec, start, isEndOfLine ? fileSpec.length : offset); + let end = isEndOfLine ? fileSpec.length : offset; + let pattern = createRegularExpressionFromWildcard(fileSpec, start, end); for (let entry of entries) { // skip the entry if it does not match the pattern. if (!pattern.test(entry)) { From a41f5f8545a235e6af094258e240d8166e8a5147 Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Tue, 19 May 2015 17:51:17 -0700 Subject: [PATCH 07/11] Removed redundant comment --- src/compiler/commandLineParser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 9ff965a0b8faf..e94e4771f471a 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -804,7 +804,7 @@ module ts { for (let i = start; i < len; ++i) { let charCode = fileSpec.charCodeAt(i); if (charCode === CharacterCodes.asterisk || - charCode === CharacterCodes.question /*question*/ || + charCode === CharacterCodes.question || charCode === CharacterCodes.openBracket) { return i; } From 6d0e21283d185f31d5f87340180921d7bc87c7a0 Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Tue, 19 May 2015 17:52:34 -0700 Subject: [PATCH 08/11] Fixed issue with sys.fileExists on node giving false positive for directories --- src/compiler/sys.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 5afec57d4da64..87948cf6aa869 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -288,7 +288,7 @@ module 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(); From a4c5684b5f7ab779c5e64e66916d07cc019b84bf Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Tue, 19 May 2015 17:54:03 -0700 Subject: [PATCH 09/11] Minor edit for readability --- src/compiler/commandLineParser.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index e94e4771f471a..d63f5ff605100 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -791,7 +791,8 @@ module ts { * @param offset The offset into the file specification. */ function isDirectorySeparatorOrEndOfLine(fileSpec: string, offset: number) { - return offset === fileSpec.length || (offset < fileSpec.length && fileSpec.charCodeAt(offset) === CharacterCodes.slash); + return offset === fileSpec.length || + (offset < fileSpec.length && fileSpec.charCodeAt(offset) === CharacterCodes.slash); } /** From 93c66c89b0f51b3b8b47742a04fdf274498fc6c5 Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Tue, 19 May 2015 18:21:00 -0700 Subject: [PATCH 10/11] Updated createExcludeRegularExpression to match directories --- src/compiler/commandLineParser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index d63f5ff605100..29112e97a3316 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -693,7 +693,7 @@ module ts { pattern += "|"; } - pattern += "(" + combinePaths(basePath, excludePattern) + ")"; + pattern += "(" + combinePaths(basePath, excludePattern) + "($|/.*))"; } } From f93687903bdf41462f4f9a3db0300478adc19447 Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Wed, 20 May 2015 17:03:02 -0700 Subject: [PATCH 11/11] Moved glob patterns from 'files' to 'include', some cleanup, added tests. --- Jakefile.js | 7 +- src/compiler/commandLineParser.ts | 739 +++++++++++++-------------- src/compiler/core.ts | 6 +- src/harness/external/chai.d.ts | 3 + src/harness/runner.ts | 21 +- tests/cases/unittests/expandFiles.ts | 236 +++++++++ 6 files changed, 631 insertions(+), 381 deletions(-) create mode 100644 tests/cases/unittests/expandFiles.ts diff --git a/Jakefile.js b/Jakefile.js index cb53b479502e6..575f293a6235b 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -128,7 +128,8 @@ var harnessSources = [ "services/preProcessFile.ts", "services/patternMatcher.ts", "versionCache.ts", - "convertToBase64.ts" + "convertToBase64.ts", + "expandFiles.ts" ].map(function (f) { return path.join(unittestsDirectory, f); })).concat([ @@ -505,7 +506,9 @@ function cleanTestDirs() { // used to pass data from jake command line directly to run.js function writeTestConfigFile(tests, testConfigFile) { console.log('Running test(s): ' + tests); - var testConfigContents = '{\n' + '\ttest: [\'' + tests + '\']\n}'; + var testConfigContents = JSON.stringify({ + "test": [tests] + }); fs.writeFileSync('test.config', testConfigContents); } diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 29112e97a3316..a168f363e17f2 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -396,453 +396,448 @@ module ts { } function getFileNames(): string[] { - var exclude = json["exclude"] instanceof Array ? json["exclude"] : undefined; + let fileNames: string[]; if (hasProperty(json, "files")) { - var fileNames = json["files"] instanceof Array ? json["files"] : undefined; - return expandFiles(fileNames, exclude, /*literalFiles*/ false); + fileNames = json["files"] instanceof Array ? json["files"] : []; } - else { - var fileNames: string[] = []; - var sysFiles = host.readDirectory(basePath, ".ts"); - for (var i = 0; i < sysFiles.length; i++) { - var name = sysFiles[i]; - if (!fileExtensionIs(name, ".d.ts") || !contains(sysFiles, name.substr(0, name.length - 5) + ".ts")) { - fileNames.push(name); - } - } - - if (!exclude) { - return fileNames; - } - - return expandFiles(fileNames, exclude, /*literalFiles*/ true); + + let includeSpecs = json["include"] instanceof Array ? json["include"] : undefined; + if (!fileNames && !includeSpecs) { + includeSpecs = ["**/*.ts"]; + } + + let excludeSpecs = json["exclude"] instanceof Array ? json["exclude"] : undefined; + return expandFiles(basePath, fileNames, includeSpecs, excludeSpecs, options, host, errors); + } + } + + /** + * Expands an array of file specifications. + * @param basePath The base path for any relative 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 options Compiler options. + * @param host The host used to resolve files and directories. + * @param errors An array for diagnostic reporting. + */ + export function expandFiles(basePath: string, fileNames: string[], includeSpecs: string[], excludeSpecs: string[], options: CompilerOptions, host: ParseConfigHost, errors?: Diagnostic[]): string[] { + const wildcardCharacterPattern = /[\*\?\[]/g; + const reservedCharacterPattern = /[^\w\-]/g; + + let output: string[] = []; + basePath = normalizePath(basePath); + basePath = removeTrailingDirectorySeparator(basePath); + + let ignoreCase = !host.useCaseSensitiveFileNames; + let isExpandingRecursiveDirectory = false; + let fileSet: Map = {}; + let excludePattern: RegExp; + + // include every literal file + if (fileNames) { + for (let fileName of fileNames) { + let path = getNormalizedAbsolutePath(fileName, basePath); + includeFile(path, /*isLiteralFile*/ true); + } + } + + // expand and include the provided files into the file set. + if (includeSpecs) { + // populate the file exclusion pattern + excludePattern = createExcludeRegularExpression(basePath, excludeSpecs); + + for (let includeSpec of includeSpecs) { + includeSpec = normalizePath(includeSpec); + includeSpec = removeTrailingDirectorySeparator(includeSpec); + expandFileSpec(basePath, includeSpec, 0, includeSpec.length); } } + return output; + /** - * Expands an array of file specifications. - * @param fileSpecs The file specifications to expand. - * @param excludeSpecs Any file specifications to exclude. - * @param literalFiles A value indicating whether the fileSpecs array contains only literal file names and not wildcards. + * Expands a directory with wildcards. + * @param basePath The directory to expand. + * @param fileSpec The original file specification. + * @param start The starting offset in the file specification. + * @param end The end offset in the file specification. */ - function expandFiles(fileSpecs: string[], excludeSpecs: string[], literalFiles: boolean): string[] { - let output: string[] = []; - if (!fileSpecs) { - return output; + function expandFileSpec(basePath: string, fileSpec: string, start: number, end: number): ExpandResult { + // Skip expansion if the base path matches an exclude pattern. + if (isExcludedPath(basePath)) { + return ExpandResult.Ok; } - - let normalizedBasePath = normalizePath(basePath); - let ignoreCase = !host.useCaseSensitiveFileNames; - let isExpandingRecursiveDirectory = false; - let fileSet: StringSet = {}; - // populate the file exclusion pattern - let excludePattern = createExcludeRegularExpression(normalizedBasePath, excludeSpecs); + // 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. + let path = combinePaths(basePath, fileSpec.substring(start)); + includeFile(path, /*isLiteralFile*/ false); + return ExpandResult.Ok; + } - // expand and include the provided files into the file set. - for (let fileSpec of fileSpecs) { - let normalizedFileSpec = normalizePath(fileSpec); - if (literalFiles) { - normalizedFileSpec = combinePaths(basePath, normalizedFileSpec); - includeFile(normalizedFileSpec); - } - else { - normalizedFileSpec = removeTrailingDirectorySeparator(normalizedFileSpec); - expandDirectory(normalizedBasePath, normalizedFileSpec, 0); + // 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(basePath)) { + return ExpandResult.Ok; } + + start = offset + 1; } - return output; + // Find the offset of the next directory separator to extract the wildcard path segment. + offset = getEndOfPathSegment(fileSpec, start, end); - /** - * Expands a directory with wildcards. - * @param basePath The directory to expand. - * @param fileSpec The original file specification. - * @param start The starting offset in the file specification. - */ - function expandDirectory(basePath: string, fileSpec: string, start: number): ExpandResult { - // 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 process the match if the file exists and was not excluded. - let path = combinePaths(basePath, fileSpec.substring(start)); - includeFile(path); - return ExpandResult.Ok; - } - - // find the last directory separator before the wildcard to get the leading path. - let end = fileSpec.lastIndexOf(directorySeparator, offset); - if (end > start) { - // wildcard occurs in a later segment, include remaining path up to wildcard in prefix - basePath = combinePaths(basePath, fileSpec.substring(start, end)); - start = end + 1; - } - - // Expand the wildcard portion of the path. - return expandWildcard(basePath, fileSpec, start); - } - - /** - * Expands a wildcard portion of a file specification. - * @param basePath The prefix path. - * @param fileSpec The file specification. - * @param start The starting offset in the file specification. - */ - function expandWildcard(basePath: string, fileSpec: string, start: number): ExpandResult { - // find the offset of the next directory separator to extract the wildcard path segment. - let offset = fileSpec.indexOf(directorySeparator, start); - let isEndOfLine = offset < 0; - if (isRecursiveDirectoryWildcard(fileSpec, start)) { - // If the path contains only "**", then this is a recursive directory pattern. - if (isEndOfLine) { - // 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; - } + // Check if the current offset is the beginning of a recursive directory pattern. + if (isRecursiveDirectoryWildcard(fileSpec, start, offset)) { + if (offset >= end) { + // 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; + } - // expand the recursive directory pattern - return expandRecursiveDirectory(basePath, fileSpec, offset + 1); + // Expand the recursive directory pattern. + return expandRecursiveDirectory(basePath, fileSpec, offset + 1, end); + } + + // Match the entries in the directory against the wildcard pattern. + let pattern = createRegularExpressionFromWildcard(fileSpec, start, offset); + let entries = host.readDirectoryFlat(basePath); + for (let entry of entries) { + // Skip the entry if it does not match the pattern. + if (!pattern.test(entry)) { + continue; } - // match the entries in the directory against the wildcard pattern. - let entries = host.readDirectoryFlat(basePath); - let end = isEndOfLine ? fileSpec.length : offset; - let pattern = createRegularExpressionFromWildcard(fileSpec, start, end); - for (let entry of entries) { - // skip the entry if it does not match the pattern. - if (!pattern.test(entry)) { + let path = combinePaths(basePath, entry); + if (offset >= end) { + // If the entry is a declaration file and there is a source file with the + // same name in this directory, skip the file. + if (fileExtensionIs(entry, ".d.ts") && + contains(entries, entry.substr(0, entry.length - 5) + ".ts")) { continue; } - let path = combinePaths(basePath, entry); - if (isEndOfLine) { - // This wildcard has no further directory to process, so include the file. - includeFile(path); - } - else if (host.directoryExists(path)) { - // If this was a directory, process the directory. - if (expandDirectory(path, fileSpec, offset + 1) === ExpandResult.Error) { - return ExpandResult.Error; - } + // This wildcard has no further directory to process, so include the file. + includeFile(path, /*isLiteralFile*/ false); + } + else if (host.directoryExists(path)) { + // If this was a directory, process the directory. + if (expandFileSpec(path, fileSpec, offset + 1, end) === 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. - */ - function expandRecursiveDirectory(basePath: string, fileSpec: string, start: number): ExpandResult { - if (isExpandingRecursiveDirectory) { + 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 end The end offset in the file specification. + */ + function expandRecursiveDirectory(basePath: string, fileSpec: string, start: number, end: number): ExpandResult { + 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 non-recursive part of the file specification against the prefix path. - isExpandingRecursiveDirectory = true; - let result = expandDirectory(basePath, fileSpec, start); - isExpandingRecursiveDirectory = false; - - if (result !== ExpandResult.Error) { - // Recursively expand each subdirectory. - let entries = host.readDirectoryFlat(basePath); - for (let entry of entries) { - let path = combinePaths(basePath, entry); - if (host.directoryExists(path)) { - if (expandRecursiveDirectory(path, fileSpec, start) === ExpandResult.Error) { - result = ExpandResult.Error; - break; - } + return ExpandResult.Error; + } + + // expand the non-recursive part of the file specification against the prefix path. + isExpandingRecursiveDirectory = true; + let result = expandFileSpec(basePath, fileSpec, start, end); + isExpandingRecursiveDirectory = false; + + if (result !== ExpandResult.Error) { + // Recursively expand each subdirectory. + let entries = host.readDirectoryFlat(basePath); + for (let entry of entries) { + let path = combinePaths(basePath, entry); + if (host.directoryExists(path)) { + if (expandRecursiveDirectory(path, fileSpec, start, end) === ExpandResult.Error) { + result = ExpandResult.Error; + break; } } } - - return result; } - /** - * Includes a file in a file set. - * @param file The file to include. - */ - function includeFile(file: string): void { - let key = ignoreCase ? file.toLowerCase() : file; - // ignore the file if we've already matched it. - if (hasProperty(fileSet, key)) { - return; - } - - // ignore the file if it matches an exclude pattern - if (excludePattern && excludePattern.test(file)) { + return result; + } + + /** + * Includes a file in a file set. + * @param file The file to include. + * @param isLiteralFile A value indicating whether the file to be added is + * a literal file, or the result of matching a file specification. + */ + function includeFile(file: string, isLiteralFile: boolean): void { + if (!isLiteralFile) { + // Ignore the file if it should be excluded + if (isExcludedPath(file)) { return; } - // ignore the file if it doesn't exist or is a directory - if (!host.fileExists(file) || host.directoryExists(file)) { + // Ignore the file if it doesn't exist. + if (!host.fileExists(file)) { return; } - // ignore the file if it does not have a supported extension + // Ignore the file if it does not have a supported extension. if (!options.allowNonTsExtensions && !hasSupportedFileExtension(file)) { return; } - - fileSet[key] = true; - output.push(file); } - /** - * 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. - */ - function createRegularExpressionFromWildcard(fileSpec: string, start: number, end: number): RegExp { - let pattern = createPatternFromWildcard(fileSpec, start, end); - return new RegExp("^" + pattern + "$", ignoreCase ? "i" : ""); + // Ignore the file if we've already included it. + let key = ignoreCase ? file.toLowerCase() : file; + if (hasProperty(fileSet, key)) { + return; } + + fileSet[key] = file; + output.push(file); + } + + /** + * Determines whether a path should be excluded. + */ + function isExcludedPath(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. + */ + function createRegularExpressionFromWildcard(fileSpec: string, start: number, end: number): RegExp { + let pattern = createPatternFromWildcard(fileSpec, start, end); + return new RegExp("^" + pattern + "$", ignoreCase ? "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); - } + /** + * 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); + } + + let 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 (one character) pattern to the regular expression + pattern += "[^/]"; + } + else if (charCode === CharacterCodes.openBracket) { + // Append a character range (one character) pattern to the regular expression + pattern += "(?!/)["; - let 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 (one character) pattern to the regular expression - pattern += "."; + // If the next character is an exclamation token, append a caret (^) to negate the + // character range and advance the start of the range by one. + start = offset + 1; + charCode = fileSpec.charCodeAt(start); + if (charCode === CharacterCodes.exclamation) { + pattern += "^"; + start++; } - else if (charCode === CharacterCodes.openBracket) { - // Append a character range (one character) pattern to the regular expression - pattern += "["; - - // If the next character is an exclamation token, append a caret (^) to negate the - // character range and advance the start of the range by one. - start = offset + 1; - charCode = fileSpec.charCodeAt(start); - if (charCode === CharacterCodes.exclamation) { - pattern += "^"; - start++; - } - - // Find the end of the character range. If it can't be found, fix up the range - // to the end of the wildcard - offset = fileSpec.indexOf(']', start); - if (offset < 0 || offset > end) { - offset = end; - } - - // Escape and append the character range - pattern += escapeRegularExpressionText(fileSpec, start, offset); - pattern += "]"; + + // Find the end of the character range. If it can't be found, fix up the range + // to the end of the wildcard + offset = fileSpec.indexOf(']', start); + if (offset < 0 || offset > end) { + offset = end; } - start = offset + 1; - offset = indexOfWildcard(fileSpec, start); + // Escape and append the character range + pattern += escapeRegularExpressionText(fileSpec, start, offset); + pattern += "]"; } - // Escape and append any remaining non-wildcard portion. - if (start < end) { - pattern += escapeRegularExpressionText(fileSpec, start, end); - } - - return pattern; + start = offset + 1; + offset = indexOfWildcard(fileSpec, start); } - /** - * Creates a regular expression from a glob-style wildcard used to exclude a file. - * @param basePath The prefix path - * @param excludeSpecs The file specifications to exclude. - */ - function createExcludeRegularExpression(basePath: string, excludeSpecs: string[]): RegExp { - // ignore an empty exclusion list - if (!excludeSpecs || excludeSpecs.length === 0) { - return undefined; - } - - basePath = escapeRegularExpressionText(basePath, 0, basePath.length); - - let pattern = ""; - for (let excludeSpec of excludeSpecs) { - // skip empty entries. - if (!excludeSpec) { - continue; + // 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 basePath The prefix path + * @param excludeSpecs The file specifications to exclude. + */ + function createExcludeRegularExpression(basePath: string, excludeSpecs: string[]): RegExp { + // Ignore an empty exclusion list + if (!excludeSpecs || excludeSpecs.length === 0) { + return undefined; + } + + basePath = escapeRegularExpressionText(basePath, 0, basePath.length); + + let pattern = ""; + for (let excludeSpec of excludeSpecs) { + let excludePattern = createExcludePattern(basePath, excludeSpec); + if (excludePattern) { + if (pattern) { + pattern += "|"; } - let normalizedExcludeSpec = normalizePath(excludeSpec); - let excludePattern = createExcludePattern(normalizedExcludeSpec); - if (excludePattern) { - if (pattern) { - pattern += "|"; - } - - pattern += "(" + combinePaths(basePath, excludePattern) + "($|/.*))"; - } + pattern += "(" + excludePattern + ")"; } - - if (pattern) { - return new RegExp("^(" + pattern + ")$", ignoreCase ? "i" : ""); - } - + } + + if (pattern) { + return new RegExp("^(" + pattern + ")($|/)", ignoreCase ? "i" : ""); + } + + return undefined; + } + + /** + * Creates a pattern for used to exclude a file. + * @param excludeSpec The file specification to exclude. + */ + function createExcludePattern(basePath: string, excludeSpec: string): string { + if (!excludeSpec) { return undefined; } + + excludeSpec = normalizePath(excludeSpec); + excludeSpec = removeTrailingDirectorySeparator(excludeSpec); - /** - * Creates a pattern for the excludePattern regular expression used to exclude a file. - * @param excludeSpec The file specification to exclude. - */ - function createExcludePattern(excludeSpec: string): string { - let hasRecursiveDirectoryWildcard = false; - let pattern = ""; - let start = 0; - let end = excludeSpec.length; - let offset = excludeSpec.indexOf(directorySeparator, start); - while (offset >= 0 && offset < end) { - if (isRecursiveDirectoryWildcard(excludeSpec, start)) { - if (hasRecursiveDirectoryWildcard) { + let pattern = isRootedDiskPath(excludeSpec) ? "" : basePath; + let hasRecursiveDirectoryWildcard = false; + let start = 0; + let end = excludeSpec.length; + let offset = getEndOfPathSegment(excludeSpec, start, end); + while (start < offset) { + if (isRecursiveDirectoryWildcard(excludeSpec, start, offset)) { + if (hasRecursiveDirectoryWildcard) { + if (errors) { errors.push(createCompilerDiagnostic(Diagnostics.File_specification_cannot_contain_multiple_recursive_directory_wildcards_Asterisk_Asterisk_Colon_0, excludeSpec)); - return undefined; } - - hasRecursiveDirectoryWildcard = true; + return undefined; } - if (offset > start) { - pattern += createPatternFromWildcard(excludeSpec, start, offset); - } + // 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, offset, end)) { + break; + } - pattern += excludeSpec.charAt(offset); - start = offset + 1; - offset = excludeSpec.indexOf(directorySeparator, start); - } - - if (start < end) { - pattern += createPatternFromWildcard(excludeSpec, start, end); + hasRecursiveDirectoryWildcard = true; + pattern += "(/.+)?";; } - - return pattern; - } - - /** - * Escape regular expression reserved tokens. - * @param text The text to escape. - */ - function escapeRegularExpressionText(text: string, start: number, end: number) { - let offset = indexOfReservedCharacter(text, start); - if (offset < 0) { - return text; - } - - let escaped: string = ""; - while (offset >= 0 && offset < end) { - if (offset > start) { - escaped += text.substring(start, offset); + else { + if (pattern) { + pattern += directorySeparator; } - - escaped += "\\"; - escaped += text.charAt(offset); - start = offset + 1; - offset = indexOfReservedCharacter(text, start); - } - - if (start < end) { - escaped += text.substring(start, end); + + pattern += createPatternFromWildcard(excludeSpec, start, offset); } - return escaped; + start = offset + 1; + offset = getEndOfPathSegment(excludeSpec, start, end); } - /** - * Determines whether the wildcard at the current offset is a recursive directory wildcard. - * @param fileSpec The file specification. - * @param offset The offset into the file specification. - */ - function isRecursiveDirectoryWildcard(fileSpec: string, offset: number) { - if (offset + 2 <= fileSpec.length && - fileSpec.charCodeAt(offset) === CharacterCodes.asterisk && - fileSpec.charCodeAt(offset + 1) === CharacterCodes.asterisk && - isDirectorySeparatorOrEndOfLine(fileSpec, offset + 2)) { - return true; - } - - return false; - } - - /** - * Determines whether the provided offset points to a directory separator or the end of the line. - * @param fileSpec The file specification. - * @param offset The offset into the file specification. - */ - function isDirectorySeparatorOrEndOfLine(fileSpec: string, offset: number) { - return offset === fileSpec.length || - (offset < fileSpec.length && fileSpec.charCodeAt(offset) === CharacterCodes.slash); + return pattern; + } + + function canElideRecursiveDirectorySegment(excludeSpec: string, offset: number, end: number) { + if (offset === end || offset + 1 === end) { + return true; } - /** - * Gets the index of the next wildcard character in a file specification. - * @param fileSpec The file specification. - * @param start The offset at which to start the search. - */ - function indexOfWildcard(fileSpec: string, start: number): number { - const len = fileSpec.length; - for (let i = start; i < len; ++i) { - let charCode = fileSpec.charCodeAt(i); - if (charCode === CharacterCodes.asterisk || - charCode === CharacterCodes.question || - charCode === CharacterCodes.openBracket) { - return i; - } + return canElideWildcardSegment(excludeSpec, offset + 1, end); + } + + function canElideWildcardSegment(excludeSpec: string, start: number, end: number) { + let charCode = excludeSpec.charCodeAt(start); + if (charCode === CharacterCodes.asterisk) { + if (start + 1 === end) { + return true; } - return -1; - } - - /** - * Gets the index of the next reserved character in a regular expression. - * @param text The text to search. - * @param start The offset at which to start the search. - */ - function indexOfReservedCharacter(text: string, start: number) { - const len = text.length; - for (let i = start; i < len; ++i) { - let charCode = text.charCodeAt(i); - switch (charCode) { - case CharacterCodes.question: - case CharacterCodes.asterisk: - case CharacterCodes.plus: - case CharacterCodes.dot: - case CharacterCodes.caret: - case CharacterCodes.$: - case CharacterCodes.openParen: - case CharacterCodes.closeParen: - case CharacterCodes.openBrace: - case CharacterCodes.closeBrace: - case CharacterCodes.openBracket: - case CharacterCodes.closeBracket: - case CharacterCodes.bar: - return i; - } + if (start + 4 === end && + !options.allowNonTsExtensions && + fileExtensionIs(excludeSpec, ".ts")) { + return true; } - - return -1; } + return false; + } + + /** + * Escape regular expression reserved tokens. + * @param fileSpec The file specification. + * @param start The starting offset in the file specification. + * @param end The ending offset in the file specification. + */ + 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 start The starting offset in the file specification. + * @param end The ending offset in the file specification. + */ + function isRecursiveDirectoryWildcard(fileSpec: string, start: number, end: number) { + return end - start === 2 && + fileSpec.charCodeAt(start) === CharacterCodes.asterisk && + fileSpec.charCodeAt(start + 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 { + wildcardCharacterPattern.lastIndex = start; + wildcardCharacterPattern.test(fileSpec); + return wildcardCharacterPattern.lastIndex - 1; + } + + function getEndOfPathSegment(fileSpec: string, start: number, end: number): number { + if (start >= end) { + return end; + } + + let offset = fileSpec.indexOf(directorySeparator, start); + return offset < 0 || offset > end ? end : offset; } } } diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 416e1d0fddd7f..ae3032b528a0a 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -681,10 +681,12 @@ module ts { return path; } - export function compareNormalizedPaths(a: string, b: string, currentDirectory: string, ignoreCase?: boolean) { + 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); let aComponents = getNormalizedPathComponents(a, currentDirectory); let bComponents = getNormalizedPathComponents(b, currentDirectory); let sharedLength = Math.min(aComponents.length, bComponents.length); @@ -692,7 +694,7 @@ module ts { let result = compareStrings(aComponents[i], bComponents[i], ignoreCase); if (result) return result; } - + return compareValues(aComponents.length, bComponents.length); } diff --git a/src/harness/external/chai.d.ts b/src/harness/external/chai.d.ts index 814de75e7b254..2fa5b20de476c 100644 --- a/src/harness/external/chai.d.ts +++ b/src/harness/external/chai.d.ts @@ -171,5 +171,8 @@ declare module chai { function isFalse(value: any, message?: string): void; function isNull(value: any, message?: string): void; function isNotNull(value: 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; } } \ No newline at end of file diff --git a/src/harness/runner.ts b/src/harness/runner.ts index 37451639d75fc..a5308e8546e21 100644 --- a/src/harness/runner.ts +++ b/src/harness/runner.ts @@ -37,11 +37,19 @@ var testConfigFile = Harness.IO.fileExists(mytestconfig) ? Harness.IO.readFile(mytestconfig) : (Harness.IO.fileExists(testconfig) ? Harness.IO.readFile(testconfig) : ''); +var unitTestsRequested = false; + if (testConfigFile !== '') { - // TODO: not sure why this is crashing mocha - //var testConfig = JSON.parse(testConfigRaw); - var testConfig = testConfigFile.match(/test:\s\['(.*)'\]/); - var options = testConfig ? [testConfig[1]] : []; + var options: string[]; + try { + let testConfigJson = JSON.parse(testConfigFile); + options = testConfigJson.test instanceof Array ? testConfigJson.test : []; + } + catch (e) { + let testConfig = testConfigFile.match(/test:\s\['(.*)'\]/); + options = testConfig ? [testConfig[1]] : []; + } + for (var i = 0; i < options.length; i++) { switch (options[i]) { case 'compiler': @@ -73,11 +81,14 @@ if (testConfigFile !== '') { case 'test262': runners.push(new Test262BaselineRunner()); break; + case 'unit': + unitTestsRequested = true; + break; } } } -if (runners.length === 0) { +if (runners.length === 0 && !unitTestsRequested) { // compiler runners.push(new CompilerBaselineRunner(CompilerTestType.Conformance)); runners.push(new CompilerBaselineRunner(CompilerTestType.Regressions)); diff --git a/tests/cases/unittests/expandFiles.ts b/tests/cases/unittests/expandFiles.ts new file mode 100644 index 0000000000000..22b85687e9c48 --- /dev/null +++ b/tests/cases/unittests/expandFiles.ts @@ -0,0 +1,236 @@ +/// +/// + +describe("expandFiles", () => { + 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); + + let expect = _chai.expect; + describe("with literal file list", () => { + it("without exclusions", () => { + let fileNames = ["a.ts", "b.ts"]; + let results = ts.expandFiles(basePath, fileNames, /*includeSpecs*/ undefined, /*excludeSpecs*/ undefined, {}, caseInsensitiveHost); + assert.deepEqual(results, ["c:/dev/a.ts", "c:/dev/b.ts"]); + }); + it("missing files are still present", () => { + let fileNames = ["z.ts", "x.ts"]; + let results = ts.expandFiles(basePath, fileNames, /*includeSpecs*/ undefined, /*excludeSpecs*/ undefined, {}, caseInsensitiveHost); + assert.deepEqual(results, ["c:/dev/z.ts", "c:/dev/x.ts"]); + }); + it("are not removed due to excludes", () => { + let fileNames = ["a.ts", "b.ts"]; + let excludeSpecs = ["b.ts"]; + let results = ts.expandFiles(basePath, fileNames, /*includeSpecs*/ undefined, excludeSpecs, {}, caseInsensitiveHost); + assert.deepEqual(results, ["c:/dev/a.ts", "c:/dev/b.ts"]); + }); + }); + + describe("with literal include list", () => { + it("without exclusions", () => { + let includeSpecs = ["a.ts", "b.ts"]; + let results = ts.expandFiles(basePath, /*fileNames*/ undefined, includeSpecs, /*excludeSpecs*/ undefined, {}, caseInsensitiveHost); + assert.deepEqual(results, ["c:/dev/a.ts", "c:/dev/b.ts"]); + }); + it("with non .ts file extensions are excluded", () => { + let includeSpecs = ["a.js", "b.js"]; + let results = ts.expandFiles(basePath, /*fileNames*/ undefined, includeSpecs, /*excludeSpecs*/ undefined, {}, caseInsensitiveHost); + assert.deepEqual(results, []); + }); + it("with missing files are excluded", () => { + let includeSpecs = ["z.ts", "x.ts"]; + let results = ts.expandFiles(basePath, /*fileNames*/ undefined, includeSpecs, /*excludeSpecs*/ undefined, {}, caseInsensitiveHost); + assert.deepEqual(results, []); + }); + it("with literal excludes", () => { + let includeSpecs = ["a.ts", "b.ts"]; + let excludeSpecs = ["b.ts"]; + let results = ts.expandFiles(basePath, /*fileNames*/ undefined, includeSpecs, excludeSpecs, {}, caseInsensitiveHost); + assert.deepEqual(results, ["c:/dev/a.ts"]); + }); + it("with wildcard excludes", () => { + let includeSpecs = ["a.ts", "b.ts", "z/a.ts", "z/abz.ts", "z/aba.ts", "x/b.ts"]; + let excludeSpecs = ["*.ts", "z/??[b-z].ts", "*/b.ts"]; + let results = ts.expandFiles(basePath, /*fileNames*/ undefined, includeSpecs, excludeSpecs, {}, caseInsensitiveHost); + assert.deepEqual(results, ["c:/dev/z/a.ts", "c:/dev/z/aba.ts"]); + }); + it("with recursive excludes", () => { + let includeSpecs = ["a.ts", "b.ts", "x/a.ts", "x/b.ts", "x/y/a.ts", "x/y/b.ts"]; + let excludeSpecs = ["**/b.ts"]; + let results = ts.expandFiles(basePath, /*fileNames*/ undefined, includeSpecs, excludeSpecs, {}, caseInsensitiveHost); + assert.deepEqual(results, ["c:/dev/a.ts", "c:/dev/x/a.ts", "c:/dev/x/y/a.ts"]); + }); + it("with case sensitive exclude", () => { + let includeSpecs = ["B.ts"]; + let excludeSpecs = ["**/b.ts"]; + let results = ts.expandFiles(basePath, /*fileNames*/ undefined, includeSpecs, excludeSpecs, {}, caseSensitiveHost); + assert.deepEqual(results, ["c:/dev/B.ts"]); + }); + }); + + describe("with wildcard include list", () => { + it("same named declarations are excluded", () => { + let includeSpecs = ["*.ts"]; + let results = ts.expandFiles(basePath, /*fileNames*/ undefined, includeSpecs, /*excludeSpecs*/ undefined, {}, caseInsensitiveHost); + assert.deepEqual(results, ["c:/dev/a.ts", "c:/dev/b.ts", "c:/dev/c.d.ts"]); + }); + it("`*` matches only ts files", () => { + let includeSpecs = ["*"]; + let results = ts.expandFiles(basePath, /*fileNames*/ undefined, includeSpecs, /*excludeSpecs*/ undefined, {}, caseInsensitiveHost); + assert.deepEqual(results, ["c:/dev/a.ts", "c:/dev/b.ts", "c:/dev/c.d.ts"]); + }); + it("`?` matches only a single character", () => { + let includeSpecs = ["x/?.ts"]; + let results = ts.expandFiles(basePath, /*fileNames*/ undefined, includeSpecs, /*excludeSpecs*/ undefined, {}, caseInsensitiveHost); + assert.deepEqual(results, ["c:/dev/x/a.ts", "c:/dev/x/b.ts"]); + }); + it("`[]` matches only a single character", () => { + let includeSpecs = ["x/[b-z].ts"]; + let results = ts.expandFiles(basePath, /*fileNames*/ undefined, includeSpecs, /*excludeSpecs*/ undefined, {}, caseInsensitiveHost); + assert.deepEqual(results, ["c:/dev/x/b.ts"]); + }); + it("with recursive directory", () => { + let includeSpecs = ["**/a.ts"]; + let results = ts.expandFiles(basePath, /*fileNames*/ undefined, includeSpecs, /*excludeSpecs*/ undefined, {}, 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", () => { + let includeSpecs = ["**/A.ts"]; + let results = ts.expandFiles(basePath, /*fileNames*/ undefined, includeSpecs, /*excludeSpecs*/ undefined, {}, caseSensitiveHost); + assert.deepEqual(results, ["c:/dev/A.ts"]); + }); + it("with missing files are excluded", () => { + let includeSpecs = ["*/z.ts"]; + let results = ts.expandFiles(basePath, /*fileNames*/ undefined, includeSpecs, /*excludeSpecs*/ undefined, {}, caseInsensitiveHost); + assert.deepEqual(results, []); + }); + it("always include literal files", () => { + let fileNames = ["a.ts"]; + let includeSpecs = ["*/z.ts"]; + let excludeSpecs = ["**/a.ts"]; + let results = ts.expandFiles(basePath, fileNames, includeSpecs, excludeSpecs, {}, caseInsensitiveHost); + assert.deepEqual(results, ["c:/dev/a.ts"]); + }); + }); + + function createMockParseConfigHost(basePath: string, files: string[], ignoreCase: boolean): ts.ParseConfigHost { + let fileSet: ts.Map = {}; + let directorySet: ts.Map> = {}; + + files.sort((a, b) => ts.comparePaths(a, b, basePath, ignoreCase)); + for (let file of files) { + addFile(ts.getNormalizedAbsolutePath(file, basePath)); + } + + return { + useCaseSensitiveFileNames: !ignoreCase, + fileExists, + directoryExists, + readDirectory, + readDirectoryFlat + }; + + function fileExists(path: string): boolean { + let fileKey = ignoreCase ? path.toLowerCase() : path; + return ts.hasProperty(fileSet, fileKey); + } + + function directoryExists(path: string): boolean { + let directoryKey = ignoreCase ? path.toLowerCase() : path; + return ts.hasProperty(directorySet, directoryKey); + } + + function readDirectory(rootDir: string, extension: string): string[] { + throw new Error("Not implemented"); + } + + function readDirectoryFlat(path: string): string[] { + path = ts.getNormalizedAbsolutePath(path, basePath); + path = ts.removeTrailingDirectorySeparator(path); + let directoryKey = ignoreCase ? path.toLowerCase() : path; + let entries = ts.getProperty(directorySet, directoryKey); + let result: string[] = []; + ts.forEachKey(entries, key => { result.push(key); }); + result.sort((a, b) => ts.compareStrings(a, b, ignoreCase)); + return result; + } + + function addFile(file: string) { + let fileKey = ignoreCase ? file.toLowerCase() : file; + if (!ts.hasProperty(fileSet, fileKey)) { + fileSet[fileKey] = file; + let name = ts.getBaseFileName(file); + let parent = ts.getDirectoryPath(file); + addToDirectory(parent, name); + } + } + + function addDirectory(directory: string) { + directory = ts.removeTrailingDirectorySeparator(directory); + let directoryKey = ignoreCase ? directory.toLowerCase() : directory; + if (!ts.hasProperty(directorySet, directoryKey)) { + directorySet[directoryKey] = {}; + let name = ts.getBaseFileName(directory); + let parent = ts.getDirectoryPath(directory); + if (parent !== directory) { + addToDirectory(parent, name); + } + } + } + + function addToDirectory(directory: string, entry: string) { + addDirectory(directory); + directory = ts.removeTrailingDirectorySeparator(directory); + let directoryKey = ignoreCase ? directory.toLowerCase() : directory; + let entryKey = ignoreCase ? entry.toLowerCase() : entry; + let entries = directorySet[directoryKey]; + if (!ts.hasProperty(entries, entryKey)) { + entries[entryKey] = entry; + } + } + } +});