Skip to content

fix: preserve globstar in non-final extglob alternatives#177

Open
DucMinhNe wants to merge 1 commit into
micromatch:masterfrom
DucMinhNe:fix/extglob-globstar-alternative-order
Open

fix: preserve globstar in non-final extglob alternatives#177
DucMinhNe wants to merge 1 commit into
micromatch:masterfrom
DucMinhNe:fix/extglob-globstar-alternative-order

Conversation

@DucMinhNe

Copy link
Copy Markdown

Fixes #154.

Problem

In a negation extglob like !(a/**|b/**), the result depends on the order of the alternatives:

const pm = require('picomatch');
const opts = { dot: true };

pm('!(a/**|b/**)**', opts)('a/x/y'); // => true  (wrong — a/** should exclude it)
pm('!(b/**|a/**)**', opts)('a/x/y'); // => false (correct)

Only the last alternative's ** compiles to a real globstar. Every non-final alternative's ** is silently downgraded to a single path segment ([^/]*?), so it can't match deep paths inside the lookahead — and the negation fails to exclude them.

Cause

The globstar-downgrade guard in push (lib/parse.js) keeps a ** as a globstar when the following token is a pipe:

const isExtglob = tok.extglob === true || (extglobs.length && (tok.type === 'pipe' || tok.type === 'paren'));

But the | token is pushed as { type: 'text', value: '|' } (see the Pipes branch), never type: 'pipe'. So tok.type === 'pipe' is effectively dead code and the preceding globstar gets downgraded. The last alternative survives only because it is followed by ) (type: 'paren', which is guarded) — hence the order-dependence.

Fix

Also treat a token whose value is | as an extglob boundary:

const isExtglob = tok.extglob === true || (extglobs.length && (tok.type === 'pipe' || tok.value === '|' || tok.type === 'paren'));

This is surgical — it only affects ** immediately followed by | inside an extglob.

Tests

Added a regression test in test/extglobs.js asserting the negation result is independent of alternative order, that the last alternative still excludes deep paths, and that unrelated paths are still matched (no over-exclusion).

Full suite: 1976 passing, lint clean. Verified the new test fails on master and passes with the fix.

In a negation extglob such as `!(a/**|b/**)`, only the last alternative's
`**` compiled to a real globstar; every non-final alternative's `**` was
downgraded to a single path segment (`[^/]*?`). This made negation results
depend on the order of the alternatives — e.g. `!(a/**|b/**)**` and
`!(b/**|a/**)**` matched `a/x/y` differently.

The globstar-downgrade guard in `push` kept a `**` only when the following
token was `tok.type === 'pipe'`, but the `|` token is pushed as
`{ type: 'text', value: '|' }`, so that branch never fired. Also treat a
token whose value is `|` as an extglob boundary.

Fixes micromatch#154
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.

Negation extglob produces different results depending on order of alternatives

1 participant