Skip to content

fix(partial-matcher): let ** match zero path segments - #215

Open
MFA-G wants to merge 1 commit into
SuperchupuDev:mainfrom
MFA-G:fix/partial-matcher-globstar-zero-segments
Open

MFA-G wants to merge 1 commit into
SuperchupuDev:mainfrom
MFA-G:fix/partial-matcher-globstar-zero-segments

Conversation

@MFA-G

@MFA-G MFA-G commented Sep 4, 2026

Copy link
Copy Markdown

Fixes #187.

The bug

getPartialMatcher is the predicate that decides whether a directory is worth descending into. It walked the pattern parts and the input parts in lockstep with a single index, which meant ** could only ever stand for one or more segments — as soon as the ** matcher rejected the current input part, the whole pattern was abandoned.

** also matches zero segments, and that case matters as soon as the segment right after the ** is the one that matches. With dot: false, picomatch's ** does not match .foo, so:

await glob('**/.foo/*.ts', { cwd, expandDirectories: false });
// tinyglobby: []
// fast-glob:  ['.foo/foo.ts']

The crawler never entered .foo, so the correct full matcher downstream never got the chance to run. With dot: true the ** matcher accepts .foo, which is why the issue only reproduces on the default.

The same shape breaks any pattern where a ** precedes a segment the ** itself won't match — **/.deep/a/**/*.txt returned [] too.

The fix

Track the pattern position (j) and the input position (k) separately. When a ** doesn't consume the current input segment, advance only the pattern and retry the next pattern part against the same segment, instead of ending the walk. When it does consume it, the existing early return true is unchanged.

The two indices only diverge in that one case, so the common path costs the same. noglobstar still treats ** as a literal part.

Validation

  • pnpm test135 pass, 0 fail. Reverting only src/utils.ts fails all 5 new assertions, so they pin the fix rather than restate current behavior.

  • pnpm typecheck, pnpm check (biome), pnpm build all clean.

  • pnpm bench on the typescript-eslint fixture — no regression, tinyglobby still fastest in both:

    tinyglobby fast-glob glob
    packages/*/tsconfig.json 2627 ops/s 2598 2249
    **/* 50 ops/s 35 19

Tests added: three unit tests on getPartialMatcher (leading **, mid-pattern **, and a negative case that the looser walk doesn't turn into a false positive) plus two integration tests against the existing .a / .deep fixtures.

I did not touch the dot semantics themselves — this is purely the traversal predicate being stricter than the matcher it guards. The other half of #187 (whether dot: false should skip dot directories at all, as opposed to dot files) is a deliberate behavior choice and is left alone.


🤖 Written with assistance from Claude Code.

`getPartialMatcher` decides whether a directory is worth crawling. It walked
the pattern and the input in lockstep, so `**` could only ever stand for one
or more segments: once its matcher rejected an input part, the whole pattern
was abandoned.

That is wrong whenever the segment after `**` is the one that matches. With
`dot: false` picomatch's `**` does not match `.a`, so `**/.a/*.txt` never
descended into `.a` and the glob returned nothing, while fast-glob returns
the file.

Track the pattern and input positions separately so a `**` that does not
consume the current segment falls through to the next pattern part instead
of ending the walk.

Fixes SuperchupuDev#187
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dotfile matching behaviour differs from fast-glob

1 participant