Skip to content
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
20 changes: 19 additions & 1 deletion src/compiler/commandLineParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -891,6 +891,21 @@ namespace ts {
*/
const invalidMultipleRecursionPatterns = /(^|\/)\*\*\/(.*\/)?\*\*($|\/)/;

/**
* Tests for a path where .. appears after a recursive directory wildcard.
* Matches **\..\*, **\a\..\*, and **\.., but not ..\**\*
*
* NOTE: used \ in place of / above to avoid issues with multiline comments.
*
* Breakdown:
* (^|\/) # matches either the beginning of the string or a directory separator.
* \*\*\/ # matches a recursive directory wildcard "**" followed by a directory separator.
* (.*\/)? # optionally matches any number of characters followed by a directory separator.
* \.\. # matches a parent directory path component ".."
* ($|\/) # matches either the end of the string or a directory separator.
*/
const invalidDotDotAfterRecursiveWildcardPattern = /(^|\/)\*\*\/(.*\/)?\.\.($|\/)/;

/**
* Tests for a path containing a wildcard character in a directory component of the path.
* Matches \*\, \?\, and \a*b\, but not \a\ or \a\*.
Expand Down Expand Up @@ -1023,6 +1038,9 @@ namespace ts {
else if (invalidMultipleRecursionPatterns.test(spec)) {
errors.push(createCompilerDiagnostic(Diagnostics.File_specification_cannot_contain_multiple_recursive_directory_wildcards_Asterisk_Asterisk_Colon_0, spec));
}
else if (invalidDotDotAfterRecursiveWildcardPattern.test(spec)) {
errors.push(createCompilerDiagnostic(Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, spec));
}
else {
validSpecs.push(spec);
}
Expand Down Expand Up @@ -1052,7 +1070,7 @@ namespace ts {
if (include !== undefined) {
const recursiveKeys: string[] = [];
for (const file of include) {
const name = combinePaths(path, file);
const name = normalizePath(combinePaths(path, file));
if (excludeRegex && excludeRegex.test(name)) {
continue;
}
Expand Down
20 changes: 11 additions & 9 deletions src/compiler/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1072,15 +1072,17 @@ namespace ts {
// Storage for literal base paths amongst the include patterns.
const includeBasePaths: string[] = [];
for (const include of includes) {
if (isRootedDiskPath(include)) {
const wildcardOffset = indexOfAnyCharCode(include, wildcardCharCodes);
const includeBasePath = wildcardOffset < 0
? removeTrailingDirectorySeparator(getDirectoryPath(include))
: include.substring(0, include.lastIndexOf(directorySeparator, wildcardOffset));

// Append the literal and canonical candidate base paths.
includeBasePaths.push(includeBasePath);
}
// 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);
}

// Sort the offsets array using either the literal or canonical path representations.
Expand Down
4 changes: 4 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -2332,6 +2332,10 @@
"category": "Error",
"code": 5064
},
"File specification cannot contain a parent directory ('..') that appears after a recursive directory wildcard ('**'): '{0}'.": {
"category": "Error",
"code": 5065
},
"Concatenate and emit output to single file.": {
"category": "Message",
"code": 6001
Expand Down
198 changes: 196 additions & 2 deletions tests/cases/unittests/matchFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ namespace ts {
"c:/dev/x/y/b.ts",
"c:/dev/js/a.js",
"c:/dev/js/b.js",
"c:/ext/ext.ts"
"c:/ext/ext.ts",
"c:/ext/b/a..b.ts"
]);

const caseSensitiveBasePath = "/dev/";
Expand Down Expand Up @@ -740,7 +741,7 @@ namespace ts {
"c:/dev/a.ts",
"c:/dev/b.ts",
"c:/dev/c.d.ts",
"c:/ext/ext.ts",
"c:/ext/ext.ts"
],
wildcardDirectories: {
"c:/dev": ts.WatchDirectoryFlags.None,
Expand All @@ -752,6 +753,97 @@ namespace ts {
assert.deepEqual(actual.wildcardDirectories, expected.wildcardDirectories);
assert.deepEqual(actual.errors, expected.errors);
});
it("include paths outside of the project using relative paths", () => {
const json = {
include: [
"*",
"../ext/*"
],
exclude: [
"**"
]
};
const expected: ts.ParsedCommandLine = {
options: {},
errors: [],
fileNames: [
"c:/ext/ext.ts"
],
wildcardDirectories: {
"c:/ext": ts.WatchDirectoryFlags.None
}
};
const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath);
assert.deepEqual(actual.fileNames, expected.fileNames);
assert.deepEqual(actual.wildcardDirectories, expected.wildcardDirectories);
assert.deepEqual(actual.errors, expected.errors);
});
it("exclude paths outside of the project using relative paths", () => {
const json = {
include: [
"c:/**/*"
],
exclude: [
"../**"
]
};
const expected: ts.ParsedCommandLine = {
options: {},
errors: [],
fileNames: [],
wildcardDirectories: {}
};
const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath);
assert.deepEqual(actual.fileNames, expected.fileNames);
assert.deepEqual(actual.wildcardDirectories, expected.wildcardDirectories);
assert.deepEqual(actual.errors, expected.errors);
});
it("include files with .. in their name", () => {
const json = {
include: [
"c:/ext/b/a..b.ts"
],
exclude: [
"**"
]
};
const expected: ts.ParsedCommandLine = {
options: {},
errors: [],
fileNames: [
"c:/ext/b/a..b.ts"
],
wildcardDirectories: {}
};
const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath);
assert.deepEqual(actual.fileNames, expected.fileNames);
assert.deepEqual(actual.wildcardDirectories, expected.wildcardDirectories);
assert.deepEqual(actual.errors, expected.errors);
});
it("exclude files with .. in their name", () => {
const json = {
include: [
"c:/ext/**/*"
],
exclude: [
"c:/ext/b/a..b.ts"
]
};
const expected: ts.ParsedCommandLine = {
options: {},
errors: [],
fileNames: [
"c:/ext/ext.ts",
],
wildcardDirectories: {
"c:/ext": ts.WatchDirectoryFlags.Recursive
}
};
const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath);
assert.deepEqual(actual.fileNames, expected.fileNames);
assert.deepEqual(actual.wildcardDirectories, expected.wildcardDirectories);
assert.deepEqual(actual.errors, expected.errors);
});
it("with jsx=none, allowJs=false", () => {
const json = {
compilerOptions: {
Expand Down Expand Up @@ -951,6 +1043,108 @@ namespace ts {
assert.deepEqual(actual.errors, expected.errors);
});
});

describe("with parent directory symbols after a recursive directory pattern", () => {
it("in includes immediately after", () => {
const json = {
include: [
"**/../*"
]
};
const expected: ts.ParsedCommandLine = {
options: {},
errors: [
ts.createCompilerDiagnostic(ts.Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, "**/../*")
],
fileNames: [],
wildcardDirectories: {}
};
const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath);
assert.deepEqual(actual.fileNames, expected.fileNames);
assert.deepEqual(actual.wildcardDirectories, expected.wildcardDirectories);
assert.deepEqual(actual.errors, expected.errors);
});

it("in includes after a subdirectory", () => {
const json = {
include: [
"**/y/../*"
]
};
const expected: ts.ParsedCommandLine = {
options: {},
errors: [
ts.createCompilerDiagnostic(ts.Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, "**/y/../*")
],
fileNames: [],
wildcardDirectories: {}
};
const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath);
assert.deepEqual(actual.fileNames, expected.fileNames);
assert.deepEqual(actual.wildcardDirectories, expected.wildcardDirectories);
assert.deepEqual(actual.errors, expected.errors);
});

it("in excludes immediately after", () => {
const json = {
include: [
"**/a.ts"
],
exclude: [
"**/.."
]
};
const expected: ts.ParsedCommandLine = {
options: {},
errors: [
ts.createCompilerDiagnostic(ts.Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, "**/..")
],
fileNames: [
"c:/dev/a.ts",
"c:/dev/x/a.ts",
"c:/dev/x/y/a.ts",
"c:/dev/z/a.ts"
],
wildcardDirectories: {
"c:/dev": ts.WatchDirectoryFlags.Recursive
}
};
const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath);
assert.deepEqual(actual.fileNames, expected.fileNames);
assert.deepEqual(actual.wildcardDirectories, expected.wildcardDirectories);
assert.deepEqual(actual.errors, expected.errors);
});

it("in excludes after a subdirectory", () => {
const json = {
include: [
"**/a.ts"
],
exclude: [
"**/y/.."
]
};
const expected: ts.ParsedCommandLine = {
options: {},
errors: [
ts.createCompilerDiagnostic(ts.Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, "**/y/..")
],
fileNames: [
"c:/dev/a.ts",
"c:/dev/x/a.ts",
"c:/dev/x/y/a.ts",
"c:/dev/z/a.ts"
],
wildcardDirectories: {
"c:/dev": ts.WatchDirectoryFlags.Recursive
}
};
const actual = ts.parseJsonConfigFileContent(json, caseInsensitiveHost, caseInsensitiveBasePath);
assert.deepEqual(actual.fileNames, expected.fileNames);
assert.deepEqual(actual.wildcardDirectories, expected.wildcardDirectories);
assert.deepEqual(actual.errors, expected.errors);
});
});
});
});
}