-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1177,7 +1177,7 @@ namespace ts { | |
|
||
/** | ||
* Returns the path except for its basename. Eg: | ||
* | ||
* | ||
* /path/to/file.ext -> /path/to | ||
*/ | ||
export function getDirectoryPath(path: Path): Path; | ||
|
@@ -1186,6 +1186,12 @@ namespace ts { | |
return path.substr(0, Math.max(getRootLength(path), path.lastIndexOf(directorySeparator))); | ||
} | ||
|
||
function getBasename(path: Path): Path; | ||
function getBasename(path: string): string; | ||
function getBasename(path: string): any { | ||
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. You shouldn't need to use |
||
return path.substr(Math.max(getRootLength(path), path.lastIndexOf(directorySeparator))); | ||
} | ||
|
||
export function isUrl(path: string) { | ||
return path && !isRootedDiskPath(path) && path.indexOf("://") !== -1; | ||
} | ||
|
@@ -1476,7 +1482,7 @@ namespace ts { | |
return undefined; | ||
} | ||
|
||
const replaceWildcardCharacter = usage === "files" ? replaceWildCardCharacterFiles : replaceWildCardCharacterOther; | ||
const replaceWildcardCharacter = usage === "files" ? replaceWildCardCharacterFiles : replaceWildCardCharacterOther; | ||
const singleAsteriskRegexFragment = usage === "files" ? singleAsteriskRegexFragmentFiles : singleAsteriskRegexFragmentOther; | ||
|
||
/** | ||
|
@@ -1487,81 +1493,103 @@ 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; | ||
} | ||
return "^(" + pattern + (usage === "exclude" ? ")($|/)" : ")$"); | ||
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. Separate the last part into a variable and comment what the purpose is. |
||
} | ||
|
||
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\**\*" (replace \ with /) if its last component has no extension, | ||
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. what do you mean by "replace \ with /"? |
||
* 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) { | ||
|
@@ -1648,43 +1676,46 @@ 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 "*" in the path | ||
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. Isn't Also the comment should probably read |
||
return isImplicitGlob(getBasename(absolute)) | ||
? absolute | ||
: removeTrailingDirectorySeparator(getDirectoryPath(absolute)); | ||
} | ||
else { | ||
return absolute.substring(0, absolute.lastIndexOf(directorySeparator, wildcardOffset)); | ||
} | ||
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. Same nit as above about |
||
} | ||
|
||
export function ensureScriptKind(fileName: string, scriptKind?: ScriptKind): ScriptKind { | ||
// Using scriptKind as a condition handles both: | ||
// - 'scriptKind' is unspecified and thus it is `undefined` | ||
|
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.
can you turn this into a series of
if
s? this is pretty unreadable to me.