Skip to content
Closed
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
131 changes: 130 additions & 1 deletion lib/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,119 @@ const parse = (input, options) => {
decrement('parens');
};

const findMatchingClose = startIndex => {
let depth = 1;
let escaped = false;
let quote = 0;

for (let i = startIndex + 1; i < len; i++) {
const ch = input[i];

if (escaped === true) {
escaped = false;
continue;
}

if (ch === '\\') {
escaped = true;
continue;
}

if (ch === '"') {
quote = quote === 1 ? 0 : 1;
continue;
}

if (quote === 1) {
continue;
}

if (ch === '(') {
depth++;
} else if (ch === ')') {
depth--;
if (depth === 0) {
return i;
}
}
}

return -1;
};

const parenBodyNeedsGroup = closeIndex => {
if (closeIndex === -1) {
return false;
}

const body = input.slice(state.index + 1, closeIndex);
let escaped = false;
let quote = 0;

for (const ch of body) {
if (escaped === true) {
return true;
}

if (ch === '\\') {
escaped = true;
continue;
}

if (ch === '"') {
quote = quote === 1 ? 0 : 1;
continue;
}

if (quote === 1) {
continue;
}

if (ch === '|' || ch === '*' || ch === '?' || ch === '[' || ch === '/' || ch === '{') {
return true;
}
}

return false;
};

const isLiteralParen = () => {
if (state.parens > 0 || extglobs.length > 0) {
return false;
}

// `@(...)` lands here: the `@` token is marked extglob and the `(` is next.
if (prev && (prev.extglob === true || prev.type === 'at')) {
return false;
}

// Regex non-capturing groups and lookarounds: `(?:`, `(?=`, `(?!`, `(?<`...
if (peek() === '?' && /[!=<:]/.test(peek(2))) {
return false;
}

const close = findMatchingClose(state.index);

// Unmatched `(` must still increment `state.parens` so strictBrackets
// can throw at the end of the parse, matching the previous behavior.
if (close === -1) {
return opts.strictBrackets !== true;
}

if (parenBodyNeedsGroup(close)) {
return false;
}

// `(a)*` / `(a)+` are documented regex-style quantifiers after a group.
// A following `?` stays a glob qmark, so `/foo/(a)?` does not match `/foo/a`.
const after = input[close + 1];
if (after === '*' || after === '+') {
return false;
}

return true;
};

/**
* Fast paths
*/
Expand Down Expand Up @@ -787,9 +900,20 @@ const parse = (input, options) => {

/**
* Parentheses
*
* Bash treats bare `(` / `)` as literals. Picomatch still compiles them as
* grouping when they form a real extglob (`?(`, `*(`, `+(`, `@(`, `!(`), a
* regex extension (`(?:`, `(?=`, `(?!`, `(?<=`, `(?<!`), or a group whose
* body needs grouping (`|`, wildcards, slashes, etc.). Inert wraps such as
* `(a)` stay literal so `/foo/(a)` does not match `/foo/a` (issue #171).
*/

if (value === '(') {
if (isLiteralParen()) {
push({ type: 'text', value, output: '\\(' });
continue;
}

increment('parens');
push({ type: 'paren', value });
continue;
Expand All @@ -806,7 +930,12 @@ const parse = (input, options) => {
continue;
}

push({ type: 'paren', value, output: state.parens ? ')' : '\\)' });
if (state.parens === 0) {
push({ type: 'text', value, output: '\\)' });
continue;
}

push({ type: 'paren', value, output: ')' });
decrement('parens');
continue;
}
Expand Down
20 changes: 10 additions & 10 deletions test/extglobs-bash.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,8 @@ describe('extglobs (bash)', () => {
assert(!isMatch('a', '!(a)*', { bash: true, windows: true }));
});

it('"a" should match "(a)"', () => {
assert(isMatch('a', '(a)', { bash: true, windows: true }));
it('"a" should not match "(a)"', () => {
assert(!isMatch('a', '(a)', { bash: true, windows: true }));
});

it('"a" should not match "(b)"', () => {
Expand Down Expand Up @@ -280,8 +280,8 @@ describe('extglobs (bash)', () => {
assert(isMatch('a.a', '(a|d).(a|b)*', { bash: true, windows: true }));
});

it('"a.a" should match "(b|a).(a)"', () => {
assert(isMatch('a.a', '(b|a).(a)', { bash: true, windows: true }));
it('"a.a" should not match "(b|a).(a)"', () => {
assert(!isMatch('a.a', '(b|a).(a)', { bash: true, windows: true }));
});

it('"a.a" should match "*!(.a|.b|.c)"', () => {
Expand Down Expand Up @@ -1481,8 +1481,8 @@ describe('extglobs (bash)', () => {
assert(!isMatch('b', 'a!(b)*', { bash: true, windows: true }));
});

it('"b.a" should match "(b|a).(a)"', () => {
assert(isMatch('b.a', '(b|a).(a)', { bash: true, windows: true }));
it('"b.a" should not match "(b|a).(a)"', () => {
assert(!isMatch('b.a', '(b|a).(a)', { bash: true, windows: true }));
});

it('"b.a" should match "@(b|a).@(a)"', () => {
Expand Down Expand Up @@ -1877,8 +1877,8 @@ describe('extglobs (bash)', () => {
assert(!isMatch('e.e', '*.(a|b|@(ab|a*@(b))*(c)d)', { bash: true, windows: true }));
});

it('"ef" should match "()ef"', () => {
assert(isMatch('ef', '()ef', { bash: true, windows: true }));
it('"ef" should not match "()ef"', () => {
assert(!isMatch('ef', '()ef', { bash: true, windows: true }));
});

it('"effgz" should match "@(b+(c)d|e*(f)g?|?(h)i@(j|k))"', () => {
Expand Down Expand Up @@ -2281,8 +2281,8 @@ describe('extglobs (bash)', () => {
assert(!isMatch('foobb', '!(foo)b*', { bash: true, windows: true }));
});

it('"foobb" should match "(foo)bb"', () => {
assert(isMatch('foobb', '(foo)bb', { bash: true, windows: true }));
it('"foobb" should not match "(foo)bb"', () => {
assert(!isMatch('foobb', '(foo)bb', { bash: true, windows: true }));
});

it('"(foo)bb" should match "\\(foo\\)bb"', () => {
Expand Down
20 changes: 10 additions & 10 deletions test/extglobs-minimatch.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,8 @@ describe('extglobs (minimatch)', () => {
assert(!isMatch('a', '!(a)*', { windows: true }));
});

it('"a" should match "(a)"', () => {
assert(isMatch('a', '(a)', { windows: true }));
it('"a" should not match "(a)"', () => {
assert(!isMatch('a', '(a)', { windows: true }));
});

it('"a" should not match "(b)"', () => {
Expand Down Expand Up @@ -276,8 +276,8 @@ describe('extglobs (minimatch)', () => {
assert(isMatch('a.a', '(a|d).(a|b)*', { windows: true }));
});

it('"a.a" should match "(b|a).(a)"', () => {
assert(isMatch('a.a', '(b|a).(a)', { windows: true }));
it('"a.a" should not match "(b|a).(a)"', () => {
assert(!isMatch('a.a', '(b|a).(a)', { windows: true }));
});

it('"a.a" should match "*!(.a|.b|.c)"', () => {
Expand Down Expand Up @@ -1468,8 +1468,8 @@ describe('extglobs (minimatch)', () => {
assert(!isMatch('b', 'a!(b)*', { windows: true }));
});

it('"b.a" should match "(b|a).(a)"', () => {
assert(isMatch('b.a', '(b|a).(a)', { windows: true }));
it('"b.a" should not match "(b|a).(a)"', () => {
assert(!isMatch('b.a', '(b|a).(a)', { windows: true }));
});

it('"b.a" should match "@(b|a).@(a)"', () => {
Expand Down Expand Up @@ -1864,8 +1864,8 @@ describe('extglobs (minimatch)', () => {
assert(!isMatch('e.e', '*.(a|b|@(ab|a*@(b))*(c)d)', { windows: true }));
});

it('"ef" should match "()ef"', () => {
assert(isMatch('ef', '()ef', { windows: true }));
it('"ef" should not match "()ef"', () => {
assert(!isMatch('ef', '()ef', { windows: true }));
});

it('"effgz" should match "@(b+(c)d|e*(f)g?|?(h)i@(j|k))"', () => {
Expand Down Expand Up @@ -2264,8 +2264,8 @@ describe('extglobs (minimatch)', () => {
assert(!isMatch('foobb', '!(foo)b*', { windows: true }));
});

it('"foobb" should match "(foo)bb"', () => {
assert(isMatch('foobb', '(foo)bb', { windows: true }));
it('"foobb" should not match "(foo)bb"', () => {
assert(!isMatch('foobb', '(foo)bb', { windows: true }));
});

it('"(foo)bb" should match "\\(foo\\)bb"', () => {
Expand Down
3 changes: 2 additions & 1 deletion test/extglobs-temp.js
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,8 @@ describe('extglobs', () => {
assert(!isMatch('fofoofoofofoo', '(foo)bb', { windows: true }));
assert(!isMatch('foo', '(foo)bb', { windows: true }));
assert(!isMatch('foob', '(foo)bb', { windows: true }));
assert(isMatch('foobb', '(foo)bb', { windows: true }));
assert(!isMatch('foobb', '(foo)bb', { windows: true }));
assert(isMatch('(foo)bb', '(foo)bb', { windows: true }));
assert(!isMatch('foofoofo', '(foo)bb', { windows: true }));
assert(!isMatch('fooofoofofooo', '(foo)bb', { windows: true }));
assert(!isMatch('foooofo', '(foo)bb', { windows: true }));
Expand Down
7 changes: 4 additions & 3 deletions test/extglobs.js
Original file line number Diff line number Diff line change
Expand Up @@ -458,10 +458,10 @@ describe('extglobs', () => {
assert(!isMatch('a.bb', '(b|a).(a)'));
assert(!isMatch('a.aa.a', '(b|a).(a)'));
assert(!isMatch('cc.a', '(b|a).(a)'));
assert(isMatch('a.a', '(b|a).(a)'));
assert(!isMatch('a.a', '(b|a).(a)'));
assert(!isMatch('c.a', '(b|a).(a)'));
assert(!isMatch('dd.aa.d', '(b|a).(a)'));
assert(isMatch('b.a', '(b|a).(a)'));
assert(!isMatch('b.a', '(b|a).(a)'));

assert(!isMatch('aa.aa', '@(b|a).@(a)'));
assert(!isMatch('a.bb', '@(b|a).@(a)'));
Expand Down Expand Up @@ -706,7 +706,8 @@ describe('extglobs', () => {
assert(isMatch('ef', '@()ef'));

assert(!isMatch('def', '()ef'));
assert(isMatch('ef', '()ef'));
assert(!isMatch('ef', '()ef'));
assert(isMatch('()ef', '()ef'));
});

it('should match escaped parens', () => {
Expand Down
22 changes: 22 additions & 0 deletions test/issue-related.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,28 @@ describe('issue-related tests', () => {
assert(!isMatch('test/utils', 'test(/utils/**)/file'));
});

it('picomatch issue#171 - bare parentheses are literals, not capture groups', () => {
assert(!isMatch('/foo/a', '/foo/(a)'));
assert(!isMatch('/foo/a', '/foo/(a)', { noextglob: true }));
assert(!isMatch('/foo/a', '/foo/(a)', { noextglob: false }));

assert(isMatch('/foo/(a)', '/foo/(a)'));
assert(isMatch('/foo/(a)', '/foo/(a)', { noextglob: true }));
assert(isMatch('/foo/(a)', '/foo/(a)', { noextglob: false }));

assert(!isMatch('/foo/a', '/foo/(a)?'));
assert(!isMatch('/foo/a', '/foo/(a)?', { noextglob: true }));
assert(!isMatch('/foo/a', '/foo/(a)?', { noextglob: false }));

// Real extglobs must keep working.
assert(isMatch('/foo/a', '/foo/@(a)'));
assert(isMatch('/foo/a', '/foo/?(a)'));
assert(isMatch('/foo/a', '/foo/+(a)'));
assert(isMatch('/foo/aa', '/foo/*(a)'));
assert(!isMatch('/foo/a', '/foo/!(a)'));
assert(!isMatch('/foo/a', '/foo/@(a)', { noextglob: true }));
});

it('should treat a leading `**` followed by a literal as a single star (picomatch/issues#99)', () => {
// `**` only acts as a globstar when it is the sole content of a path segment.
// When it is adjacent to other characters in the same segment (here `.thing.js`),
Expand Down
2 changes: 1 addition & 1 deletion test/negation.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ describe('negation patterns - "!"', () => {
assert(!isMatch('a/a', '!a/(*)'));
assert(!isMatch('a/b', '!a/(*)'));
assert(!isMatch('a/c', '!a/(*)'));
assert(!isMatch('a/b', '!a/(b)'));
assert(isMatch('a/b', '!a/(b)'));
assert(!isMatch('a/a', '!a/*'));
assert(!isMatch('a/b', '!a/*'));
assert(!isMatch('a/c', '!a/*'));
Expand Down
2 changes: 1 addition & 1 deletion test/options.ignore.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ describe('options.ignore', () => {
assert.deepStrictEqual(match(['foo.js', 'a/foo.js'], '**/foo.js', { dot: true }), ['foo.js', 'a/foo.js']);

assert.deepStrictEqual(match(negations, '!b/a', opts), ['b/b', 'b/c']);
assert.deepStrictEqual(match(negations, '!b/(a)', opts), ['b/b', 'b/c']);
assert.deepStrictEqual(match(negations, '!b/(a)', opts), ['b/a', 'b/b', 'b/c']);
assert.deepStrictEqual(match(negations, '!(b/(a))', opts), ['b/b', 'b/c']);
assert.deepStrictEqual(match(negations, '!(b/a)', opts), ['b/b', 'b/c']);

Expand Down
16 changes: 8 additions & 8 deletions test/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,24 +70,24 @@ describe('options', () => {

it('should not match extglobs when noextglob is true', () => {
assert(!isMatch('ax', '?(a*|b)', { noextglob: true, windows: true }));
assert.deepStrictEqual(match(['a.j.js', 'a.md.js'], '*.*(j).js', { noextglob: true, windows: true }), ['a.j.js']);
assert.deepStrictEqual(match(['a.j.js', 'a.md.js'], '*.*(j).js', { noextglob: true, windows: true }), []);
assert.deepStrictEqual(match(['a/z', 'a/b', 'a/!(z)'], 'a/!(z)', { noextglob: true, windows: true }), ['a/!(z)']);
assert.deepStrictEqual(match(['a/z', 'a/b'], 'a/!(z)', { noextglob: true, windows: true }), []);
assert.deepStrictEqual(match(['c/a/v'], 'c/!(z)/v', { noextglob: true, windows: true }), []);
assert.deepStrictEqual(match(['c/z/v', 'c/a/v'], 'c/!(z)/v', { noextglob: true, windows: true }), []);
assert.deepStrictEqual(match(['c/z/v', 'c/a/v'], 'c/@(z)/v', { noextglob: true, windows: true }), []);
assert.deepStrictEqual(match(['c/z/v', 'c/a/v'], 'c/+(z)/v', { noextglob: true, windows: true }), []);
assert.deepStrictEqual(match(['c/z/v', 'c/a/v'], 'c/*(z)/v', { noextglob: true, windows: true }), ['c/z/v']);
assert.deepStrictEqual(match(['c/z/v', 'z', 'zf', 'fz'], '?(z)', { noextglob: true, windows: true }), ['fz']);
assert.deepStrictEqual(match(['c/z/v', 'c/a/v'], 'c/*(z)/v', { noextglob: true, windows: true }), []);
assert.deepStrictEqual(match(['c/z/v', 'z', 'zf', 'fz'], '?(z)', { noextglob: true, windows: true }), []);
assert.deepStrictEqual(match(['c/z/v', 'z', 'zf', 'fz'], '+(z)', { noextglob: true, windows: true }), []);
assert.deepStrictEqual(match(['c/z/v', 'z', 'zf', 'fz'], '*(z)', { noextglob: true, windows: true }), ['z', 'fz']);
assert.deepStrictEqual(match(['c/z/v', 'z', 'zf', 'fz'], '*(z)', { noextglob: true, windows: true }), []);
assert.deepStrictEqual(match(['cz', 'abz', 'az'], 'a@(z)', { noextglob: true, windows: true }), []);
assert.deepStrictEqual(match(['cz', 'abz', 'az'], 'a*@(z)', { noextglob: true, windows: true }), []);
assert.deepStrictEqual(match(['cz', 'abz', 'az'], 'a!(z)', { noextglob: true, windows: true }), []);
assert.deepStrictEqual(match(['cz', 'abz', 'az', 'azz'], 'a?(z)', { noextglob: true, windows: true }), ['abz', 'azz']);
assert.deepStrictEqual(match(['cz', 'abz', 'az', 'azz', 'a+z'], 'a+(z)', { noextglob: true, windows: true }), ['a+z']);
assert.deepStrictEqual(match(['cz', 'abz', 'az'], 'a*(z)', { noextglob: true, windows: true }), ['abz', 'az']);
assert.deepStrictEqual(match(['cz', 'abz', 'az'], 'a**(z)', { noextglob: true, windows: true }), ['abz', 'az']);
assert.deepStrictEqual(match(['cz', 'abz', 'az', 'azz'], 'a?(z)', { noextglob: true, windows: true }), []);
assert.deepStrictEqual(match(['cz', 'abz', 'az', 'azz', 'a+z'], 'a+(z)', { noextglob: true, windows: true }), []);
assert.deepStrictEqual(match(['cz', 'abz', 'az'], 'a*(z)', { noextglob: true, windows: true }), []);
assert.deepStrictEqual(match(['cz', 'abz', 'az'], 'a**(z)', { noextglob: true, windows: true }), []);
assert.deepStrictEqual(match(['cz', 'abz', 'az'], 'a*!(z)', { noextglob: true, windows: true }), []);
});
});
Expand Down
6 changes: 4 additions & 2 deletions test/options.noextglob.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@ const { isMatch } = require('..');

describe('options.noextglob', () => {
it('should disable extglob support when options.noextglob is true', () => {
assert(isMatch('a+z', 'a+(z)', { noextglob: true }));
assert(isMatch('a+(z)', 'a+(z)', { noextglob: true }));
assert(!isMatch('a+z', 'a+(z)', { noextglob: true }));
assert(!isMatch('az', 'a+(z)', { noextglob: true }));
assert(!isMatch('azz', 'a+(z)', { noextglob: true }));
assert(!isMatch('azzz', 'a+(z)', { noextglob: true }));
});

it('should work with noext alias to support minimatch', () => {
assert(isMatch('a+z', 'a+(z)', { noext: true }));
assert(isMatch('a+(z)', 'a+(z)', { noext: true }));
assert(!isMatch('a+z', 'a+(z)', { noext: true }));
assert(!isMatch('az', 'a+(z)', { noext: true }));
assert(!isMatch('azz', 'a+(z)', { noext: true }));
assert(!isMatch('azzz', 'a+(z)', { noext: true }));
Expand Down
Loading