Skip to content

Commit 2f77a54

Browse files
author
Andy Hanson
committed
Implicitly consider an extensionless file in "includes" to be a recursive directory glob
1 parent fe32282 commit 2f77a54

4 files changed

Lines changed: 197 additions & 233 deletions

File tree

src/compiler/commandLineParser.ts

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -984,9 +984,7 @@ namespace ts {
984984
function convertTypingOptionsFromJsonWorker(jsonOptions: any,
985985
basePath: string, errors: Diagnostic[], configFileName?: string): TypingOptions {
986986

987-
const options: TypingOptions = getBaseFileName(configFileName) === "jsconfig.json"
988-
? { enableAutoDiscovery: true, include: [], exclude: [] }
989-
: { enableAutoDiscovery: false, include: [], exclude: [] };
987+
const options: TypingOptions = { enableAutoDiscovery: getBaseFileName(configFileName) === "jsconfig.json", include: [], exclude: [] };
990988
convertOptionsFromJson(typingOptionDeclarations, jsonOptions, basePath, options, Diagnostics.Unknown_typing_option_0, errors);
991989
return options;
992990
}
@@ -1239,12 +1237,13 @@ namespace ts {
12391237
/**
12401238
* Gets directories in a set of include patterns that should be watched for changes.
12411239
*/
1242-
function getWildcardDirectories(include: string[], exclude: string[], path: string, useCaseSensitiveFileNames: boolean) {
1240+
function getWildcardDirectories(include: string[], exclude: string[], path: string, useCaseSensitiveFileNames: boolean): Map<WatchDirectoryFlags> {
12431241
// We watch a directory recursively if it contains a wildcard anywhere in a directory segment
12441242
// of the pattern:
12451243
//
12461244
// /a/b/**/d - Watch /a/b recursively to catch changes to any d in any subfolder recursively
12471245
// /a/b/*/d - Watch /a/b recursively to catch any d in any immediate subfolder, even if a new subfolder is added
1246+
// /a/b - Watch /a/b recursively to catch changes to anything in any recursive subfoler
12481247
//
12491248
// We watch a directory without recursion if it contains a wildcard in the file segment of
12501249
// the pattern:
@@ -1257,15 +1256,14 @@ namespace ts {
12571256
if (include !== undefined) {
12581257
const recursiveKeys: string[] = [];
12591258
for (const file of include) {
1260-
const name = normalizePath(combinePaths(path, file));
1261-
if (excludeRegex && excludeRegex.test(name)) {
1259+
const spec = normalizePath(combinePaths(path, file));
1260+
if (excludeRegex && excludeRegex.test(spec)) {
12621261
continue;
12631262
}
12641263

1265-
const match = wildcardDirectoryPattern.exec(name);
1264+
const match = getWildcardDirectoryFromSpec(spec, useCaseSensitiveFileNames);
12661265
if (match) {
1267-
const key = useCaseSensitiveFileNames ? match[0] : match[0].toLowerCase();
1268-
const flags = watchRecursivePattern.test(name) ? WatchDirectoryFlags.Recursive : WatchDirectoryFlags.None;
1266+
const { key, flags } = match;
12691267
const existingFlags = wildcardDirectories[key];
12701268
if (existingFlags === undefined || existingFlags < flags) {
12711269
wildcardDirectories[key] = flags;
@@ -1289,6 +1287,18 @@ namespace ts {
12891287
return wildcardDirectories;
12901288
}
12911289

1290+
function getWildcardDirectoryFromSpec(spec: string, useCaseSensitiveFileNames: boolean): { key: string, flags: WatchDirectoryFlags } | undefined {
1291+
const match = wildcardDirectoryPattern.exec(spec);
1292+
return match
1293+
? {
1294+
key: useCaseSensitiveFileNames ? match[0] : match[0].toLowerCase(),
1295+
flags: watchRecursivePattern.test(spec) ? WatchDirectoryFlags.Recursive : WatchDirectoryFlags.None
1296+
}
1297+
: isImplicitGlob(spec)
1298+
? { key: spec, flags: WatchDirectoryFlags.Recursive }
1299+
: undefined;
1300+
}
1301+
12921302
/**
12931303
* Determines whether a literal or wildcard file has already been included that has a higher
12941304
* extension priority.

src/compiler/core.ts

Lines changed: 102 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1177,7 +1177,7 @@ namespace ts {
11771177

11781178
/**
11791179
* Returns the path except for its basename. Eg:
1180-
*
1180+
*
11811181
* /path/to/file.ext -> /path/to
11821182
*/
11831183
export function getDirectoryPath(path: Path): Path;
@@ -1186,6 +1186,12 @@ namespace ts {
11861186
return path.substr(0, Math.max(getRootLength(path), path.lastIndexOf(directorySeparator)));
11871187
}
11881188

1189+
function getBasename(path: Path): Path;
1190+
function getBasename(path: string): string;
1191+
function getBasename(path: string): any {
1192+
return path.substr(Math.max(getRootLength(path), path.lastIndexOf(directorySeparator)));
1193+
}
1194+
11891195
export function isUrl(path: string) {
11901196
return path && !isRootedDiskPath(path) && path.indexOf("://") !== -1;
11911197
}
@@ -1476,7 +1482,7 @@ namespace ts {
14761482
return undefined;
14771483
}
14781484

1479-
const replaceWildcardCharacter = usage === "files" ? replaceWildCardCharacterFiles : replaceWildCardCharacterOther;
1485+
const replaceWildcardCharacter = usage === "files" ? replaceWildCardCharacterFiles : replaceWildCardCharacterOther;
14801486
const singleAsteriskRegexFragment = usage === "files" ? singleAsteriskRegexFragmentFiles : singleAsteriskRegexFragmentOther;
14811487

14821488
/**
@@ -1487,81 +1493,103 @@ namespace ts {
14871493

14881494
let pattern = "";
14891495
let hasWrittenSubpattern = false;
1490-
spec: for (const spec of specs) {
1496+
for (const spec of specs) {
14911497
if (!spec) {
14921498
continue;
14931499
}
14941500

1495-
let subpattern = "";
1496-
let hasRecursiveDirectoryWildcard = false;
1497-
let hasWrittenComponent = false;
1498-
const components = getNormalizedPathComponents(spec, basePath);
1499-
if (usage !== "exclude" && components[components.length - 1] === "**") {
1500-
continue spec;
1501+
const subPattern = getSubPatternFromSpec(spec, basePath, usage, singleAsteriskRegexFragment, doubleAsteriskRegexFragment, replaceWildcardCharacter);
1502+
if (subPattern === undefined) {
1503+
continue;
15011504
}
15021505

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

1507-
let optionalCount = 0;
1508-
for (let component of components) {
1509-
if (component === "**") {
1510-
if (hasRecursiveDirectoryWildcard) {
1511-
continue spec;
1512-
}
1510+
pattern += "(" + subPattern + ")";
1511+
hasWrittenSubpattern = true;
1512+
}
15131513

1514-
subpattern += doubleAsteriskRegexFragment;
1515-
hasRecursiveDirectoryWildcard = true;
1516-
hasWrittenComponent = true;
1517-
}
1518-
else {
1519-
if (usage === "directories") {
1520-
subpattern += "(";
1521-
optionalCount++;
1522-
}
1514+
if (!pattern) {
1515+
return undefined;
1516+
}
15231517

1524-
if (hasWrittenComponent) {
1525-
subpattern += directorySeparator;
1526-
}
1518+
return "^(" + pattern + (usage === "exclude" ? ")($|/)" : ")$");
1519+
}
15271520

1528-
if (usage !== "exclude") {
1529-
// The * and ? wildcards should not match directories or files that start with . if they
1530-
// appear first in a component. Dotted directories and files can be included explicitly
1531-
// like so: **/.*/.*
1532-
if (component.charCodeAt(0) === CharacterCodes.asterisk) {
1533-
subpattern += "([^./]" + singleAsteriskRegexFragment + ")?";
1534-
component = component.substr(1);
1535-
}
1536-
else if (component.charCodeAt(0) === CharacterCodes.question) {
1537-
subpattern += "[^./]";
1538-
component = component.substr(1);
1539-
}
1540-
}
1521+
/**
1522+
* An "includes" path "foo" is implicitly a glob "foo\**\*" (replace \ with /) if its last component has no extension,
1523+
* and does not contain any glob characters itself.
1524+
*/
1525+
export function isImplicitGlob(lastPathComponent: string): boolean {
1526+
return !/[.*?]/.test(lastPathComponent);
1527+
}
1528+
1529+
function getSubPatternFromSpec(spec: string, basePath: string, usage: "files" | "directories" | "exclude", singleAsteriskRegexFragment: string, doubleAsteriskRegexFragment: string, replaceWildcardCharacter: (match: string) => string): string | undefined {
1530+
let subpattern = "";
1531+
let hasRecursiveDirectoryWildcard = false;
1532+
let hasWrittenComponent = false;
1533+
const components = getNormalizedPathComponents(spec, basePath);
1534+
const lastComponent = lastOrUndefined(components);
1535+
if (usage !== "exclude" && lastComponent === "**") {
1536+
return undefined;
1537+
}
1538+
1539+
// getNormalizedPathComponents includes the separator for the root component.
1540+
// We need to remove to create our regex correctly.
1541+
components[0] = removeTrailingDirectorySeparator(components[0]);
15411542

1542-
subpattern += component.replace(reservedCharacterPattern, replaceWildcardCharacter);
1543-
hasWrittenComponent = true;
1543+
if (isImplicitGlob(lastComponent)) {
1544+
components.push("**", "*");
1545+
}
1546+
1547+
let optionalCount = 0;
1548+
for (let component of components) {
1549+
if (component === "**") {
1550+
if (hasRecursiveDirectoryWildcard) {
1551+
return undefined;
15441552
}
1545-
}
15461553

1547-
while (optionalCount > 0) {
1548-
subpattern += ")?";
1549-
optionalCount--;
1554+
subpattern += doubleAsteriskRegexFragment;
1555+
hasRecursiveDirectoryWildcard = true;
15501556
}
1557+
else {
1558+
if (usage === "directories") {
1559+
subpattern += "(";
1560+
optionalCount++;
1561+
}
15511562

1552-
if (hasWrittenSubpattern) {
1553-
pattern += "|";
1563+
if (hasWrittenComponent) {
1564+
subpattern += directorySeparator;
1565+
}
1566+
1567+
if (usage !== "exclude") {
1568+
// The * and ? wildcards should not match directories or files that start with . if they
1569+
// appear first in a component. Dotted directories and files can be included explicitly
1570+
// like so: **/.*/.*
1571+
if (component.charCodeAt(0) === CharacterCodes.asterisk) {
1572+
subpattern += "([^./]" + singleAsteriskRegexFragment + ")?";
1573+
component = component.substr(1);
1574+
}
1575+
else if (component.charCodeAt(0) === CharacterCodes.question) {
1576+
subpattern += "[^./]";
1577+
component = component.substr(1);
1578+
}
1579+
}
1580+
1581+
subpattern += component.replace(reservedCharacterPattern, replaceWildcardCharacter);
15541582
}
15551583

1556-
pattern += "(" + subpattern + ")";
1557-
hasWrittenSubpattern = true;
1584+
hasWrittenComponent = true;
15581585
}
15591586

1560-
if (!pattern) {
1561-
return undefined;
1587+
while (optionalCount > 0) {
1588+
subpattern += ")?";
1589+
optionalCount--;
15621590
}
15631591

1564-
return "^(" + pattern + (usage === "exclude" ? ")($|/)" : ")$");
1592+
return subpattern;
15651593
}
15661594

15671595
function replaceWildCardCharacterFiles(match: string) {
@@ -1648,43 +1676,46 @@ namespace ts {
16481676
function getBasePaths(path: string, includes: string[], useCaseSensitiveFileNames: boolean) {
16491677
// Storage for our results in the form of literal paths (e.g. the paths as written by the user).
16501678
const basePaths: string[] = [path];
1679+
16511680
if (includes) {
16521681
// Storage for literal base paths amongst the include patterns.
16531682
const includeBasePaths: string[] = [];
16541683
for (const include of includes) {
16551684
// We also need to check the relative paths by converting them to absolute and normalizing
16561685
// in case they escape the base path (e.g "..\somedirectory")
16571686
const absolute: string = isRootedDiskPath(include) ? include : normalizePath(combinePaths(path, include));
1658-
1659-
const wildcardOffset = indexOfAnyCharCode(absolute, wildcardCharCodes);
1660-
const includeBasePath = wildcardOffset < 0
1661-
? removeTrailingDirectorySeparator(getDirectoryPath(absolute))
1662-
: absolute.substring(0, absolute.lastIndexOf(directorySeparator, wildcardOffset));
1663-
16641687
// Append the literal and canonical candidate base paths.
1665-
includeBasePaths.push(includeBasePath);
1688+
includeBasePaths.push(getIncludeBasePath(absolute));
16661689
}
16671690

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

16711694
// Iterate over each include base path and include unique base paths that are not a
16721695
// subpath of an existing base path
1673-
include: for (let i = 0; i < includeBasePaths.length; i++) {
1674-
const includeBasePath = includeBasePaths[i];
1675-
for (let j = 0; j < basePaths.length; j++) {
1676-
if (containsPath(basePaths[j], includeBasePath, path, !useCaseSensitiveFileNames)) {
1677-
continue include;
1678-
}
1696+
for (const includeBasePath of includeBasePaths) {
1697+
if (ts.every(basePaths, basePath => !containsPath(basePath, includeBasePath, path, !useCaseSensitiveFileNames))) {
1698+
basePaths.push(includeBasePath);
16791699
}
1680-
1681-
basePaths.push(includeBasePath);
16821700
}
16831701
}
16841702

16851703
return basePaths;
16861704
}
16871705

1706+
function getIncludeBasePath(absolute: string): string {
1707+
const wildcardOffset = indexOfAnyCharCode(absolute, wildcardCharCodes);
1708+
if (wildcardOffset < 0) {
1709+
// No "*" in the path
1710+
return isImplicitGlob(getBasename(absolute))
1711+
? absolute
1712+
: removeTrailingDirectorySeparator(getDirectoryPath(absolute));
1713+
}
1714+
else {
1715+
return absolute.substring(0, absolute.lastIndexOf(directorySeparator, wildcardOffset));
1716+
}
1717+
}
1718+
16881719
export function ensureScriptKind(fileName: string, scriptKind?: ScriptKind): ScriptKind {
16891720
// Using scriptKind as a condition handles both:
16901721
// - 'scriptKind' is unspecified and thus it is `undefined`

src/compiler/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3073,6 +3073,7 @@ namespace ts {
30733073
Pretty,
30743074
}
30753075

3076+
/** Either a parsed command line or a parsed tsconfig.json */
30763077
export interface ParsedCommandLine {
30773078
options: CompilerOptions;
30783079
typingOptions?: TypingOptions;

0 commit comments

Comments
 (0)