Skip to content

Commit 9eba8d7

Browse files
committed
Handle relative paths in tsconfig exclude and include globs
1 parent a591d40 commit 9eba8d7

File tree

4 files changed

+230
-12
lines changed

4 files changed

+230
-12
lines changed

src/compiler/commandLineParser.ts

+19-1
Original file line numberDiff line numberDiff line change
@@ -891,6 +891,21 @@ namespace ts {
891891
*/
892892
const invalidMultipleRecursionPatterns = /(^|\/)\*\*\/(.*\/)?\*\*($|\/)/;
893893

894+
/**
895+
* Tests for a path where .. appears after a recursive directory wildcard.
896+
* Matches **\..\*, **\a\..\*, and **\.., but not ..\**\*
897+
*
898+
* NOTE: used \ in place of / above to avoid issues with multiline comments.
899+
*
900+
* Breakdown:
901+
* (^|\/) # matches either the beginning of the string or a directory separator.
902+
* \*\*\/ # matches a recursive directory wildcard "**" followed by a directory separator.
903+
* (.*\/)? # optionally matches any number of characters followed by a directory separator.
904+
* \.\. # matches a parent directory path component ".."
905+
* ($|\/) # matches either the end of the string or a directory separator.
906+
*/
907+
const invalidDotDotAfterRecursiveWildcardPattern = /(^|\/)\*\*\/(.*\/)?\.\.($|\/)/;
908+
894909
/**
895910
* Tests for a path containing a wildcard character in a directory component of the path.
896911
* Matches \*\, \?\, and \a*b\, but not \a\ or \a\*.
@@ -1023,6 +1038,9 @@ namespace ts {
10231038
else if (invalidMultipleRecursionPatterns.test(spec)) {
10241039
errors.push(createCompilerDiagnostic(Diagnostics.File_specification_cannot_contain_multiple_recursive_directory_wildcards_Asterisk_Asterisk_Colon_0, spec));
10251040
}
1041+
else if (invalidDotDotAfterRecursiveWildcardPattern.test(spec)) {
1042+
errors.push(createCompilerDiagnostic(Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, spec));
1043+
}
10261044
else {
10271045
validSpecs.push(spec);
10281046
}
@@ -1052,7 +1070,7 @@ namespace ts {
10521070
if (include !== undefined) {
10531071
const recursiveKeys: string[] = [];
10541072
for (const file of include) {
1055-
const name = combinePaths(path, file);
1073+
const name = normalizePath(combinePaths(path, file));
10561074
if (excludeRegex && excludeRegex.test(name)) {
10571075
continue;
10581076
}

src/compiler/core.ts

+11-9
Original file line numberDiff line numberDiff line change
@@ -1072,15 +1072,17 @@ namespace ts {
10721072
// Storage for literal base paths amongst the include patterns.
10731073
const includeBasePaths: string[] = [];
10741074
for (const include of includes) {
1075-
if (isRootedDiskPath(include)) {
1076-
const wildcardOffset = indexOfAnyCharCode(include, wildcardCharCodes);
1077-
const includeBasePath = wildcardOffset < 0
1078-
? removeTrailingDirectorySeparator(getDirectoryPath(include))
1079-
: include.substring(0, include.lastIndexOf(directorySeparator, wildcardOffset));
1080-
1081-
// Append the literal and canonical candidate base paths.
1082-
includeBasePaths.push(includeBasePath);
1083-
}
1075+
// We also need to check the relative paths by converting them to absolute and normalizing
1076+
// in case they escape the base path (e.g "..\somedirectory")
1077+
const absolute: string = isRootedDiskPath(include) ? include : normalizePath(combinePaths(path, include));
1078+
1079+
const wildcardOffset = indexOfAnyCharCode(absolute, wildcardCharCodes);
1080+
const includeBasePath = wildcardOffset < 0
1081+
? removeTrailingDirectorySeparator(getDirectoryPath(absolute))
1082+
: absolute.substring(0, absolute.lastIndexOf(directorySeparator, wildcardOffset));
1083+
1084+
// Append the literal and canonical candidate base paths.
1085+
includeBasePaths.push(includeBasePath);
10841086
}
10851087

10861088
// Sort the offsets array using either the literal or canonical path representations.

src/compiler/diagnosticMessages.json

+4
Original file line numberDiff line numberDiff line change
@@ -2332,6 +2332,10 @@
23322332
"category": "Error",
23332333
"code": 5064
23342334
},
2335+
"File specification cannot contain a parent directory ('..') that appears after a recursive directory wildcard ('**'): '{0}'.": {
2336+
"category": "Error",
2337+
"code": 5065
2338+
},
23352339
"Concatenate and emit output to single file.": {
23362340
"category": "Message",
23372341
"code": 6001

tests/cases/unittests/matchFiles.ts

+196-2
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ namespace ts {
2424
"c:/dev/x/y/b.ts",
2525
"c:/dev/js/a.js",
2626
"c:/dev/js/b.js",
27-
"c:/ext/ext.ts"
27+
"c:/ext/ext.ts",
28+
"c:/ext/b/a..b.ts"
2829
]);
2930

3031
const caseSensitiveBasePath = "/dev/";
@@ -740,7 +741,7 @@ namespace ts {
740741
"c:/dev/a.ts",
741742
"c:/dev/b.ts",
742743
"c:/dev/c.d.ts",
743-
"c:/ext/ext.ts",
744+
"c:/ext/ext.ts"
744745
],
745746
wildcardDirectories: {
746747
"c:/dev": ts.WatchDirectoryFlags.None,
@@ -752,6 +753,97 @@ namespace ts {
752753
assert.deepEqual(actual.wildcardDirectories, expected.wildcardDirectories);
753754
assert.deepEqual(actual.errors, expected.errors);
754755
});
756+
it("include paths outside of the project using relative paths", () => {
757+
const json = {
758+
include: [
759+
"*",
760+
"../ext/*"
761+
],
762+
exclude: [
763+
"**"
764+
]
765+
};
766+
const expected: ts.ParsedCommandLine = {
767+
options: {},
768+
errors: [],
769+
fileNames: [
770+
"c:/ext/ext.ts"
771+
],
772+
wildcardDirectories: {
773+
"c:/ext": ts.WatchDirectoryFlags.None
774+
}
775+
};
776+
const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath);
777+
assert.deepEqual(actual.fileNames, expected.fileNames);
778+
assert.deepEqual(actual.wildcardDirectories, expected.wildcardDirectories);
779+
assert.deepEqual(actual.errors, expected.errors);
780+
});
781+
it("exclude paths outside of the project using relative paths", () => {
782+
const json = {
783+
include: [
784+
"c:/**/*"
785+
],
786+
exclude: [
787+
"../**"
788+
]
789+
};
790+
const expected: ts.ParsedCommandLine = {
791+
options: {},
792+
errors: [],
793+
fileNames: [],
794+
wildcardDirectories: {}
795+
};
796+
const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath);
797+
assert.deepEqual(actual.fileNames, expected.fileNames);
798+
assert.deepEqual(actual.wildcardDirectories, expected.wildcardDirectories);
799+
assert.deepEqual(actual.errors, expected.errors);
800+
});
801+
it("include files with .. in their name", () => {
802+
const json = {
803+
include: [
804+
"c:/ext/b/a..b.ts"
805+
],
806+
exclude: [
807+
"**"
808+
]
809+
};
810+
const expected: ts.ParsedCommandLine = {
811+
options: {},
812+
errors: [],
813+
fileNames: [
814+
"c:/ext/b/a..b.ts"
815+
],
816+
wildcardDirectories: {}
817+
};
818+
const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath);
819+
assert.deepEqual(actual.fileNames, expected.fileNames);
820+
assert.deepEqual(actual.wildcardDirectories, expected.wildcardDirectories);
821+
assert.deepEqual(actual.errors, expected.errors);
822+
});
823+
it("exclude files with .. in their name", () => {
824+
const json = {
825+
include: [
826+
"c:/ext/**/*"
827+
],
828+
exclude: [
829+
"c:/ext/b/a..b.ts"
830+
]
831+
};
832+
const expected: ts.ParsedCommandLine = {
833+
options: {},
834+
errors: [],
835+
fileNames: [
836+
"c:/ext/ext.ts",
837+
],
838+
wildcardDirectories: {
839+
"c:/ext": ts.WatchDirectoryFlags.Recursive
840+
}
841+
};
842+
const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath);
843+
assert.deepEqual(actual.fileNames, expected.fileNames);
844+
assert.deepEqual(actual.wildcardDirectories, expected.wildcardDirectories);
845+
assert.deepEqual(actual.errors, expected.errors);
846+
});
755847
it("with jsx=none, allowJs=false", () => {
756848
const json = {
757849
compilerOptions: {
@@ -951,6 +1043,108 @@ namespace ts {
9511043
assert.deepEqual(actual.errors, expected.errors);
9521044
});
9531045
});
1046+
1047+
describe("with parent directory symbols after a recursive directory pattern", () => {
1048+
it("in includes immediately after", () => {
1049+
const json = {
1050+
include: [
1051+
"**/../*"
1052+
]
1053+
};
1054+
const expected: ts.ParsedCommandLine = {
1055+
options: {},
1056+
errors: [
1057+
ts.createCompilerDiagnostic(ts.Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, "**/../*")
1058+
],
1059+
fileNames: [],
1060+
wildcardDirectories: {}
1061+
};
1062+
const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath);
1063+
assert.deepEqual(actual.fileNames, expected.fileNames);
1064+
assert.deepEqual(actual.wildcardDirectories, expected.wildcardDirectories);
1065+
assert.deepEqual(actual.errors, expected.errors);
1066+
});
1067+
1068+
it("in includes after a subdirectory", () => {
1069+
const json = {
1070+
include: [
1071+
"**/y/../*"
1072+
]
1073+
};
1074+
const expected: ts.ParsedCommandLine = {
1075+
options: {},
1076+
errors: [
1077+
ts.createCompilerDiagnostic(ts.Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, "**/y/../*")
1078+
],
1079+
fileNames: [],
1080+
wildcardDirectories: {}
1081+
};
1082+
const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath);
1083+
assert.deepEqual(actual.fileNames, expected.fileNames);
1084+
assert.deepEqual(actual.wildcardDirectories, expected.wildcardDirectories);
1085+
assert.deepEqual(actual.errors, expected.errors);
1086+
});
1087+
1088+
it("in excludes immediately after", () => {
1089+
const json = {
1090+
include: [
1091+
"**/a.ts"
1092+
],
1093+
exclude: [
1094+
"**/.."
1095+
]
1096+
};
1097+
const expected: ts.ParsedCommandLine = {
1098+
options: {},
1099+
errors: [
1100+
ts.createCompilerDiagnostic(ts.Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, "**/..")
1101+
],
1102+
fileNames: [
1103+
"c:/dev/a.ts",
1104+
"c:/dev/x/a.ts",
1105+
"c:/dev/x/y/a.ts",
1106+
"c:/dev/z/a.ts"
1107+
],
1108+
wildcardDirectories: {
1109+
"c:/dev": ts.WatchDirectoryFlags.Recursive
1110+
}
1111+
};
1112+
const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath);
1113+
assert.deepEqual(actual.fileNames, expected.fileNames);
1114+
assert.deepEqual(actual.wildcardDirectories, expected.wildcardDirectories);
1115+
assert.deepEqual(actual.errors, expected.errors);
1116+
});
1117+
1118+
it("in excludes after a subdirectory", () => {
1119+
const json = {
1120+
include: [
1121+
"**/a.ts"
1122+
],
1123+
exclude: [
1124+
"**/y/.."
1125+
]
1126+
};
1127+
const expected: ts.ParsedCommandLine = {
1128+
options: {},
1129+
errors: [
1130+
ts.createCompilerDiagnostic(ts.Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, "**/y/..")
1131+
],
1132+
fileNames: [
1133+
"c:/dev/a.ts",
1134+
"c:/dev/x/a.ts",
1135+
"c:/dev/x/y/a.ts",
1136+
"c:/dev/z/a.ts"
1137+
],
1138+
wildcardDirectories: {
1139+
"c:/dev": ts.WatchDirectoryFlags.Recursive
1140+
}
1141+
};
1142+
const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath);
1143+
assert.deepEqual(actual.fileNames, expected.fileNames);
1144+
assert.deepEqual(actual.wildcardDirectories, expected.wildcardDirectories);
1145+
assert.deepEqual(actual.errors, expected.errors);
1146+
});
1147+
});
9541148
});
9551149
});
9561150
}

0 commit comments

Comments
 (0)