-
Notifications
You must be signed in to change notification settings - Fork 12.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adds glob-style pattern matching for files in tsconfig.json #5980
Merged
Changes from 8 commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
f9ae3e4
Initial support for globs in tsconfig.json
rbuckton 30575db
Added caching, more tests
rbuckton 5de2fcc
Merge branch 'master' into glob2
rbuckton def3ba1
Added stubs to ChakraHost interface for readDirectoryNames/readFileNames
rbuckton c3c3bca
Fixed typos in comments
rbuckton bf9af46
Changed name of 'reduce' method added to FileSet
rbuckton d857250
Heavily revised implementation that relies on an updated 'readDirecto…
rbuckton 247657f
Merge branch 'master' into glob2
rbuckton 94a5327
more tests
rbuckton 6f85fe9
Minor update to shims.ts for forthcoming VS support for globs.
rbuckton d23df34
Detailed comments for regular expressions and renamed some files.
rbuckton c224917
Comment cleanup
rbuckton cde12ef
Merge branch 'master' into glob2
rbuckton c1205eb
Fixed new linter warnings
rbuckton File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -488,53 +488,50 @@ namespace ts { | |
const { options: optionsFromJsonConfigFile, errors } = convertCompilerOptionsFromJson(json["compilerOptions"], basePath); | ||
|
||
const options = extend(existingOptions, optionsFromJsonConfigFile); | ||
const { fileNames, wildcardDirectories } = getFileNames(); | ||
return { | ||
options, | ||
fileNames: getFileNames(), | ||
errors | ||
fileNames, | ||
errors, | ||
wildcardDirectories | ||
}; | ||
|
||
function getFileNames(): string[] { | ||
let fileNames: string[] = []; | ||
function getFileNames(): ExpandResult { | ||
let fileNames: string[]; | ||
if (hasProperty(json, "files")) { | ||
if (json["files"] instanceof Array) { | ||
fileNames = map(<string[]>json["files"], s => combinePaths(basePath, s)); | ||
if (isArray(json["files"])) { | ||
fileNames = <string[]>json["files"]; | ||
} | ||
else { | ||
errors.push(createCompilerDiagnostic(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "files", "Array")); | ||
} | ||
} | ||
else { | ||
const filesSeen: Map<boolean> = {}; | ||
const exclude = json["exclude"] instanceof Array ? map(<string[]>json["exclude"], normalizeSlashes) : undefined; | ||
const supportedExtensions = getSupportedExtensions(options); | ||
Debug.assert(indexOf(supportedExtensions, ".ts") < indexOf(supportedExtensions, ".d.ts"), "Changed priority of extensions to pick"); | ||
|
||
// Get files of supported extensions in their order of resolution | ||
for (const extension of supportedExtensions) { | ||
const filesInDirWithExtension = host.readDirectory(basePath, extension, exclude); | ||
for (const fileName of filesInDirWithExtension) { | ||
// .ts extension would read the .d.ts extension files too but since .d.ts is lower priority extension, | ||
// lets pick them when its turn comes up | ||
if (extension === ".ts" && fileExtensionIs(fileName, ".d.ts")) { | ||
continue; | ||
} | ||
|
||
// If this is one of the output extension (which would be .d.ts and .js if we are allowing compilation of js files) | ||
// do not include this file if we included .ts or .tsx file with same base name as it could be output of the earlier compilation | ||
if (extension === ".d.ts" || (options.allowJs && contains(supportedJavascriptExtensions, extension))) { | ||
const baseName = fileName.substr(0, fileName.length - extension.length); | ||
if (hasProperty(filesSeen, baseName + ".ts") || hasProperty(filesSeen, baseName + ".tsx")) { | ||
continue; | ||
} | ||
} | ||
let includeSpecs: string[]; | ||
if (hasProperty(json, "include")) { | ||
if (isArray(json["include"])) { | ||
includeSpecs = <string[]>json["include"]; | ||
} | ||
else { | ||
errors.push(createCompilerDiagnostic(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "include", "Array")); | ||
} | ||
} | ||
|
||
filesSeen[fileName] = true; | ||
fileNames.push(fileName); | ||
} | ||
let excludeSpecs: string[]; | ||
if (hasProperty(json, "exclude")) { | ||
if (isArray(json["exclude"])) { | ||
excludeSpecs = <string[]>json["exclude"]; | ||
} | ||
else { | ||
errors.push(createCompilerDiagnostic(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "exclude", "Array")); | ||
} | ||
} | ||
return fileNames; | ||
|
||
if (fileNames === undefined && includeSpecs === undefined) { | ||
includeSpecs = ["**/*"]; | ||
} | ||
|
||
return expandFiles(fileNames, includeSpecs, excludeSpecs, basePath, options, host, errors); | ||
} | ||
} | ||
|
||
|
@@ -584,4 +581,233 @@ namespace ts { | |
|
||
return { options, errors }; | ||
} | ||
|
||
const invalidTrailingRecursionPattern = /(^|\/)\*\*\/?$/; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you add comments to all these regular expressions.. they are impossible to parse. |
||
const invalidMultipleRecursionPatterns = /(^|\/)\*\*\/(.*\/)?\*\*($|\/)/; | ||
|
||
/** | ||
* Expands an array of file specifications. | ||
* | ||
* @param fileNames The literal file names to include. | ||
* @param includeSpecs The file specifications to expand. | ||
* @param excludeSpecs The file specifications to exclude. | ||
* @param basePath The base path for any relative file specifications. | ||
* @param options Compiler options. | ||
* @param host The host used to resolve files and directories. | ||
* @param errors An array for diagnostic reporting. | ||
*/ | ||
export function expandFiles(fileNames: string[], includeSpecs: string[], excludeSpecs: string[], basePath: string, options: CompilerOptions, host: ParseConfigHost, errors?: Diagnostic[]): ExpandResult { | ||
basePath = normalizePath(basePath); | ||
basePath = removeTrailingDirectorySeparator(basePath); | ||
|
||
// The exclude spec list is converted into a regular expression, which allows us to quickly | ||
// test whether a file or directory should be excluded before recursively traversing the | ||
// file system. | ||
const keyMapper = host.useCaseSensitiveFileNames ? caseSensitiveKeyMapper : caseInsensitiveKeyMapper; | ||
|
||
// Literal file names (provided via the "files" array in tsconfig.json) are stored in a | ||
// file map with a possibly case insensitive key. We use this map later when when including | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We use this map later when including |
||
// wildcard paths. | ||
const literalFileMap: Map<string> = {}; | ||
|
||
// Wildcard paths (provided via the "includes" array in tsconfig.json) are stored in a | ||
// file map with a possibly case insensitive key. We use this map to store paths matched | ||
// via wildcard, and to handle extension priority. | ||
const wildcardFileMap: Map<string> = {}; | ||
|
||
// Wildcard directories (provided as part of a wildcard path) are stored in a | ||
// file map that marks whether it was a regular wildcard match (with a `*` or `?` token), | ||
// or a recursive directory. This information is used by filesystem watchers to monitor for | ||
// new entries in these paths. | ||
const wildcardDirectories: Map<WatchDirectoryFlags> = getWildcardDirectories(includeSpecs, basePath, host.useCaseSensitiveFileNames); | ||
|
||
// Rather than requery this for each file and filespec, we query the supported extensions | ||
// once and store it on the expansion context. | ||
const supportedExtensions = getSupportedExtensions(options); | ||
|
||
// Literal files are always included verbatim. An "include" or "exclude" specification cannot | ||
// remove a literal file. | ||
if (fileNames) { | ||
for (const fileName of fileNames) { | ||
const file = combinePaths(basePath, fileName); | ||
literalFileMap[keyMapper(file)] = file; | ||
} | ||
} | ||
|
||
if (includeSpecs) { | ||
includeSpecs = validateSpecs(includeSpecs, errors, /*allowTrailingRecursion*/ false); | ||
if (excludeSpecs) { | ||
excludeSpecs = validateSpecs(excludeSpecs, errors, /*allowTrailingRecursion*/ true); | ||
} | ||
|
||
for (const file of host.readDirectory(basePath, supportedExtensions, excludeSpecs, includeSpecs)) { | ||
// If we have already included a literal or wildcard path with a | ||
// higher priority extension, we should skip this file. | ||
// | ||
// This handles cases where we may encounter both <file>.ts and | ||
// <file>.d.ts (or <file>.js if "allowJs" is enabled) in the same | ||
// directory when they are compilation outputs. | ||
if (hasFileWithHigherPriorityExtension(file, literalFileMap, wildcardFileMap, supportedExtensions, keyMapper)) { | ||
continue; | ||
} | ||
|
||
// We may have included a wildcard path with a lower priority | ||
// extension due to the user-defined order of entries in the | ||
// "include" array. If there is a lower priority extension in the | ||
// same directory, we should remove it. | ||
removeWildcardFilesWithLowerPriorityExtension(file, wildcardFileMap, supportedExtensions, keyMapper); | ||
|
||
const key = keyMapper(file); | ||
if (!hasProperty(literalFileMap, key) && !hasProperty(wildcardFileMap, key)) { | ||
wildcardFileMap[key] = file; | ||
} | ||
} | ||
} | ||
|
||
const literalFiles = reduceProperties(literalFileMap, addFileToOutput, []); | ||
const wildcardFiles = reduceProperties(wildcardFileMap, addFileToOutput, []); | ||
wildcardFiles.sort(host.useCaseSensitiveFileNames ? compareStrings : compareStringsCaseInsensitive); | ||
return { | ||
fileNames: literalFiles.concat(wildcardFiles), | ||
wildcardDirectories | ||
}; | ||
} | ||
|
||
function validateSpecs(specs: string[], errors: Diagnostic[], allowTrailingRecursion: boolean) { | ||
const validSpecs: string[] = []; | ||
for (const spec of specs) { | ||
if (!allowTrailingRecursion && invalidTrailingRecursionPattern.test(spec)) { | ||
errors.push(createCompilerDiagnostic(Diagnostics.File_specification_cannot_contain_multiple_recursive_directory_wildcards_Asterisk_Asterisk_Colon_0, spec)); | ||
} | ||
else if (invalidMultipleRecursionPatterns.test(spec)) { | ||
errors.push(createCompilerDiagnostic(Diagnostics.File_specification_cannot_end_in_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, spec)); | ||
} | ||
else { | ||
validSpecs.push(spec); | ||
} | ||
} | ||
|
||
return validSpecs; | ||
} | ||
|
||
const watchRecursivePattern = /\/[^/]*?[*?][^/]*\//; | ||
const wildcardDirectoryPattern = /^[^*?]*(?=\/[^/]*[*?])/; | ||
|
||
/** | ||
* Gets directories in a set of include patterns that should be watched for changes. | ||
*/ | ||
function getWildcardDirectories(includes: string[], path: string, useCaseSensitiveFileNames: boolean) { | ||
// We watch a directory recursively if it contains a wildcard anywhere in a directory segment | ||
// of the pattern: | ||
// | ||
// /a/b/**/d - Watch /a/b recursively to catch changes to any d in any subfolder recursively | ||
// /a/b/*/d - Watch /a/b recursively to catch any d in any immediate subfolder, even if a new subfolder is added | ||
// | ||
// We watch a directory without recursion if it contains a wildcard in the file segment of | ||
// the pattern: | ||
// | ||
// /a/b/* - Watch /a/b directly to catch any new file | ||
// /a/b/a?z - Watch /a/b directly to catch any new file matching a?z | ||
const wildcardDirectories: Map<WatchDirectoryFlags> = {}; | ||
if (includes !== undefined) { | ||
const recursiveKeys: string[] = []; | ||
for (const include of includes) { | ||
const name = combinePaths(path, include); | ||
const match = wildcardDirectoryPattern.exec(name); | ||
if (match) { | ||
const key = useCaseSensitiveFileNames ? match[0] : match[0].toLowerCase(); | ||
const flags = watchRecursivePattern.test(name) ? WatchDirectoryFlags.Recursive : WatchDirectoryFlags.None; | ||
const existingFlags = getProperty(wildcardDirectories, key); | ||
if (existingFlags === undefined || existingFlags < flags) { | ||
wildcardDirectories[key] = flags; | ||
if (flags === WatchDirectoryFlags.Recursive) { | ||
recursiveKeys.push(key); | ||
} | ||
} | ||
} | ||
} | ||
|
||
// Remove any subpaths under an existing recursively watched directory. | ||
for (const key in wildcardDirectories) { | ||
if (hasProperty(wildcardDirectories, key)) { | ||
for (const recursiveKey in recursiveKeys) { | ||
if (containsPath(recursiveKey, key, path, !useCaseSensitiveFileNames)) { | ||
delete wildcardDirectories[key]; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
return wildcardDirectories; | ||
} | ||
|
||
/** | ||
* Determines whether a literal or wildcard file has already been included that has a higher | ||
* extension priority. | ||
* | ||
* @param file The path to the file. | ||
* @param extensionPriority The priority of the extension. | ||
* @param context The expansion context. | ||
*/ | ||
function hasFileWithHigherPriorityExtension(file: string, literalFiles: Map<string>, wildcardFiles: Map<string>, extensions: string[], keyMapper: (value: string) => string) { | ||
const extensionPriority = getExtensionPriority(file, extensions); | ||
const adjustedExtensionPriority = adjustExtensionPriority(extensionPriority); | ||
for (let i = ExtensionPriority.Highest; i < adjustedExtensionPriority; ++i) { | ||
const higherPriorityExtension = extensions[i]; | ||
const higherPriorityPath = keyMapper(changeExtension(file, higherPriorityExtension)); | ||
if (hasProperty(literalFiles, higherPriorityPath) || hasProperty(wildcardFiles, higherPriorityPath)) { | ||
return true; | ||
} | ||
} | ||
|
||
return false; | ||
} | ||
|
||
/** | ||
* Removes files included via wildcard expansion with a lower extension priority that have | ||
* already been included. | ||
* | ||
* @param file The path to the file. | ||
* @param extensionPriority The priority of the extension. | ||
* @param context The expansion context. | ||
*/ | ||
function removeWildcardFilesWithLowerPriorityExtension(file: string, wildcardFiles: Map<string>, extensions: string[], keyMapper: (value: string) => string) { | ||
const extensionPriority = getExtensionPriority(file, extensions); | ||
const nextExtensionPriority = getNextLowestExtensionPriority(extensionPriority); | ||
for (let i = nextExtensionPriority; i < extensions.length; ++i) { | ||
const lowerPriorityExtension = extensions[i]; | ||
const lowerPriorityPath = keyMapper(changeExtension(file, lowerPriorityExtension)); | ||
delete wildcardFiles[lowerPriorityPath]; | ||
} | ||
} | ||
|
||
/** | ||
* Adds a file to an array of files. | ||
* | ||
* @param output The output array. | ||
* @param file The file path. | ||
*/ | ||
function addFileToOutput(output: string[], file: string) { | ||
output.push(file); | ||
return output; | ||
} | ||
|
||
/** | ||
* Gets a case sensitive key. | ||
* | ||
* @param key The original key. | ||
*/ | ||
function caseSensitiveKeyMapper(key: string) { | ||
return key; | ||
} | ||
|
||
/** | ||
* Gets a case insensitive key. | ||
* | ||
* @param key The original key. | ||
*/ | ||
function caseInsensitiveKeyMapper(key: string) { | ||
return key.toLowerCase(); | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add this file to the tsconfig.json files in
src\**\
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is no tsconfig.json in either src/harness or tests/cases/unittests