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

Implicitly consider an extensionless file in "includes" to be a recursive directory glob #11495

Merged
5 commits merged into from
Oct 31, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 21 additions & 9 deletions src/compiler/commandLineParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1008,9 +1008,7 @@ namespace ts {
function convertTypingOptionsFromJsonWorker(jsonOptions: any,
basePath: string, errors: Diagnostic[], configFileName?: string): TypingOptions {

const options: TypingOptions = getBaseFileName(configFileName) === "jsconfig.json"
? { enableAutoDiscovery: true, include: [], exclude: [] }
: { enableAutoDiscovery: false, include: [], exclude: [] };
const options: TypingOptions = { enableAutoDiscovery: getBaseFileName(configFileName) === "jsconfig.json", include: [], exclude: [] };
convertOptionsFromJson(typingOptionDeclarations, jsonOptions, basePath, options, Diagnostics.Unknown_typing_option_0, errors);
return options;
}
Expand Down Expand Up @@ -1263,12 +1261,13 @@ namespace ts {
/**
* Gets directories in a set of include patterns that should be watched for changes.
*/
function getWildcardDirectories(include: string[], exclude: string[], path: string, useCaseSensitiveFileNames: boolean) {
function getWildcardDirectories(include: string[], exclude: string[], path: string, useCaseSensitiveFileNames: boolean): Map<WatchDirectoryFlags> {
// 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
// /a/b - Watch /a/b recursively to catch changes to anything in any recursive subfoler
//
// We watch a directory without recursion if it contains a wildcard in the file segment of
// the pattern:
Expand All @@ -1281,15 +1280,14 @@ namespace ts {
if (include !== undefined) {
const recursiveKeys: string[] = [];
for (const file of include) {
const name = normalizePath(combinePaths(path, file));
if (excludeRegex && excludeRegex.test(name)) {
const spec = normalizePath(combinePaths(path, file));
if (excludeRegex && excludeRegex.test(spec)) {
continue;
}

const match = wildcardDirectoryPattern.exec(name);
const match = getWildcardDirectoryFromSpec(spec, useCaseSensitiveFileNames);
if (match) {
const key = useCaseSensitiveFileNames ? match[0] : match[0].toLowerCase();
const flags = watchRecursivePattern.test(name) ? WatchDirectoryFlags.Recursive : WatchDirectoryFlags.None;
const { key, flags } = match;
const existingFlags = wildcardDirectories[key];
if (existingFlags === undefined || existingFlags < flags) {
wildcardDirectories[key] = flags;
Expand All @@ -1313,6 +1311,20 @@ namespace ts {
return wildcardDirectories;
}

function getWildcardDirectoryFromSpec(spec: string, useCaseSensitiveFileNames: boolean): { key: string, flags: WatchDirectoryFlags } | undefined {
const match = wildcardDirectoryPattern.exec(spec);
if (match) {
return {
key: useCaseSensitiveFileNames ? match[0] : match[0].toLowerCase(),
flags: watchRecursivePattern.test(spec) ? WatchDirectoryFlags.Recursive : WatchDirectoryFlags.None
};
}
if (isImplicitGlob(spec)) {
return { key: spec, flags: WatchDirectoryFlags.Recursive };
}
return undefined;
}

/**
* Determines whether a literal or wildcard file has already been included that has a higher
* extension priority.
Expand Down
171 changes: 100 additions & 71 deletions src/compiler/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1231,7 +1231,7 @@ namespace ts {
*/
export function getDirectoryPath(path: Path): Path;
export function getDirectoryPath(path: string): string;
export function getDirectoryPath(path: string): any {
export function getDirectoryPath(path: string): string {
return path.substr(0, Math.max(getRootLength(path), path.lastIndexOf(directorySeparator)));
}

Expand Down Expand Up @@ -1491,6 +1491,10 @@ namespace ts {
return expectedPos >= 0 && str.indexOf(suffix, expectedPos) === expectedPos;
}

export function hasExtension(fileName: string): boolean {
return getBaseFileName(fileName).indexOf(".") >= 0;
}

export function fileExtensionIs(path: string, extension: string): boolean {
return path.length > extension.length && endsWith(path, extension);
}
Expand Down Expand Up @@ -1525,7 +1529,7 @@ namespace ts {
return undefined;
}

const replaceWildcardCharacter = usage === "files" ? replaceWildCardCharacterFiles : replaceWildCardCharacterOther;
const replaceWildcardCharacter = usage === "files" ? replaceWildCardCharacterFiles : replaceWildCardCharacterOther;
const singleAsteriskRegexFragment = usage === "files" ? singleAsteriskRegexFragmentFiles : singleAsteriskRegexFragmentOther;

/**
Expand All @@ -1536,81 +1540,105 @@ namespace ts {

let pattern = "";
let hasWrittenSubpattern = false;
spec: for (const spec of specs) {
for (const spec of specs) {
if (!spec) {
continue;
}

let subpattern = "";
let hasRecursiveDirectoryWildcard = false;
let hasWrittenComponent = false;
const components = getNormalizedPathComponents(spec, basePath);
if (usage !== "exclude" && components[components.length - 1] === "**") {
continue spec;
const subPattern = getSubPatternFromSpec(spec, basePath, usage, singleAsteriskRegexFragment, doubleAsteriskRegexFragment, replaceWildcardCharacter);
if (subPattern === undefined) {
continue;
}

// getNormalizedPathComponents includes the separator for the root component.
// We need to remove to create our regex correctly.
components[0] = removeTrailingDirectorySeparator(components[0]);
if (hasWrittenSubpattern) {
pattern += "|";
}

let optionalCount = 0;
for (let component of components) {
if (component === "**") {
if (hasRecursiveDirectoryWildcard) {
continue spec;
}
pattern += "(" + subPattern + ")";
hasWrittenSubpattern = true;
}

subpattern += doubleAsteriskRegexFragment;
hasRecursiveDirectoryWildcard = true;
hasWrittenComponent = true;
}
else {
if (usage === "directories") {
subpattern += "(";
optionalCount++;
}
if (!pattern) {
return undefined;
}

if (hasWrittenComponent) {
subpattern += directorySeparator;
}
// If excluding, match "foo/bar/baz...", but if including, only allow "foo".
const terminator = usage === "exclude" ? "($|/)" : "$";
return `^(${pattern})${terminator}`;
}

if (usage !== "exclude") {
// The * and ? wildcards should not match directories or files that start with . if they
// appear first in a component. Dotted directories and files can be included explicitly
// like so: **/.*/.*
if (component.charCodeAt(0) === CharacterCodes.asterisk) {
subpattern += "([^./]" + singleAsteriskRegexFragment + ")?";
component = component.substr(1);
}
else if (component.charCodeAt(0) === CharacterCodes.question) {
subpattern += "[^./]";
component = component.substr(1);
}
}
/**
* An "includes" path "foo" is implicitly a glob "foo/** /*" (without the space) if its last component has no extension,
* and does not contain any glob characters itself.
*/
export function isImplicitGlob(lastPathComponent: string): boolean {
return !/[.*?]/.test(lastPathComponent);
}

function getSubPatternFromSpec(spec: string, basePath: string, usage: "files" | "directories" | "exclude", singleAsteriskRegexFragment: string, doubleAsteriskRegexFragment: string, replaceWildcardCharacter: (match: string) => string): string | undefined {
let subpattern = "";
let hasRecursiveDirectoryWildcard = false;
let hasWrittenComponent = false;
const components = getNormalizedPathComponents(spec, basePath);
const lastComponent = lastOrUndefined(components);
if (usage !== "exclude" && lastComponent === "**") {
return undefined;
}

// getNormalizedPathComponents includes the separator for the root component.
// We need to remove to create our regex correctly.
components[0] = removeTrailingDirectorySeparator(components[0]);

subpattern += component.replace(reservedCharacterPattern, replaceWildcardCharacter);
hasWrittenComponent = true;
if (isImplicitGlob(lastComponent)) {
components.push("**", "*");
}

let optionalCount = 0;
for (let component of components) {
if (component === "**") {
if (hasRecursiveDirectoryWildcard) {
return undefined;
}
}

while (optionalCount > 0) {
subpattern += ")?";
optionalCount--;
subpattern += doubleAsteriskRegexFragment;
hasRecursiveDirectoryWildcard = true;
}
else {
if (usage === "directories") {
subpattern += "(";
optionalCount++;
}

if (hasWrittenSubpattern) {
pattern += "|";
if (hasWrittenComponent) {
subpattern += directorySeparator;
}

if (usage !== "exclude") {
// The * and ? wildcards should not match directories or files that start with . if they
// appear first in a component. Dotted directories and files can be included explicitly
// like so: **/.*/.*
if (component.charCodeAt(0) === CharacterCodes.asterisk) {
subpattern += "([^./]" + singleAsteriskRegexFragment + ")?";
component = component.substr(1);
}
else if (component.charCodeAt(0) === CharacterCodes.question) {
subpattern += "[^./]";
component = component.substr(1);
}
}

subpattern += component.replace(reservedCharacterPattern, replaceWildcardCharacter);
}

pattern += "(" + subpattern + ")";
hasWrittenSubpattern = true;
hasWrittenComponent = true;
}

if (!pattern) {
return undefined;
while (optionalCount > 0) {
subpattern += ")?";
optionalCount--;
}

return "^(" + pattern + (usage === "exclude" ? ")($|/)" : ")$");
return subpattern;
}

function replaceWildCardCharacterFiles(match: string) {
Expand Down Expand Up @@ -1697,43 +1725,44 @@ namespace ts {
function getBasePaths(path: string, includes: string[], useCaseSensitiveFileNames: boolean) {
// Storage for our results in the form of literal paths (e.g. the paths as written by the user).
const basePaths: string[] = [path];

if (includes) {
// Storage for literal base paths amongst the include patterns.
const includeBasePaths: string[] = [];
for (const include of includes) {
// We also need to check the relative paths by converting them to absolute and normalizing
// in case they escape the base path (e.g "..\somedirectory")
const absolute: string = isRootedDiskPath(include) ? include : normalizePath(combinePaths(path, include));

const wildcardOffset = indexOfAnyCharCode(absolute, wildcardCharCodes);
const includeBasePath = wildcardOffset < 0
? removeTrailingDirectorySeparator(getDirectoryPath(absolute))
: absolute.substring(0, absolute.lastIndexOf(directorySeparator, wildcardOffset));

// Append the literal and canonical candidate base paths.
includeBasePaths.push(includeBasePath);
includeBasePaths.push(getIncludeBasePath(absolute));
}

// Sort the offsets array using either the literal or canonical path representations.
includeBasePaths.sort(useCaseSensitiveFileNames ? compareStrings : compareStringsCaseInsensitive);

// Iterate over each include base path and include unique base paths that are not a
// subpath of an existing base path
include: for (let i = 0; i < includeBasePaths.length; i++) {
const includeBasePath = includeBasePaths[i];
for (let j = 0; j < basePaths.length; j++) {
if (containsPath(basePaths[j], includeBasePath, path, !useCaseSensitiveFileNames)) {
continue include;
}
for (const includeBasePath of includeBasePaths) {
if (ts.every(basePaths, basePath => !containsPath(basePath, includeBasePath, path, !useCaseSensitiveFileNames))) {
basePaths.push(includeBasePath);
}

basePaths.push(includeBasePath);
}
}

return basePaths;
}

function getIncludeBasePath(absolute: string): string {
const wildcardOffset = indexOfAnyCharCode(absolute, wildcardCharCodes);
if (wildcardOffset < 0) {
// No "*" or "?" in the path
return !hasExtension(absolute)
? absolute
: removeTrailingDirectorySeparator(getDirectoryPath(absolute));
}
return absolute.substring(0, absolute.lastIndexOf(directorySeparator, wildcardOffset));
}

export function ensureScriptKind(fileName: string, scriptKind?: ScriptKind): ScriptKind {
// Using scriptKind as a condition handles both:
// - 'scriptKind' is unspecified and thus it is `undefined`
Expand Down
4 changes: 0 additions & 4 deletions src/compiler/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -971,10 +971,6 @@ namespace ts {
return sortAndDeduplicateDiagnostics(allDiagnostics);
}

function hasExtension(fileName: string): boolean {
return getBaseFileName(fileName).indexOf(".") >= 0;
}

function processRootFile(fileName: string, isDefaultLib: boolean) {
processSourceFile(normalizePath(fileName), isDefaultLib);
}
Expand Down
1 change: 1 addition & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3092,6 +3092,7 @@ namespace ts {
Pretty,
}

/** Either a parsed command line or a parsed tsconfig.json */
export interface ParsedCommandLine {
options: CompilerOptions;
typingOptions?: TypingOptions;
Expand Down
Loading