Visitar URL original
fix(partial-matcher): let `**` match zero path segments by MFA-G · Pull Request #215 · SuperchupuDev/tinyglobby · GitHub
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 test → 135 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
kasperpeulen added a commit to storybookjs/storybook that referenced this pull request Nov 4, 2026
Backport the fix from SuperchupuDev/tinyglobby#215 as a yarn patch on
core's bundled tinyglobby, so '**/.docs/*.stories.ts' finds
'.docs/A.stories.ts' again. Add a MIGRATION.md entry for the remaining
story-glob differences and commit the differential comparison script.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kasperpeulen

Copy link
Copy Markdown

Storybook hits this bug while moving its story globs from globby to tinyglobby (storybookjs/storybook#37171). A stories entry like '../src/**/.docs/*.stories.ts' misses src/.docs/A.stories.ts with tinyglobby 0.2.17, but globby finds it.

I checked out this PR and it fixes our case. It also restores globby's results for patterns like **/!(node_modules)/**/*.stories.tsx, which skipped dot folders the same way. Until a release includes the fix, we ship this change as a yarn patch on 0.2.17, and we'll drop the patch once it is released.

Thanks for tinyglobby, and thanks @MFA-G for the fix!

@SuperchupuDev

Copy link
Copy Markdown
Owner

hello, sorry for not being active lately, been busy with real life stuff. i believe the latest picomatch version fixed this on its own? i might be wrong though

@MFA-G

MFA-G commented Nov 6, 2026

Copy link
Copy Markdown
Author

Checked: this repo already resolves picomatch 4.0.7 (latest on npm), and the bug still reproduces on main with it. The failing step is not picomatch — it is getPartialMatcher's lockstep walk in src/utils.ts.

With dot: false, picomatch('**')('.foo') correctly returns false. That is exactly why the crawler must not stop there: ** can match zero segments, so the next pattern part (.foo) still has to be tried against the same input segment.

On main, getPartialMatcher tests patternParts[j] against inputParts[j] with one shared index and breaks as soon as the ** matcher rejects the segment. .foo/ is then never descended into, so the correct full matcher downstream never gets a chance to see .foo/foo.ts. Bumping picomatch cannot change that break.

Repro on a fixture, expandDirectories: false, picomatch 4.0.7 installed from the lockfile:

  • **/.foo/*.ts → main: [] · this PR: ['.foo/foo.ts']
  • **/.deep/a/**/*.txt → main: [] · this PR: ['.deep/a/b.txt']
  • **/.foo/*.ts with dot: true → both: ['.foo/foo.ts'] (the ** matcher already accepts .foo, so the walk never diverges)

pnpm test on this branch with picomatch 4.0.7: 135 pass, 0 fail. The Storybook case from @kasperpeulen above (../src/**/.docs/*.stories.ts) exercises the same zero-segment path, so I think the walk fix is still needed.

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

3 participants