Skip to content
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
merged 14 commits into from
Jun 20, 2016
Merged
4 changes: 3 additions & 1 deletion Jakefile.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ var languageServiceLibrarySources = [

var harnessCoreSources = [
"harness.ts",
"vfs.ts",
"sourceMapRecorder.ts",
"harnessLanguageService.ts",
"fourslash.ts",
Expand Down Expand Up @@ -161,7 +162,8 @@ var harnessSources = harnessCoreSources.concat([
"reuseProgramStructure.ts",
"cachingInServerLSHost.ts",
"moduleResolution.ts",
"tsconfigParsing.ts"
"tsconfigParsing.ts",
"expandFiles.ts"
Copy link
Contributor

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\**\

Copy link
Member Author

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

].map(function (f) {
return path.join(unittestsDirectory, f);
})).concat([
Expand Down
292 changes: 259 additions & 33 deletions src/compiler/commandLineParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -584,4 +581,233 @@ namespace ts {

return { options, errors };
}

const invalidTrailingRecursionPattern = /(^|\/)\*\*\/?$/;
Copy link
Contributor

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

The 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();
}
}
Loading