Skip to content

fix: match variable-depth ** alongside {placeholder} and excludeFiles globs - #58

Open
mliem2k wants to merge 2 commits into
vercel-labs:mainfrom
mliem2k:fix/placeholder-double-star-matching
Open

fix: match variable-depth ** alongside {placeholder} and excludeFiles globs#58
mliem2k wants to merge 2 commits into
vercel-labs:mainfrom
mliem2k:fix/placeholder-double-star-matching

Conversation

@mliem2k

@mliem2k mliem2k commented Jul 12, 2026

Copy link
Copy Markdown

Pull Request Description

Fixes #56
Fixes #57

Both issues share the same root cause, per the reproduction steps in each: tryExtractPlaceholders() in src/core/path-matcher.ts required the pattern and the matched path to have the exact same number of /-separated segments. Since ** can legitimately match a variable number of segments, this equal-length check silently rejected any candidate where ** didn't happen to consume exactly one segment.

Relevant technical choices

Rather than special-casing or warning about ** next to a placeholder, this implements the "real fix" the reporter suggested: path segment matching (matchPatternSegments in path-matcher.ts) now backtracks through candidate consumption counts when it hits a ** pattern segment, trying each possible number of consumed path segments until the rest of the pattern (including any placeholders after the **) also matches. This is a standard backtracking glob-with-captures matcher; it replaces the old index-by-index walk in tryExtractPlaceholders.

excludeFiles matching in runner.ts (isFileExcluded) previously only supported exact-path and basename comparisons — it never actually ran patterns through glob matching, which is why **/-prefixed patterns (and any other wildcard syntax) never worked despite docs/reference/configuration.md documenting "**/*.test.ts" as valid. I added a new exported matchesPathPattern() in path-matcher.ts (reusing the same backtracking segment matcher) and call it from isFileExcluded as an additional check, so excludeFiles now supports the same *, ?, and ** glob syntax as paths, while keeping the existing exact-path/basename behavior for plain filenames unchanged.

Testing

  • Added unit tests in path-matcher.test.ts reproducing the exact ** next to a {placeholder} silently matches only one directory level (not all depths) #56 fixture tree/pattern (all three depths now match with correct placeholder values), a baseline check that plain ** without a placeholder still matches every depth, a multi-placeholder-consistency check with ** present, and direct tests of matchesPathPattern covering **/-prefixed, trailing-**, and literal patterns.
  • Added a regression test in runner.test.ts reproducing excludeFiles patterns prefixed with **/ are silently ignored (docs show **/*.test.ts as valid) #57's exclusion scenario end-to-end through run().
  • Updated the exclude-files e2e fixture to exclude via a **/-prefixed pattern instead of a full relative path, exercising the real tinyglobby-backed CLI path; confirmed (by temporarily reverting the source fix) that this fixture fails without the fix and passes with it.
  • Full suite (pnpm test, pnpm typecheck, pnpm check, pnpm test:e2e) passes.

Checklist

  • All commits are signed (PRs with unsigned commits cannot be merged)
  • Tests have been added / updated
  • A changeset has been added (run pnpm changeset in the project root if package files were modified)
  • I have reviewed this pull request (self-review)

Copilot AI review requested due to automatic review settings July 12, 2026 13:35

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@mliem2k
mliem2k force-pushed the fix/placeholder-double-star-matching branch from fe0e55a to 294e853 Compare July 12, 2026 15:44

@felixarntz felixarntz left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mliem2k Thank you for the PR! Overall the fix looks solid, but it introduces one regression elsewhere and therefore does not yet fully address the problem. See comment below and the failure for the e2e test I added.

Comment on lines +207 to +221
if (segment === "**") {
for (
let consumedUpTo = pathIndex;
consumedUpTo <= pathSegments.length;
consumedUpTo++
) {
const rest = matchPatternSegments({
patternSegments,
pathSegments,
patternIndex: patternIndex + 1,
pathIndex: consumedUpTo,
});
if (rest !== null) {
return rest;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

** stops backtracking before placeholder semantics are validated: The matcher returns the first structurally matching ** split. Placeholder consistency is merged later, and constraints are checked only after matching completes. If that first split fails either check, later valid splits are never tried.

In 7ae5e18, I've added an e2e test that confirms this regression. This needs to be addressed before we can merge this PR.

mliem2k and others added 2 commits July 22, 2026 06:31
…excludeFiles globs

** previously matched greedily with no backtracking, so it couldn't
coexist with {placeholder} segments elsewhere in the same pattern (vercel-labs#56),
and excludeFiles globs containing ** never matched anything past the
first consumed segment (vercel-labs#57).

matchPatternSegments now backtracks through candidate ** consumption
counts, and validates placeholder consistency and constraints as soon
as a segment is bound rather than after the full match completes, so a
conflict on one split causes the nearest enclosing ** to try its next
candidate instead of failing the whole match outright.
@mliem2k
mliem2k force-pushed the fix/placeholder-double-star-matching branch from 7ae5e18 to 2aa6e0c Compare July 22, 2026 05:48
@mliem2k

mliem2k commented Jul 22, 2026

Copy link
Copy Markdown
Author

Thanks for catching this, and for the regression test, that pinned down the exact failure mode.

Root cause: matchPatternSegments returned the first structurally matching ** split, but placeholder consistency and constraints were only checked after the full match completed. If that first split conflicted with an earlier placeholder value or failed a constraint, later valid splits were never tried.

Fix: the accumulated placeholder values are now threaded through the recursion and validated as soon as a segment binds, so a conflict causes the nearest enclosing ** to backtrack to its next candidate instead of failing the whole match.

Your e2e test (globstar-placeholder-backtracking) now passes, and I added a matching unit test in path-matcher.test.ts. Full suite (unit + e2e + typecheck) is green.

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

Labels

None yet

Projects

None yet

3 participants