Skip to content
Open
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
38 changes: 26 additions & 12 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ export function getPartialMatcher(patterns: string[], options: PartialMatcherOpt
const patternsCount = patterns.length;
const patternsParts: string[][] = Array(patternsCount);
const matchers: Matcher[][] = Array(patternsCount);
let i: number, j: number;
const globstar = !options.noglobstar;
let i: number, j: number, k: number;

for (i = 0; i < patternsCount; i++) {
const parts = splitPattern(patterns[i]);
Expand All @@ -45,14 +46,18 @@ export function getPartialMatcher(patterns: string[], options: PartialMatcherOpt
return true;
}

const inputPartsCount = inputParts.length;

for (i = 0; i < patternsCount; i++) {
const patternParts = patternsParts[i];
const matcher = matchers[i];
const inputPatternCount = inputParts.length;
const minParts = Math.min(inputPatternCount, patternParts.length);
const patternPartsCount = patternParts.length;

// `j` walks the pattern, `k` walks the input. They only go out of sync when a
// `**` matches zero path segments, which is the one case where the two differ.
j = 0;
while (j < minParts) {
k = 0;
while (j < patternPartsCount && k < inputPartsCount) {
const part = patternParts[j];

// handling slashes in parts is very hard, not even fast-glob does it
Expand All @@ -62,20 +67,29 @@ export function getPartialMatcher(patterns: string[], options: PartialMatcherOpt
return true;
}

if (!matcher[j](inputParts[j])) {
break;
if (globstar && part === '**') {
// unlike popular belief, `**` doesn't return true in *all* cases
// some examples are when matching it to `.a` with dot: false or `..`
// so it needs to match to consume one or more segments and return early
if (matcher[j](inputParts[k])) {
return true;
}

// `**` also matches zero segments, so the rest of the pattern still has to
// be tried against the same input part. Without this, a pattern like
// `**/.a/*.ts` would never crawl into `.a` when `dot` is false.
j++;
continue;
}

// unlike popular belief, `**` doesn't return true in *all* cases
// some examples are when matching it to `.a` with dot: false or `..`
// so it needs to match to return early
if (!options.noglobstar && part === '**') {
return true;
if (!matcher[j](inputParts[k])) {
break;
}

j++;
k++;
}
if (j === inputPatternCount) {
if (k === inputPartsCount) {
return true;
}
}
Expand Down
12 changes: 12 additions & 0 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,18 @@ test('leading ../', async () => {
assert.deepEqual(files.sort(), ['../b/a.txt', '../b/b.txt']);
});

// `**` also matches zero path segments, so `**/.a/**` has to reach `.a` at the
// root even though `dot` is false and `**` on its own doesn't match `.a`.
test('leading ** before a dot directory', async () => {
const files = await glob('**/.a/a/*.txt', { cwd, expandDirectories: false });
assert.deepEqual(files.sort(), ['.a/a/a.txt']);
});

test('** before a dot directory with a trailing **', async () => {
const files = await glob('**/.deep/a/**/*.txt', { cwd, expandDirectories: false });
assert.deepEqual(files.sort(), ['.deep/a/a/a.txt']);
});

test('leading ../ with only dirs', async () => {
const files = await glob('../.a/*', { cwd: path.join(cwd, 'a'), onlyDirectories: true, expandDirectories: false });
assert.deepEqual(files.sort(), ['../.a/a/']);
Expand Down
27 changes: 27 additions & 0 deletions test/utils/partial-matcher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,31 @@ describe('getPartialMatcher', () => {
assert.ok(matcher('src/utils'));
assert.ok(!matcher('src/gaming'));
});

test('leading ** matches zero segments', () => {
// `**` matching nothing is what makes `**/.a` match `.a` at the root.
// With `dot: false` the `**` matcher itself rejects `.a`, so the rest of
// the pattern has to be tried against the same input part.
const matcher = getPartialMatcher(['**/.a/*.txt']);
assert.ok(matcher('.a'));
assert.ok(matcher('.a/b.txt'));
assert.ok(matcher('src/.a'));
assert.ok(!matcher('.b'));
});

test('** matches zero segments in the middle of a pattern', () => {
const matcher = getPartialMatcher(['test/**/.a/*.txt']);
assert.ok(matcher('test'));
assert.ok(matcher('test/.a'));
assert.ok(matcher('test/utils/.a'));
assert.ok(!matcher('test/.b'));
});

test('** matching zero segments does not create false positives', () => {
const matcher = getPartialMatcher(['a/**/.b/c']);
assert.ok(matcher('a'));
assert.ok(matcher('a/.b'));
assert.ok(!matcher('.b'));
assert.ok(!matcher('a/.c'));
});
});