Initial support for globs in tsconfig.json

This commit is contained in:
Ron Buckton
2015-12-03 10:44:24 -08:00
parent 7d920c2aad
commit f9ae3e4f2b
14 changed files with 1086 additions and 87 deletions

View File

@@ -495,46 +495,51 @@ namespace ts {
};
function getFileNames(): string[] {
let fileNames: string[] = [];
let fileNames: string[];
if (hasProperty(json, "files")) {
if (json["files"] instanceof Array) {
fileNames = map(<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;
}
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"));
}
}
// If this is one of the output extension (which would be .d.ts and .js if we are allowing compilation of js files)
// do not include this file if we included .ts or .tsx file with same base name as it could be output of the earlier compilation
if (extension === ".d.ts" || (options.allowJs && contains(supportedJavascriptExtensions, extension))) {
const baseName = fileName.substr(0, fileName.length - extension.length);
if (hasProperty(filesSeen, baseName + ".ts") || hasProperty(filesSeen, baseName + ".tsx")) {
continue;
}
}
let excludeSpecs: string[];
if (hasProperty(json, "exclude")) {
if (isArray(json["exclude"])) {
excludeSpecs = <string[]>json["exclude"];
}
else {
errors.push(createCompilerDiagnostic(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "exclude", "Array"));
}
}
filesSeen[fileName] = true;
fileNames.push(fileName);
if (fileNames === undefined && includeSpecs === undefined) {
includeSpecs = ["**/*.ts"];
if (options.jsx) {
includeSpecs.push("**/*.tsx");
}
if (options.allowJs) {
includeSpecs.push("**/*.js");
if (options.jsx) {
includeSpecs.push("**/*.jsx");
}
}
}
return fileNames;
return expandFiles(fileNames, includeSpecs, excludeSpecs, basePath, options, host, errors);
}
}
@@ -584,4 +589,514 @@ namespace ts {
return { options, errors };
}
// Simplified whitelist, forces escaping of any non-word (or digit), non-whitespace character.
const reservedCharacterPattern = /[^\w\s]/g;
const enum ExpandResult {
Ok,
Error
}
/**
* Expands an array of file specifications.
*
* @param fileNames The literal file names to include.
* @param includeSpecs The file specifications to expand.
* @param excludeSpecs The file specifications to exclude.
* @param basePath The base path for any relative file specifications.
* @param options Compiler options.
* @param host The host used to resolve files and directories.
* @param errors An array for diagnostic reporting.
*/
export function expandFiles(fileNames: string[], includeSpecs: string[], excludeSpecs: string[], basePath: string, options: CompilerOptions, host: ParseConfigHost, errors?: Diagnostic[]): string[] {
basePath = normalizePath(basePath);
basePath = removeTrailingDirectorySeparator(basePath);
const excludePattern = includeSpecs ? createExcludeRegularExpression(excludeSpecs, basePath, options, host, errors) : undefined;
const fileSet = createFileMap<Path>(host.useCaseSensitiveFileNames ? caseSensitiveKeyMapper : caseInsensitiveKeyMapper);
// include every literal file.
if (fileNames) {
for (const fileName of fileNames) {
const path = toPath(fileName, basePath, caseSensitiveKeyMapper);
if (!fileSet.contains(path)) {
fileSet.set(path, path);
}
}
}
// expand and include the provided files into the file set.
if (includeSpecs) {
for (let includeSpec of includeSpecs) {
includeSpec = normalizePath(includeSpec);
includeSpec = removeTrailingDirectorySeparator(includeSpec);
expandFileSpec(basePath, includeSpec, 0, excludePattern, options, host, errors, fileSet);
}
}
const output = fileSet.reduce(addFileToOutput, []);
return output;
}
/**
* Expands a file specification with wildcards.
*
* @param basePath The directory to expand.
* @param fileSpec The original file specification.
* @param start The starting offset in the file specification.
* @param excludePattern A pattern used to exclude a file specification.
* @param options Compiler options.
* @param host The host used to resolve files and directories.
* @param errors An array for diagnostic reporting.
* @param fileSet The set of matching files.
* @param isExpandingRecursiveDirectory A value indicating whether the file specification includes a recursive directory wildcard prior to the start of this segment.
*/
function expandFileSpec(basePath: string, fileSpec: string, start: number, excludePattern: RegExp, options: CompilerOptions, host: ParseConfigHost, errors: Diagnostic[], fileSet: FileMap<Path>, isExpandingRecursiveDirectory?: boolean): ExpandResult {
// Skip expansion if the base path matches an exclude pattern.
if (isExcludedPath(excludePattern, basePath)) {
return ExpandResult.Ok;
}
// Find the offset of the next wildcard in the file specification
let offset = indexOfWildcard(fileSpec, start);
if (offset < 0) {
// There were no more wildcards, so include the file.
const path = toPath(fileSpec.substring(start), basePath, caseSensitiveKeyMapper);
includeFile(path, excludePattern, options, host, fileSet);
return ExpandResult.Ok;
}
// Find the last directory separator before the wildcard to get the leading path.
offset = fileSpec.lastIndexOf(directorySeparator, offset);
if (offset > start) {
// The wildcard occurs in a later segment, include remaining path up to
// wildcard in prefix.
basePath = combinePaths(basePath, fileSpec.substring(start, offset));
// Skip this wildcard path if the base path now matches an exclude pattern.
if (isExcludedPath(excludePattern, basePath)) {
return ExpandResult.Ok;
}
start = offset + 1;
}
// Find the offset of the next directory separator to extract the wildcard path segment.
offset = getEndOfPathSegment(fileSpec, start);
// Check if the current offset is the beginning of a recursive directory pattern.
if (isRecursiveDirectoryWildcard(fileSpec, start, offset)) {
if (offset >= fileSpec.length) {
// If there is no file specification following the recursive directory pattern
// we cannot match any files, so we will ignore this pattern.
return ExpandResult.Ok;
}
// Stop expansion if a file specification contains more than one recursive directory pattern.
if (isExpandingRecursiveDirectory) {
if (errors) {
errors.push(createCompilerDiagnostic(Diagnostics.File_specification_cannot_contain_multiple_recursive_directory_wildcards_Asterisk_Asterisk_Colon_0, fileSpec));
}
return ExpandResult.Error;
}
// Expand the recursive directory pattern.
return expandRecursiveDirectory(basePath, fileSpec, offset + 1, excludePattern, options, host, errors, fileSet);
}
// Match the entries in the directory against the wildcard pattern.
const pattern = createRegularExpressionFromWildcard(fileSpec, start, offset, host);
// If there are no more directory separators (the offset is at the end of the file specification), then
// this must be a file.
if (offset >= fileSpec.length) {
const files = host.readFileNames(basePath);
for (const extension of getSupportedExtensions(options)) {
for (const file of files) {
if (fileExtensionIs(file, extension)) {
const path = toPath(file, basePath, caseSensitiveKeyMapper);
// .ts extension would read the .d.ts extension files too but since .d.ts is lower priority extension,
// lets pick them when its turn comes up.
if (extension === ".ts" && fileExtensionIs(file, ".d.ts")) {
continue;
}
// If this is one of the output extension (which would be .d.ts and .js if we are allowing compilation of js files)
// do not include this file if we included .ts or .tsx file with same base name as it could be output of the earlier compilation
if (extension === ".d.ts" || (options.allowJs && contains(supportedJavascriptExtensions, extension))) {
if (fileSet.contains(changeExtension(path, ".ts")) || fileSet.contains(changeExtension(path, ".tsx"))) {
continue;
}
}
// This wildcard has no further directory to process, so include the file.
includeFile(path, excludePattern, options, host, fileSet);
}
}
}
}
else {
const directories = host.readDirectoryNames(basePath);
for (const directory of directories) {
// If this was a directory, process the directory.
const path = toPath(directory, basePath, caseSensitiveKeyMapper);
if (expandFileSpec(path, fileSpec, offset + 1, excludePattern, options, host, errors, fileSet, isExpandingRecursiveDirectory) === ExpandResult.Error) {
return ExpandResult.Error;
}
}
}
return ExpandResult.Ok;
}
/**
* Expands a `**` recursive directory wildcard.
*
* @param basePath The directory to recursively expand.
* @param fileSpec The original file specification.
* @param start The starting offset in the file specification.
* @param excludePattern A pattern used to exclude a file specification.
* @param options Compiler options.
* @param host The host used to resolve files and directories.
* @param errors An array for diagnostic reporting.
* @param fileSet The set of matching files.
*/
function expandRecursiveDirectory(basePath: string, fileSpec: string, start: number, excludePattern: RegExp, options: CompilerOptions, host: ParseConfigHost, errors: Diagnostic[], fileSet: FileMap<Path>): ExpandResult {
// expand the non-recursive part of the file specification against the prefix path.
if (expandFileSpec(basePath, fileSpec, start, excludePattern, options, host, errors, fileSet, /*isExpandingRecursiveDirectory*/ true) === ExpandResult.Error) {
return ExpandResult.Error;
}
// Recursively expand each subdirectory.
const directories = host.readDirectoryNames(basePath);
for (const entry of directories) {
const path = combinePaths(basePath, entry);
if (expandRecursiveDirectory(path, fileSpec, start, excludePattern, options, host, errors, fileSet) === ExpandResult.Error) {
return ExpandResult.Error;
}
}
return ExpandResult.Ok;
}
/**
* Attempts to include a file in a file set.
*
* @param file The file to include.
* @param excludePattern A pattern used to exclude a file specification.
* @param options Compiler options.
* @param host The host used to resolve files and directories.
* @param fileSet The set of matching files.
*/
function includeFile(file: Path, excludePattern: RegExp, options: CompilerOptions, host: ParseConfigHost, fileSet: FileMap<Path>): void {
// Ignore the file if it should be excluded.
if (isExcludedPath(excludePattern, file)) {
return;
}
// Ignore the file if it doesn't exist.
if (!host.fileExists(file)) {
return;
}
// Ignore the file if it does not have a supported extension.
if (!options.allowNonTsExtensions && !isSupportedSourceFileName(file, options)) {
return;
}
if (!fileSet.contains(file)) {
fileSet.set(file, file);
}
}
/**
* Adds a file to an array of files.
*
* @param output The output array.
* @param file The file path.
*/
function addFileToOutput(output: string[], file: string) {
output.push(file);
return output;
}
/**
* Determines whether a path should be excluded.
*
* @param excludePattern A pattern used to exclude a file specification.
* @param path The path to test for exclusion.
*/
function isExcludedPath(excludePattern: RegExp, path: string) {
return excludePattern ? excludePattern.test(path) : false;
}
/**
* Creates a regular expression from a glob-style wildcard.
*
* @param fileSpec The file specification.
* @param start The starting offset in the file specification.
* @param end The end offset in the file specification.
* @param host The host used to resolve files and directories.
*/
function createRegularExpressionFromWildcard(fileSpec: string, start: number, end: number, host: ParseConfigHost): RegExp {
const pattern = createPatternFromWildcard(fileSpec, start, end);
return new RegExp("^" + pattern + "$", host.useCaseSensitiveFileNames ? "" : "i");
}
/**
* Creates a pattern from a wildcard segment.
*
* @param fileSpec The file specification.
* @param start The starting offset in the file specification.
* @param end The end offset in the file specification.
*/
function createPatternFromWildcard(fileSpec: string, start: number, end: number): string {
let pattern = "";
let offset = indexOfWildcard(fileSpec, start);
while (offset >= 0 && offset < end) {
if (offset > start) {
// Escape and append the non-wildcard portion to the regular expression
pattern += escapeRegularExpressionText(fileSpec, start, offset);
}
const charCode = fileSpec.charCodeAt(offset);
if (charCode === CharacterCodes.asterisk) {
// Append a multi-character (zero or more characters) pattern to the regular expression
pattern += "[^/]*";
}
else if (charCode === CharacterCodes.question) {
// Append a single-character (zero or one character) pattern to the regular expression
pattern += "[^/]";
}
start = offset + 1;
offset = indexOfWildcard(fileSpec, start);
}
// Escape and append any remaining non-wildcard portion.
if (start < end) {
pattern += escapeRegularExpressionText(fileSpec, start, end);
}
return pattern;
}
/**
* Creates a regular expression from a glob-style wildcard used to exclude a file.
*
* @param excludeSpecs The file specifications to exclude.
* @param basePath The prefix path.
* @param options Compiler options.
* @param host The host used to resolve files and directories.
* @param errors An array for diagnostic reporting.
*/
function createExcludeRegularExpression(excludeSpecs: string[], basePath: string, options: CompilerOptions, host: ParseConfigHost, errors: Diagnostic[]): RegExp {
// Ignore an empty exclusion list
if (!excludeSpecs || excludeSpecs.length === 0) {
return undefined;
}
basePath = escapeRegularExpressionText(basePath, 0, basePath.length);
let pattern = "";
for (const excludeSpec of excludeSpecs) {
const excludePattern = createExcludePattern(excludeSpec, basePath, options, host, errors);
if (excludePattern) {
if (pattern.length > 0) {
pattern += "|";
}
pattern += "(" + excludePattern + ")";
}
}
if (pattern.length > 0) {
return new RegExp("^(" + pattern + ")($|/)", host.useCaseSensitiveFileNames ? "" : "i");
}
return undefined;
}
/**
* Creates a pattern for used to exclude a file.
*
* @param excludeSpec The file specification to exclude.
* @param basePath The base path for the exclude pattern.
* @param options Compiler options.
* @param host The host used to resolve files and directories.
* @param errors An array for diagnostic reporting.
*/
function createExcludePattern(excludeSpec: string, basePath: string, options: CompilerOptions, host: ParseConfigHost, errors: Diagnostic[]): string {
if (!excludeSpec) {
return undefined;
}
excludeSpec = normalizePath(excludeSpec);
excludeSpec = removeTrailingDirectorySeparator(excludeSpec);
let pattern = isRootedDiskPath(excludeSpec) ? "" : basePath;
let hasRecursiveDirectoryWildcard = false;
let segmentStart = 0;
let segmentEnd = getEndOfPathSegment(excludeSpec, segmentStart);
while (segmentStart < segmentEnd) {
if (isRecursiveDirectoryWildcard(excludeSpec, segmentStart, segmentEnd)) {
if (hasRecursiveDirectoryWildcard) {
if (errors) {
errors.push(createCompilerDiagnostic(Diagnostics.File_specification_cannot_contain_multiple_recursive_directory_wildcards_Asterisk_Asterisk_Colon_0, excludeSpec));
}
return undefined;
}
// As an optimization, if the recursive directory is the last
// wildcard, or is followed by only `*` or `*.ts`, don't add the
// remaining pattern and exit the loop.
if (canElideRecursiveDirectorySegment(excludeSpec, segmentEnd, options, host)) {
break;
}
hasRecursiveDirectoryWildcard = true;
pattern += "(/.+)?";
}
else {
if (pattern) {
pattern += directorySeparator;
}
pattern += createPatternFromWildcard(excludeSpec, segmentStart, segmentEnd);
}
segmentStart = segmentEnd + 1;
segmentEnd = getEndOfPathSegment(excludeSpec, segmentStart);
}
return pattern;
}
/**
* Determines whether a recursive directory segment can be elided when
* building a regular expression to exclude a path.
*
* @param excludeSpec The file specification used to exclude a path.
* @param segmentEnd The end position of the recursive directory segment.
* @param options Compiler options.
* @param host The host used to resolve files and directories.
*/
function canElideRecursiveDirectorySegment(excludeSpec: string, segmentEnd: number, options: CompilerOptions, host: ParseConfigHost) {
// If there are no segments after this segment, the pattern for this segment may be elided.
if (segmentEnd + 1 >= excludeSpec.length) {
return true;
}
// If the following segment is a wildcard that may be elided, the pattern for this segment may be elided.
return canElideWildcardSegment(excludeSpec, segmentEnd + 1, options, host);
}
/**
* Determines whether a wildcard segment can be elided when building a
* regular expression to exclude a path.
*
* @param excludeSpec The file specification used to exclude a path.
* @param segmentStart The starting position of the segment.
* @param options Compiler options.
* @param host The host used to resolve files and directories.
*/
function canElideWildcardSegment(excludeSpec: string, segmentStart: number, options: CompilerOptions, host: ParseConfigHost) {
const charCode = excludeSpec.charCodeAt(segmentStart);
if (charCode === CharacterCodes.asterisk) {
const end = excludeSpec.length;
// If the segment consists only of `*`, we may elide this segment.
if (segmentStart + 1 === end) {
return true;
}
// If the segment consists only of `*.ts`, and we do not allow
// any other extensions for source files, we may elide this segment.
if (!options.allowNonTsExtensions && !options.jsx && !options.allowJs && segmentStart + 4 === end) {
const segment = excludeSpec.substr(segmentStart);
return fileExtensionIs(host.useCaseSensitiveFileNames ? segment : segment.toLowerCase(), ".ts");
}
}
return false;
}
/**
* Escape regular expression reserved tokens.
*
* @param text The text to escape.
* @param start The starting offset in the string.
* @param end The ending offset in the string.
*/
function escapeRegularExpressionText(text: string, start: number, end: number) {
return text.substring(start, end).replace(reservedCharacterPattern, "\\$&");
}
/**
* Determines whether the wildcard at the current offset is a recursive directory wildcard.
*
* @param fileSpec The file specification.
* @param segmentStart The starting offset of a segment in the file specification.
* @param segmentEnd The ending offset of a segment in the file specification.
*/
function isRecursiveDirectoryWildcard(fileSpec: string, segmentStart: number, segmentEnd: number) {
return segmentEnd - segmentStart === 2 &&
fileSpec.charCodeAt(segmentStart) === CharacterCodes.asterisk &&
fileSpec.charCodeAt(segmentStart + 1) === CharacterCodes.asterisk;
}
/**
* Gets the index of the next wildcard character in a file specification.
*
* @param fileSpec The file specification.
* @param start The starting offset in the file specification.
*/
function indexOfWildcard(fileSpec: string, start: number): number {
for (let i = start; i < fileSpec.length; ++i) {
const ch = fileSpec.charCodeAt(i);
if (ch === CharacterCodes.asterisk || ch === CharacterCodes.question) {
return i;
}
}
return -1;
}
/**
* Get the end position of a path segment, either the index of the next directory separator or
* the provided end position.
*
* @param fileSpec The file specification.
* @param segmentStart The start offset in the file specification.
*/
function getEndOfPathSegment(fileSpec: string, segmentStart: number): number {
const end = fileSpec.length;
if (segmentStart >= end) {
return end;
}
const offset = fileSpec.indexOf(directorySeparator, segmentStart);
return offset < 0 ? end : offset;
}
/**
* Gets a case sensitive key.
*
* @param key The original key.
*/
function caseSensitiveKeyMapper(key: string) {
return key;
}
/**
* Gets a case insensitive key.
*
* @param key The original key.
*/
function caseInsensitiveKeyMapper(key: string) {
return key.toLowerCase();
}
}