Fixes #12110 Trim trailing /** for file exclusions

This commit is contained in:
Christof Marti 2016-09-16 10:31:27 -07:00
parent 47393fb852
commit 9e228b20ff
3 changed files with 88 additions and 31 deletions

View file

@ -212,10 +212,15 @@ function parseRegExp(pattern: string): string {
const T1 = /^\*\*\/\*\.[\w\.-]+$/; // **/*.something
const T2 = /^\*\*\/[\w\.-]+$/; // **/something
const T3 = /^{\*\*\/[\*\.]?[\w\.-]+(,\*\*\/[\*\.]?[\w\.-]+)*}$/; // {**/*.something,**/*.else} or {**/package.json,**/project.json}
const T3_2 = /^{\*\*\/[\*\.]?[\w\.-]+(\/\*\*)?(,\*\*\/[\*\.]?[\w\.-]+(\/\*\*)?)*}$/; // Like T3, with optional trailing /**
export type ParsedPattern = (path: string, basename?: string) => boolean;
export type ParsedExpression = (path: string, basename?: string, siblingsFn?: () => string[]) => string /* the matching pattern */;
export interface IGlobOptions {
trimForExclusions?: boolean;
}
interface ParsedStringPattern {
(path: string, basename: string): string /* the matching pattern */;
basenames?: string[];
@ -239,7 +244,7 @@ const NULL = function(): string {
return null;
};
function parsePattern(pattern: string): ParsedStringPattern {
function parsePattern(pattern: string, options: IGlobOptions): ParsedStringPattern {
if (!pattern) {
return NULL;
}
@ -248,37 +253,26 @@ function parsePattern(pattern: string): ParsedStringPattern {
pattern = pattern.trim();
// Check cache
let parsedPattern = CACHE.get(pattern);
const patternKey = `${pattern}_${!!options.trimForExclusions}`;
let parsedPattern = CACHE.get(patternKey);
if (parsedPattern) {
return parsedPattern;
}
// Check for Trivias
let trimmedPattern;
if (T1.test(pattern)) { // common pattern: **/*.txt just need endsWith check
const base = pattern.substr(4); // '**/*'.length === 4
parsedPattern = function (path, basename) {
return path && strings.endsWith(path, base) ? pattern : null;
};
} else if (T2.test(pattern)) { // common pattern: **/some.txt just need basename check
const base = pattern.substr(3); // '**/'.length === 3
const slashBase = `/${base}`;
const backslashBase = `\\${base}`;
parsedPattern = function (path, basename) {
if (!path) {
return null;
}
if (basename) {
return basename === base ? pattern : null;
}
return path === base || strings.endsWith(path, slashBase) || strings.endsWith(path, backslashBase) ? pattern : null;
};
const basenames = [base];
parsedPattern.basenames = basenames;
parsedPattern.patterns = [pattern];
parsedPattern.allBasenames = basenames;
} else if (T3.test(pattern)) { // repetition of common patterns (see above) {**/*.txt,**/*.png}
parsedPattern = trivia2(pattern, pattern);
} else if (options.trimForExclusions && strings.endsWith(pattern, '/**') && T2.test(trimmedPattern = pattern.substr(0, pattern.length - 3))) { // common pattern: **/some/** for exclusions just need basename check
parsedPattern = trivia2(trimmedPattern, pattern);
} else if ((options.trimForExclusions ? T3_2 : T3).test(pattern)) { // repetition of common patterns (see above) {**/*.txt,**/*.png}
const parsedPatterns = aggregateBasenameMatches(pattern.slice(1, -1).split(',')
.map(pattern => parsePattern(pattern))
.map(pattern => parsePattern(pattern, options))
.filter(pattern => pattern !== NULL), pattern);
const n = parsedPatterns.length;
if (!n) {
@ -307,11 +301,32 @@ function parsePattern(pattern: string): ParsedStringPattern {
}
// Cache
CACHE.set(pattern, parsedPattern);
CACHE.set(patternKey, parsedPattern);
return parsedPattern;
}
// common pattern: **/some.txt just need basename check
function trivia2(pattern, originalPattern): ParsedStringPattern {
const base = pattern.substr(3); // '**/'.length === 3
const slashBase = `/${base}`;
const backslashBase = `\\${base}`;
const parsedPattern: ParsedStringPattern = function (path, basename) {
if (!path) {
return null;
}
if (basename) {
return basename === base ? originalPattern : null;
}
return path === base || strings.endsWith(path, slashBase) || strings.endsWith(path, backslashBase) ? originalPattern : null;
};
const basenames = [base];
parsedPattern.basenames = basenames;
parsedPattern.patterns = [originalPattern];
parsedPattern.allBasenames = basenames;
return parsedPattern;
}
function toRegExp(pattern: string): ParsedStringPattern {
try {
const regExp = new RegExp(`^${parseRegExp(pattern)}$`);
@ -350,16 +365,16 @@ export function match(arg1: string | IExpression, path: string, siblingsFn?: ()
* - simple brace expansion ({js,ts} => js or ts)
* - character ranges (using [...])
*/
export function parse(pattern: string): ParsedPattern;
export function parse(expression: IExpression): ParsedExpression;
export function parse(arg1: string | IExpression): any {
export function parse(pattern: string, options?: IGlobOptions): ParsedPattern;
export function parse(expression: IExpression, options?: IGlobOptions): ParsedExpression;
export function parse(arg1: string | IExpression, options: IGlobOptions = {}): any {
if (!arg1) {
return FALSE;
}
// Glob with String
if (typeof arg1 === 'string') {
const parsedPattern = parsePattern(arg1);
const parsedPattern = parsePattern(arg1, options);
if (parsedPattern === NULL) {
return FALSE;
}
@ -373,16 +388,16 @@ export function parse(arg1: string | IExpression): any {
}
// Glob with Expression
return parsedExpression(<IExpression>arg1);
return parsedExpression(<IExpression>arg1, options);
}
export function getBasenameTerms(patternOrExpression: ParsedPattern | ParsedExpression): string[] {
return (<ParsedStringPattern>patternOrExpression).allBasenames || [];
}
function parsedExpression(expression: IExpression): ParsedExpression {
function parsedExpression(expression: IExpression, options: IGlobOptions): ParsedExpression {
const parsedPatterns = aggregateBasenameMatches(Object.getOwnPropertyNames(expression)
.map(pattern => parseExpressionPattern(pattern, expression[pattern]))
.map(pattern => parseExpressionPattern(pattern, expression[pattern], options))
.filter(pattern => pattern !== NULL));
const n = parsedPatterns.length;
@ -454,12 +469,12 @@ function parsedExpression(expression: IExpression): ParsedExpression {
return resultExpression;
}
function parseExpressionPattern(pattern: string, value: any): (ParsedStringPattern | ParsedExpressionPattern) {
function parseExpressionPattern(pattern: string, value: any, options: IGlobOptions): (ParsedStringPattern | ParsedExpressionPattern) {
if (value === false) {
return NULL; // pattern is disabled
}
const parsedPattern = parsePattern(pattern);
const parsedPattern = parsePattern(pattern, options);
if (parsedPattern === NULL) {
return NULL;
}

View file

@ -712,4 +712,46 @@ suite('Glob', () => {
'**/bar': true
})), ['bar']);
});
test('expression/pattern optimization for basenames', function () {
assert.deepStrictEqual(glob.getBasenameTerms(glob.parse('**/foo/**')), []);
assert.deepStrictEqual(glob.getBasenameTerms(glob.parse('**/foo/**', { trimForExclusions: true })), ['foo']);
testOptimizationForBasenames('**/*.foo/**', [], [['baz/bar.foo/bar/baz', true]]);
testOptimizationForBasenames('**/foo/**', ['foo'], [['bar/foo', true], ['bar/foo/baz', false]]);
testOptimizationForBasenames('{**/baz/**,**/foo/**}', ['baz', 'foo'], [['bar/baz', true], ['bar/foo', true]]);
testOptimizationForBasenames({
'**/foo/**': true,
'{**/bar/**,**/baz/**}': true,
'**/bulb/**': false
}, ['foo', 'bar', 'baz'], [
['bar/foo', '**/foo/**'],
['foo/bar', '{**/bar/**,**/baz/**}'],
['bar/nope', null]
]);
const siblingsFn = () => ['baz', 'baz.zip', 'nope'];
testOptimizationForBasenames({
'**/foo/**': { when: '$(basename).zip' },
'**/bar/**': true
}, ['bar'], [
['bar/foo', null],
['bar/foo/baz', null],
['bar/foo/nope', null],
['foo/bar', '**/bar/**'],
], [
null,
siblingsFn,
siblingsFn
]);
});
function testOptimizationForBasenames(pattern: string|glob.IExpression, basenameTerms: string[], matches: [string, string|boolean][], siblingsFns: (() => string[])[] = []) {
const parsed = glob.parse(<glob.IExpression>pattern, { trimForExclusions: true });
assert.deepStrictEqual(glob.getBasenameTerms(parsed), basenameTerms);
matches.forEach(([text, result], i) => {
assert.strictEqual(parsed(text, null, siblingsFns[i]), result);
});
}
});

View file

@ -67,7 +67,7 @@ export class FileWalker {
constructor(config: IRawSearch) {
this.config = config;
this.filePattern = config.filePattern;
this.excludePattern = glob.parse(config.excludePattern);
this.excludePattern = glob.parse(config.excludePattern, { trimForExclusions: true });
this.includePattern = config.includePattern && glob.parse(config.includePattern);
this.maxResults = config.maxResults || null;
this.maxFilesize = config.maxFilesize || null;