Skip to content

fix: treat a brace range with no bounds as literal text - #232

Open
Kumar44developer wants to merge 1 commit into
micromatch:masterfrom
Kumar44developer:fix/empty-brace-range-matches-nothing
Open

Kumar44developer wants to merge 1 commit into
micromatch:masterfrom
Kumar44developer:fix/empty-brace-range-matches-nothing

Conversation

@Kumar44developer

Copy link
Copy Markdown

Treat a brace range with no bounds as literal text

Problem

A brace range with no bounds, such as {..}, is expanded to the empty character class []:

picomatch.makeRe('{..}');      //=> /^(?:[])$/
picomatch.makeRe('a{..}b');    //=> /^(?:a[]b)$/

[] is a valid JavaScript character class, so new RegExp doesn't throw and the existing fallback in expandRange never kicks in — but an empty class can never match anything. That makes the entire pattern unmatchable:

picomatch.isMatch('a{..}',  '*{..}');   //=> false  (expected true)
picomatch.isMatch('x{..}',  '?{..}');   //=> false  (expected true)
picomatch.isMatch('a{..}b', '*{..}b');  //=> false  (expected true)
picomatch.isMatch('{..}/x', '{..}/*');  //=> false  (expected true)

There is no string that *{..} can match.

This is easy to miss because isMatch(p, p) short-circuits on exact string equality, so isMatch('{..}', '{..}') returns true while the compiled regex matches nothing. makeRe exposes the real behaviour.

Both bash and picomatch's own nobrace option treat {..} as literal text:

$ echo {..}      # => {..}
$ echo a{..}b    # => a{..}b
picomatch.isMatch('a{..}', '*{..}', { nobrace: true });  //=> true

Fix

The range bounds are now collected before the tokens are consumed, so that a boundless range can fall through to the existing literal-brace handling instead of being expanded:

if (range.length === 0) {
  literalRange = true;
} else {
  // consume tokens and expand, exactly as before
}

Ranges that do have bounds are untouched — the same tokens are popped and expandRange is called with the same arguments, so {a..c} still compiles to [a-c] and {1..3} to [1-3].

This also escapes the second dot of a dots token. Previously the token's output was \.., with the second dot left raw. That never mattered while the output was always replaced by expandRange, but now that the token can be rendered as literal text the unescaped dot would act as a wildcard and {..} would match {.X}.

// after
picomatch.makeRe('{..}');                //=> /^(?:\{\.\.\})$/
picomatch.isMatch('a{..}', '*{..}');     //=> true
picomatch.isMatch('a{.X}', '*{..}');     //=> false

Scope

This fixes only the boundless case, where the result can never match under any interpretation. Ranges with a single bound ({a..}, {...}) still expand to a one-character class; those have a plausible-but-debatable intended behaviour, so I left them alone rather than fold a semantics change into a bug fix. Happy to follow up if you'd like them treated as literal too.

Tests

Added a test to test/braces.js covering the literal matches, the patterns where the equality short-circuit can't hide the bug, and the escaped dots. Verified it fails on current master:

1) braces
     should treat a range with no bounds as literal text:
   AssertionError: assert(isMatch('a{..}', '*{..}'))
   + expected - actual
   -false
   +true

With the fix the full suite passes — 1999 passing (1998 before, plus the new test) — and eslint is clean.

A range with no bounds, such as '{..}', was expanded to the empty character class '[]'. An empty class can never match, so the whole pattern became unmatchable: isMatch('a{..}', '*{..}') returned false and no string could match '*{..}'.

Collect the range bounds before consuming the tokens so a boundless range can fall through to the existing literal-brace handling, which is what bash and the nobrace option already do. The second dot of a dots token is now escaped as well, so the token still matches literal dots when it is rendered as literal text.
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.

1 participant