Skip to content

Commit 8ed4726

Browse files
author
mrmlnc
committed
fix: respect dot option in brace alternatives
Apply the leading-dot guard to brace alternatives starting with `*` or `!(...)` at path-segment boundaries, including alternatives after commas. Only add the guard when the `dot` option is disabled. Preserve literal braces and braces embedded within a segment, and add regression coverage for mrmlnc/fast-glob#405.
1 parent 36bc461 commit 8ed4726

3 files changed

Lines changed: 103 additions & 2 deletions

File tree

lib/parse.js

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,16 @@ const parse = (input, options) => {
521521
};
522522

523523
const extglobOpen = (type, value) => {
524+
const brace = braces[braces.length - 1];
525+
const isBrace = brace && stack[stack.length - 1] === 'braces' &&
526+
(brace.prev.type === 'bos' || brace.prev.type === 'slash') &&
527+
(prev.type === 'comma' || (prev.type === 'brace' && prev.value === '{'));
528+
529+
if (nodot && type === 'negate' && isBrace) {
530+
state.output += nodot;
531+
prev.output += nodot;
532+
}
533+
524534
const token = { ...EXTGLOB_CHARS[value], conditions: 1, inner: '' };
525535

526536
token.prev = prev;
@@ -1257,11 +1267,18 @@ const parse = (input, options) => {
12571267
}
12581268

12591269
const token = { type: 'star', value, output: star };
1270+
const brace = braces[braces.length - 1];
1271+
const isBrace = brace && stack[stack.length - 1] === 'braces' &&
1272+
(brace.prev.type === 'bos' || brace.prev.type === 'slash') &&
1273+
(prev.type === 'comma' || (prev.type === 'brace' && prev.value === '{'));
12601274

12611275
if (opts.bash === true) {
12621276
token.output = '.*?';
12631277
if (prev.type === 'bos' || prev.type === 'slash') {
12641278
token.output = nodot + token.output;
1279+
} else if (isBrace) {
1280+
state.output += nodot;
1281+
prev.output += nodot;
12651282
}
12661283
push(token);
12671284
continue;
@@ -1273,7 +1290,7 @@ const parse = (input, options) => {
12731290
continue;
12741291
}
12751292

1276-
if (state.index === state.start || prev.type === 'slash' || prev.type === 'dot') {
1293+
if (state.index === state.start || prev.type === 'slash' || prev.type === 'dot' || isBrace) {
12771294
if (prev.type === 'dot') {
12781295
state.output += NO_DOT_SLASH;
12791296
prev.output += NO_DOT_SLASH;

test/braces.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ describe('braces', () => {
2525
assert(isMatch('a {abc} b', 'a {abc} b'));
2626
assert(isMatch('a {a-b-c} b', 'a {a-b-c} b'));
2727
assert(isMatch('a {a.c} b', 'a {a.c} b'));
28+
assert(isMatch('{.foo}', '{*}'));
29+
assert(isMatch('{.foo}', '{*}', { bash: true }));
30+
assert(isMatch('{!.foo}', '{!(x)}'));
2831
});
2932

3033
it('should match literal braces when escaped', () => {

test/extglobs.js

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,81 @@ describe('extglobs', () => {
8686
assert(isMatch('/file.dhello.ts', '/!(*.d).@(ts)'));
8787
});
8888

89+
it('should apply dotfile rules to brace alternatives at segment starts', () => {
90+
const pattern = '**/{!(*.d).mts,!(*.d).cts,*.{mjs,cjs,js}}';
91+
const opts = { dot: false };
92+
const dotOpts = { dot: true };
93+
94+
assert(!isMatch('.cjs', pattern, opts));
95+
assert(!isMatch('.cts', pattern, opts));
96+
assert(!isMatch('.js', pattern, opts));
97+
assert(!isMatch('.mjs', pattern, opts));
98+
assert(!isMatch('.mts', pattern, opts));
99+
assert(isMatch('a.mjs', pattern, opts));
100+
assert(isMatch('ad.cts', pattern, opts));
101+
assert(isMatch('ad.mts', pattern, opts));
102+
assert(!isMatch('a/.cjs', pattern, opts));
103+
assert(!isMatch('a/.cts', pattern, opts));
104+
assert(!isMatch('a/.js', pattern, opts));
105+
assert(!isMatch('a/.mjs', pattern, opts));
106+
assert(!isMatch('a/.mts', pattern, opts));
107+
assert(isMatch('a/a.mjs', pattern, opts));
108+
assert(isMatch('a/ad.cts', pattern, opts));
109+
assert(isMatch('a/ad.mts', pattern, opts));
110+
111+
assert(isMatch('.cjs', pattern, dotOpts));
112+
assert(isMatch('.cts', pattern, dotOpts));
113+
assert(isMatch('.js', pattern, dotOpts));
114+
assert(isMatch('.mjs', pattern, dotOpts));
115+
assert(isMatch('.mts', pattern, dotOpts));
116+
assert(isMatch('a.mjs', pattern, dotOpts));
117+
assert(isMatch('ad.cts', pattern, dotOpts));
118+
assert(isMatch('ad.mts', pattern, dotOpts));
119+
assert(isMatch('a/.cjs', pattern, dotOpts));
120+
assert(isMatch('a/.cts', pattern, dotOpts));
121+
assert(isMatch('a/.js', pattern, dotOpts));
122+
assert(isMatch('a/.mjs', pattern, dotOpts));
123+
assert(isMatch('a/.mts', pattern, dotOpts));
124+
assert(isMatch('a/a.mjs', pattern, dotOpts));
125+
assert(isMatch('a/ad.cts', pattern, dotOpts));
126+
assert(isMatch('a/ad.mts', pattern, dotOpts));
127+
});
128+
129+
it('should apply dotfile rules only at segment starts', () => {
130+
assert(isMatch('.foo', '**/{.foo,*}'));
131+
assert(!isMatch('.bar', '**/{.foo,*}'));
132+
assert(isMatch('a.foo', 'a{*,b}'));
133+
});
134+
135+
it('should apply dotfile rules to first and subsequent brace alternatives', () => {
136+
assert(!isMatch('.js', '**/{*.js,x}', { dot: false }));
137+
assert(!isMatch('a/.js', '**/{*.js,x}', { dot: false }));
138+
assert(isMatch('.js', '**/{*.js,x}', { dot: true }));
139+
assert(isMatch('a/.js', '**/{*.js,x}', { dot: true }));
140+
141+
assert(!isMatch('.js', '**/{x,*.js}', { dot: false }));
142+
assert(!isMatch('a/.js', '**/{x,*.js}', { dot: false }));
143+
assert(isMatch('.js', '**/{x,*.js}', { dot: true }));
144+
assert(isMatch('a/.js', '**/{x,*.js}', { dot: true }));
145+
146+
assert(!isMatch('.mts', '**/{!(*.d).mts,x}', { dot: false }));
147+
assert(!isMatch('a/.mts', '**/{!(*.d).mts,x}', { dot: false }));
148+
assert(isMatch('.mts', '**/{!(*.d).mts,x}', { dot: true }));
149+
assert(isMatch('a/.mts', '**/{!(*.d).mts,x}', { dot: true }));
150+
151+
assert(!isMatch('.mts', '**/{x,!(*.d).mts}', { dot: false }));
152+
assert(!isMatch('a/.mts', '**/{x,!(*.d).mts}', { dot: false }));
153+
assert(isMatch('.mts', '**/{x,!(*.d).mts}', { dot: true }));
154+
assert(isMatch('a/.mts', '**/{x,!(*.d).mts}', { dot: true }));
155+
156+
assert(!isMatch('.js', '{*.js,x}', { bash: true }));
157+
assert(isMatch('a.js', '{*.js,x}', { bash: true }));
158+
assert(!isMatch('.js', '{x,*.js}', { bash: true }));
159+
assert(isMatch('a.js', '{x,*.js}', { bash: true }));
160+
assert(isMatch('a,.js', '{x,@(a,*.js)}'));
161+
assert(isMatch('a,.mts', '{x,@(a,!(*.d).mts)}'));
162+
});
163+
89164
it('should support negation extglobs in patterns with slashes', () => {
90165
assert(!isMatch('foo/abc', 'foo/!(abc)'));
91166
assert(isMatch('foo/bar', 'foo/!(abc)'));
@@ -292,6 +367,13 @@ describe('extglobs', () => {
292367
assert(isMatch('bb.md', '*(a|b).md'));
293368
});
294369

370+
it('should preserve zero-length *(...) matches in brace alternatives', () => {
371+
assert(isMatch('.md', '{*(a|b).md,x}'));
372+
assert(isMatch('a/.md', 'a/{x,*(a|b).md}'));
373+
assert(!isMatch('.a.md', '{*(a|b).md,x}', { noextglob: true }));
374+
assert(isMatch('a.md', '{*(a|b).md,x}', { noextglob: true }));
375+
});
376+
295377
it('should support matching file extensions with ?(...)', () => {
296378
assert(!isMatch('a.js', '?(a|b).md'));
297379
assert(!isMatch('bb.md', '?(a|b).md'));
@@ -770,4 +852,3 @@ describe('extglobs', () => {
770852
assert.deepStrictEqual(match(['foo', ' foo '], '(f|o)+\\b'), ['foo'], 'Should match word boundaries');
771853
});
772854
});
773-

0 commit comments

Comments
 (0)