• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

micromatch / picomatch / 23261533957

18 Mar 2026 06:49PM UTC coverage: 91.765% (-0.2%) from 91.968%
23261533957

Pull #155

github

danez
chore: Remove codeql and active in repo settings
Pull Request #155: chore: use prettier, update eslint, add node 24 and 25 to ci

752 of 837 branches covered (89.84%)

Branch coverage included in aggregate %.

64 of 69 new or added lines in 4 files covered. (92.75%)

52 existing lines in 4 files now uncovered.

875 of 936 relevant lines covered (93.48%)

3421.86 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.91
/lib/parse.js
1
'use strict';
2

3
const constants = require('./constants');
1✔
4
const utils = require('./utils');
1✔
5

6
/**
7
 * Constants
8
 */
9

10
const {
11
  MAX_LENGTH,
12
  POSIX_REGEX_SOURCE,
13
  REGEX_NON_SPECIAL_CHARS,
14
  REGEX_SPECIAL_CHARS_BACKREF,
15
  REPLACEMENTS
16
} = constants;
1✔
17

18
/**
19
 * Helpers
20
 */
21

22
const expandRange = (args, options) => {
1✔
23
  if (typeof options.expandRange === 'function') {
23✔
24
    return options.expandRange(...args, options);
20✔
25
  }
26

27
  args.sort();
3✔
28
  const value = `[${args.join('-')}]`;
3✔
29

30
  try {
3✔
31
    new RegExp(value);
3✔
32
    // eslint-disable-next-line no-unused-vars
33
  } catch (ex) {
34
    return args.map(v => utils.escapeRegex(v)).join('..');
×
35
  }
36

37
  return value;
3✔
38
};
39

40
/**
41
 * Create the message for a syntax error
42
 */
43

44
const syntaxError = (type, char) => {
1✔
45
  return `Missing ${type}: "${char}" - use "\\\\${char}" to match literal characters`;
6✔
46
};
47

48
/**
49
 * Parse the given input string.
50
 * @param {String} input
51
 * @param {Object} options
52
 * @return {Object}
53
 */
54

55
const parse = (input, options) => {
1✔
56
  if (typeof input !== 'string') {
8,525!
57
    throw new TypeError('Expected a string');
×
58
  }
59

60
  input = REPLACEMENTS[input] || input;
8,525✔
61

62
  const opts = { ...options };
8,525✔
63
  const max =
64
    typeof opts.maxLength === 'number'
8,525✔
65
      ? Math.min(MAX_LENGTH, opts.maxLength)
66
      : MAX_LENGTH;
67

68
  let len = input.length;
8,525✔
69
  if (len > max) {
8,525✔
70
    throw new SyntaxError(
2✔
71
      `Input length: ${len}, exceeds maximum allowed length: ${max}`
72
    );
73
  }
74

75
  const bos = { type: 'bos', value: '', output: opts.prepend || '' };
8,523✔
76
  const tokens = [bos];
8,523✔
77

78
  const capture = opts.capture ? '' : '?:';
8,523!
79

80
  // create constants based on platform, for windows or posix
81
  const PLATFORM_CHARS = constants.globChars(opts.windows);
8,523✔
82
  const EXTGLOB_CHARS = constants.extglobChars(PLATFORM_CHARS);
8,523✔
83

84
  const {
85
    DOT_LITERAL,
86
    PLUS_LITERAL,
87
    SLASH_LITERAL,
88
    ONE_CHAR,
89
    DOTS_SLASH,
90
    NO_DOT,
91
    NO_DOT_SLASH,
92
    NO_DOTS_SLASH,
93
    QMARK,
94
    QMARK_NO_DOT,
95
    STAR,
96
    START_ANCHOR
97
  } = PLATFORM_CHARS;
8,523✔
98

99
  const globstar = opts => {
8,523✔
100
    return `(${capture}(?:(?!${START_ANCHOR}${opts.dot ? DOTS_SLASH : DOT_LITERAL}).)*?)`;
2,982✔
101
  };
102

103
  const nodot = opts.dot ? '' : NO_DOT;
8,523✔
104
  const qmarkNoDot = opts.dot ? QMARK : QMARK_NO_DOT;
8,523✔
105
  let star = opts.bash === true ? globstar(opts) : STAR;
8,523✔
106

107
  if (opts.capture) {
8,523!
UNCOV
108
    star = `(${star})`;
×
109
  }
110

111
  // minimatch options support
112
  if (typeof opts.noext === 'boolean') {
8,523✔
113
    opts.noextglob = opts.noext;
4✔
114
  }
115

116
  const state = {
8,523✔
117
    input,
118
    index: -1,
119
    start: 0,
120
    dot: opts.dot === true,
121
    consumed: '',
122
    output: '',
123
    prefix: '',
124
    backtrack: false,
125
    negated: false,
126
    brackets: 0,
127
    braces: 0,
128
    parens: 0,
129
    quotes: 0,
130
    globstar: false,
131
    tokens
132
  };
133

134
  input = utils.removePrefix(input, state);
8,523✔
135
  len = input.length;
8,523✔
136

137
  const extglobs = [];
8,523✔
138
  const braces = [];
8,523✔
139
  const stack = [];
8,523✔
140
  let prev = bos;
8,523✔
141
  let value;
142

143
  /**
144
   * Tokenizing helpers
145
   */
146

147
  const eos = () => state.index === len - 1;
65,157✔
148
  const peek = (state.peek = (n = 1) => input[state.index + n]);
15,567✔
149
  const advance = (state.advance = () => input[++state.index] || '');
59,825!
150
  const remaining = () => input.slice(state.index + 1);
27,874✔
151
  const consume = (value = '', num = 0) => {
8,523!
152
    state.consumed += value;
59,971✔
153
    state.index += num;
59,971✔
154
  };
155

156
  const append = token => {
8,523✔
157
    state.output += token.output != null ? token.output : token.value;
57,435✔
158
    consume(token.value);
57,435✔
159
  };
160

161
  const negate = () => {
8,523✔
162
    let count = 1;
327✔
163

164
    while (peek() === '!' && (peek(2) !== '(' || peek(3) === '?')) {
327✔
165
      advance();
70✔
166
      state.start++;
70✔
167
      count++;
70✔
168
    }
169

170
    if (count % 2 === 0) {
327✔
171
      return false;
10✔
172
    }
173

174
    state.negated = true;
317✔
175
    state.start++;
317✔
176
    return true;
317✔
177
  };
178

179
  const increment = type => {
8,523✔
180
    state[type]++;
5,799✔
181
    stack.push(type);
5,799✔
182
  };
183

184
  const decrement = type => {
8,523✔
185
    state[type]--;
5,800✔
186
    stack.pop();
5,800✔
187
  };
188

189
  /**
190
   * Push tokens onto the tokens array. This helper speeds up
191
   * tokenizing by 1) helping us avoid backtracking as much as possible,
192
   * and 2) helping us avoid creating extra tokens when consecutive
193
   * characters are plain text. This improves performance and simplifies
194
   * lookbehinds.
195
   */
196

197
  const push = tok => {
8,523✔
198
    if (prev.type === 'globstar') {
50,928✔
199
      const isBrace =
200
        state.braces > 0 && (tok.type === 'comma' || tok.type === 'brace');
1,565✔
201
      const isExtglob =
202
        tok.extglob === true ||
1,565!
203
        (extglobs.length && (tok.type === 'pipe' || tok.type === 'paren'));
204

205
      if (
1,565✔
206
        tok.type !== 'slash' &&
2,502✔
207
        tok.type !== 'paren' &&
208
        !isBrace &&
209
        !isExtglob
210
      ) {
211
        state.output = state.output.slice(0, -prev.output.length);
299✔
212
        prev.type = 'star';
299✔
213
        prev.value = '*';
299✔
214
        prev.output = star;
299✔
215
        state.output += prev.output;
299✔
216
      }
217
    }
218

219
    if (extglobs.length && tok.type !== 'paren') {
50,928✔
220
      extglobs[extglobs.length - 1].inner += tok.value;
7,189✔
221
    }
222

223
    if (tok.value || tok.output) append(tok);
50,928!
224
    if (prev && prev.type === 'text' && tok.type === 'text') {
50,928✔
225
      prev.output = (prev.output || prev.value) + tok.value;
3,603✔
226
      prev.value += tok.value;
3,603✔
227
      return;
3,603✔
228
    }
229

230
    tok.prev = prev;
47,325✔
231
    tokens.push(tok);
47,325✔
232
    prev = tok;
47,325✔
233
  };
234

235
  const extglobOpen = (type, value) => {
8,523✔
236
    const token = { ...EXTGLOB_CHARS[value], conditions: 1, inner: '' };
3,356✔
237

238
    token.prev = prev;
3,356✔
239
    token.parens = state.parens;
3,356✔
240
    token.output = state.output;
3,356✔
241
    const output = (opts.capture ? '(' : '') + token.open;
3,356!
242

243
    increment('parens');
3,356✔
244
    push({ type, value, output: state.output ? '' : ONE_CHAR });
3,356✔
245
    push({ type: 'paren', extglob: true, value: advance(), output });
3,356✔
246
    extglobs.push(token);
3,356✔
247
  };
248

249
  const extglobClose = token => {
8,523✔
250
    let output = token.close + (opts.capture ? ')' : '');
3,355!
251
    let rest;
252

253
    if (token.type === 'negate') {
3,355✔
254
      let extglobStar = star;
1,568✔
255

256
      if (token.inner && token.inner.length > 1 && token.inner.includes('/')) {
1,568✔
257
        extglobStar = globstar(opts);
69✔
258
      }
259

260
      if (extglobStar !== star || eos() || /^\)+$/.test(remaining())) {
1,568✔
261
        output = token.close = `)$))${extglobStar}`;
1,068✔
262
      }
263

264
      if (
1,568✔
265
        token.inner.includes('*') &&
2,121✔
266
        (rest = remaining()) &&
267
        /^\.[^\\/.]+$/.test(rest)
268
      ) {
269
        // Any non-magical string (`.ts`) or even nested expression (`.{ts,tsx}`) can follow after the closing parenthesis.
270
        // In this case, we need to parse the string and use it in the output of the original pattern.
271
        // Suitable patterns: `/!(*.d).ts`, `/!(*.d).{ts,tsx}`, `**/!(*-dbg).@(js)`.
272
        //
273
        // Disabling the `fastpaths` option due to a problem with parsing strings as `.ts` in the pattern like `**/!(*.d).ts`.
274
        const expression = parse(rest, { ...options, fastpaths: false }).output;
28✔
275

276
        output = token.close = `)${expression})${extglobStar})`;
28✔
277
      }
278

279
      if (token.prev.type === 'bos') {
1,568✔
280
        state.negatedExtglob = true;
707✔
281
      }
282
    }
283

284
    push({ type: 'paren', extglob: true, value, output });
3,355✔
285
    decrement('parens');
3,355✔
286
  };
287

288
  /**
289
   * Fast paths
290
   */
291

292
  if (opts.fastpaths !== false && !/(^[*!]|[/()[\]{}"])/.test(input)) {
8,523✔
293
    let backslashes = false;
784✔
294

295
    let output = input.replace(
784✔
296
      REGEX_SPECIAL_CHARS_BACKREF,
297
      (m, esc, chars, first, rest, index) => {
298
        if (first === '\\') {
1,426✔
299
          backslashes = true;
57✔
300
          return m;
57✔
301
        }
302

303
        if (first === '?') {
1,369✔
304
          if (esc) {
303✔
305
            return esc + first + (rest ? QMARK.repeat(rest.length) : '');
6✔
306
          }
307
          if (index === 0) {
297✔
308
            return qmarkNoDot + (rest ? QMARK.repeat(rest.length) : '');
117✔
309
          }
310
          return QMARK.repeat(chars.length);
180✔
311
        }
312

313
        if (first === '.') {
1,066✔
314
          return DOT_LITERAL.repeat(chars.length);
141✔
315
        }
316

317
        if (first === '*') {
925✔
318
          if (esc) {
661✔
319
            return esc + first + (rest ? star : '');
112✔
320
          }
321
          return star;
549✔
322
        }
323
        return esc ? m : `\\${m}`;
264✔
324
      }
325
    );
326

327
    if (backslashes === true) {
784✔
328
      if (opts.unescape === true) {
47✔
329
        output = output.replace(/\\/g, '');
1✔
330
      } else {
331
        output = output.replace(/\\+/g, m => {
46✔
332
          return m.length % 2 === 0 ? '\\\\' : m ? '\\' : '';
54!
333
        });
334
      }
335
    }
336

337
    if (output === input && opts.contains === true) {
784!
UNCOV
338
      state.output = input;
×
UNCOV
339
      return state;
×
340
    }
341

342
    state.output = utils.wrapOutput(output, state, options);
784✔
343
    return state;
784✔
344
  }
345

346
  /**
347
   * Tokenize input until we reach end-of-string
348
   */
349

350
  while (!eos()) {
7,739✔
351
    value = advance();
54,260✔
352

353
    if (value === '\u0000') {
54,260!
354
      continue;
×
355
    }
356

357
    /**
358
     * Escaped characters
359
     */
360

361
    if (value === '\\') {
54,260✔
362
      const next = peek();
578✔
363

364
      if (next === '/' && opts.bash !== true) {
578✔
365
        continue;
6✔
366
      }
367

368
      if (next === '.' || next === ';') {
572✔
369
        continue;
24✔
370
      }
371

372
      if (!next) {
548!
UNCOV
373
        value += '\\';
×
UNCOV
374
        push({ type: 'text', value });
×
UNCOV
375
        continue;
×
376
      }
377

378
      // collapse slashes to reduce potential for exploits
379
      const match = /^\\+/.exec(remaining());
548✔
380

381
      if (match && match[0].length > 2) {
548✔
382
        const slashes = match[0].length;
3✔
383
        state.index += slashes;
3✔
384
        if (slashes % 2 !== 0) {
3!
385
          value += '\\';
3✔
386
        }
387
      }
388

389
      if (opts.unescape === true) {
548✔
390
        value = advance();
28✔
391
      } else {
392
        value += advance();
520✔
393
      }
394

395
      if (state.brackets === 0) {
548✔
396
        push({ type: 'text', value });
475✔
397
        continue;
475✔
398
      }
399
    }
400

401
    /**
402
     * If we're inside a regex character class, continue
403
     * until we reach the closing bracket.
404
     */
405

406
    if (
53,755✔
407
      state.brackets > 0 &&
62,690✔
408
      (value !== ']' || prev.value === '[' || prev.value === '[^')
409
    ) {
410
      if (opts.posix !== false && value === ':') {
5,479✔
411
        const inner = prev.value.slice(1);
733✔
412
        if (inner.includes('[')) {
733✔
413
          prev.posix = true;
703✔
414

415
          if (inner.includes(':')) {
703✔
416
            const idx = prev.value.lastIndexOf('[');
363✔
417
            const pre = prev.value.slice(0, idx);
363✔
418
            const rest = prev.value.slice(idx + 2);
363✔
419
            const posix = POSIX_REGEX_SOURCE[rest];
363✔
420
            if (posix) {
363✔
421
              prev.value = pre + posix;
350✔
422
              state.backtrack = true;
350✔
423
              advance();
350✔
424

425
              if (!bos.output && tokens.indexOf(prev) === 1) {
350✔
426
                bos.output = ONE_CHAR;
155✔
427
              }
428
              continue;
350✔
429
            }
430
          }
431
        }
432
      }
433

434
      if (
5,129✔
435
        (value === '[' && peek() !== ':') ||
11,187✔
436
        (value === '-' && peek() === ']')
437
      ) {
438
        value = `\\${value}`;
19✔
439
      }
440

441
      if (value === ']' && (prev.value === '[' || prev.value === '[^')) {
5,129!
442
        value = `\\${value}`;
6✔
443
      }
444

445
      if (opts.posix === true && value === '!' && prev.value === '[') {
5,129✔
446
        value = '^';
10✔
447
      }
448

449
      prev.value += value;
5,129✔
450
      append({ value });
5,129✔
451
      continue;
5,129✔
452
    }
453

454
    /**
455
     * If we're inside a quoted string, continue
456
     * until we reach the closing double quote.
457
     */
458

459
    if (state.quotes === 1 && value !== '"') {
48,276✔
460
      value = utils.escapeRegex(value);
228✔
461
      prev.value += value;
228✔
462
      append({ value });
228✔
463
      continue;
228✔
464
    }
465

466
    /**
467
     * Double quotes
468
     */
469

470
    if (value === '"') {
48,048✔
471
      state.quotes = state.quotes === 1 ? 0 : 1;
206✔
472
      if (opts.keepQuotes === true) {
206✔
473
        push({ type: 'text', value });
12✔
474
      }
475
      continue;
206✔
476
    }
477

478
    /**
479
     * Parentheses
480
     */
481

482
    if (value === '(') {
47,842✔
483
      increment('parens');
1,085✔
484
      push({ type: 'paren', value });
1,085✔
485
      continue;
1,085✔
486
    }
487

488
    if (value === ')') {
46,757✔
489
      if (state.parens === 0 && opts.strictBrackets === true) {
4,411✔
490
        throw new SyntaxError(syntaxError('opening', '('));
2✔
491
      }
492

493
      const extglob = extglobs[extglobs.length - 1];
4,409✔
494
      if (extglob && state.parens === extglob.parens + 1) {
4,409✔
495
        extglobClose(extglobs.pop());
3,355✔
496
        continue;
3,355✔
497
      }
498

499
      push({ type: 'paren', value, output: state.parens ? ')' : '\\)' });
1,054✔
500
      decrement('parens');
1,054✔
501
      continue;
1,054✔
502
    }
503

504
    /**
505
     * Square brackets
506
     */
507

508
    if (value === '[') {
42,346✔
509
      if (opts.nobracket === true || !remaining().includes(']')) {
1,171✔
510
        if (opts.nobracket !== true && opts.strictBrackets === true) {
18✔
511
          throw new SyntaxError(syntaxError('closing', ']'));
1✔
512
        }
513

514
        value = `\\${value}`;
17✔
515
      } else {
516
        increment('brackets');
1,153✔
517
      }
518

519
      push({ type: 'bracket', value });
1,170✔
520
      continue;
1,170✔
521
    }
522

523
    if (value === ']') {
41,175✔
524
      if (
1,159✔
525
        opts.nobracket === true ||
4,621✔
526
        (prev && prev.type === 'bracket' && prev.value.length === 1)
527
      ) {
528
        push({ type: 'text', value, output: `\\${value}` });
3✔
529
        continue;
3✔
530
      }
531

532
      if (state.brackets === 0) {
1,156✔
533
        if (opts.strictBrackets === true) {
6✔
534
          throw new SyntaxError(syntaxError('opening', '['));
1✔
535
        }
536

537
        push({ type: 'text', value, output: `\\${value}` });
5✔
538
        continue;
5✔
539
      }
540

541
      decrement('brackets');
1,150✔
542

543
      const prevValue = prev.value.slice(1);
1,150✔
544
      if (
1,150✔
545
        prev.posix !== true &&
2,207✔
546
        prevValue[0] === '^' &&
547
        !prevValue.includes('/')
548
      ) {
549
        value = `/${value}`;
156✔
550
      }
551

552
      prev.value += value;
1,150✔
553
      append({ value });
1,150✔
554

555
      // when literal brackets are explicitly disabled
556
      // assume we should match with a regex character class
557
      if (opts.literalBrackets === false || utils.hasRegexChars(prevValue)) {
1,150✔
558
        continue;
961✔
559
      }
560

561
      const escaped = utils.escapeRegex(prev.value);
189✔
562
      state.output = state.output.slice(0, -prev.value.length);
189✔
563

564
      // when literal brackets are explicitly enabled
565
      // assume we should escape the brackets to match literal characters
566
      if (opts.literalBrackets === true) {
189!
UNCOV
567
        state.output += escaped;
×
UNCOV
568
        prev.value = escaped;
×
UNCOV
569
        continue;
×
570
      }
571

572
      // when the user specifies nothing, try to match both
573
      prev.value = `(${capture}${escaped}|${prev.value})`;
189✔
574
      state.output += prev.value;
189✔
575
      continue;
189✔
576
    }
577

578
    /**
579
     * Braces
580
     */
581

582
    if (value === '{' && opts.nobrace !== true) {
40,016✔
583
      increment('braces');
205✔
584

585
      const open = {
205✔
586
        type: 'brace',
587
        value,
588
        output: '(',
589
        outputIndex: state.output.length,
590
        tokensIndex: state.tokens.length
591
      };
592

593
      braces.push(open);
205✔
594
      push(open);
205✔
595
      continue;
205✔
596
    }
597

598
    if (value === '}') {
39,811✔
599
      const brace = braces[braces.length - 1];
216✔
600

601
      if (opts.nobrace === true || !brace) {
216✔
602
        push({ type: 'text', value, output: value });
11✔
603
        continue;
11✔
604
      }
605

606
      let output = ')';
205✔
607

608
      if (brace.dots === true) {
205✔
609
        const arr = tokens.slice();
23✔
610
        const range = [];
23✔
611

612
        for (let i = arr.length - 1; i >= 0; i--) {
23✔
613
          tokens.pop();
92✔
614
          if (arr[i].type === 'brace') {
92✔
615
            break;
23✔
616
          }
617
          if (arr[i].type !== 'dots') {
69✔
618
            range.unshift(arr[i].value);
46✔
619
          }
620
        }
621

622
        output = expandRange(range, opts);
23✔
623
        state.backtrack = true;
23✔
624
      }
625

626
      if (brace.comma !== true && brace.dots !== true) {
205✔
627
        const out = state.output.slice(0, brace.outputIndex);
3✔
628
        const toks = state.tokens.slice(brace.tokensIndex);
3✔
629
        brace.value = brace.output = '\\{';
3✔
630
        value = output = '\\}';
3✔
631
        state.output = out;
3✔
632
        for (const t of toks) {
3✔
633
          state.output += t.output || t.value;
8✔
634
        }
635
      }
636

637
      push({ type: 'brace', value, output });
205✔
638
      decrement('braces');
205✔
639
      braces.pop();
205✔
640
      continue;
205✔
641
    }
642

643
    /**
644
     * Pipes
645
     */
646

647
    if (value === '|') {
39,595✔
648
      if (extglobs.length > 0) {
1,510✔
649
        extglobs[extglobs.length - 1].conditions++;
942✔
650
      }
651
      push({ type: 'text', value });
1,510✔
652
      continue;
1,510✔
653
    }
654

655
    /**
656
     * Commas
657
     */
658

659
    if (value === ',') {
38,085✔
660
      let output = value;
274✔
661

662
      const brace = braces[braces.length - 1];
274✔
663
      if (brace && stack[stack.length - 1] === 'braces') {
274✔
664
        brace.comma = true;
231✔
665
        output = '|';
231✔
666
      }
667

668
      push({ type: 'comma', value, output });
274✔
669
      continue;
274✔
670
    }
671

672
    /**
673
     * Slashes
674
     */
675

676
    if (value === '/') {
37,811✔
677
      // if the beginning of the glob is "./", advance the start
678
      // to the current index, and don't add the "./" characters
679
      // to the state. This greatly simplifies lookbehinds when
680
      // checking for BOS characters like "!" and "." (not "./")
681
      if (prev.type === 'dot' && state.index === state.start + 1) {
6,597!
UNCOV
682
        state.start = state.index + 1;
×
UNCOV
683
        state.consumed = '';
×
UNCOV
684
        state.output = '';
×
UNCOV
685
        tokens.pop();
×
UNCOV
686
        prev = bos; // reset "prev" to the first token
×
UNCOV
687
        continue;
×
688
      }
689

690
      push({ type: 'slash', value, output: SLASH_LITERAL });
6,597✔
691
      continue;
6,597✔
692
    }
693

694
    /**
695
     * Dots
696
     */
697

698
    if (value === '.') {
31,214✔
699
      if (state.braces > 0 && prev.type === 'dot') {
2,513✔
700
        if (prev.value === '.') prev.output = DOT_LITERAL;
23!
701
        const brace = braces[braces.length - 1];
23✔
702
        prev.type = 'dots';
23✔
703
        prev.output += value;
23✔
704
        prev.value += value;
23✔
705
        brace.dots = true;
23✔
706
        continue;
23✔
707
      }
708

709
      if (
2,490✔
710
        state.braces + state.parens === 0 &&
6,294✔
711
        prev.type !== 'bos' &&
712
        prev.type !== 'slash'
713
      ) {
714
        push({ type: 'text', value, output: DOT_LITERAL });
1,494✔
715
        continue;
1,494✔
716
      }
717

718
      push({ type: 'dot', value, output: DOT_LITERAL });
996✔
719
      continue;
996✔
720
    }
721

722
    /**
723
     * Question marks
724
     */
725

726
    if (value === '?') {
28,701✔
727
      const isGroup = prev && prev.value === '(';
450✔
728
      if (
450✔
729
        !isGroup &&
1,447✔
730
        opts.noextglob !== true &&
731
        peek() === '(' &&
732
        peek(2) !== '?'
733
      ) {
734
        extglobOpen('qmark', value);
130✔
735
        continue;
130✔
736
      }
737

738
      if (prev && prev.type === 'paren') {
320✔
739
        const next = peek();
27✔
740
        let output = value;
27✔
741

742
        if (
27✔
743
          (prev.value === '(' && !/[!=<:]/.test(next)) ||
71✔
744
          (next === '<' && !/<([!=]|\w+>)/.test(remaining()))
745
        ) {
746
          output = `\\${value}`;
2✔
747
        }
748

749
        push({ type: 'text', value, output });
27✔
750
        continue;
27✔
751
      }
752

753
      if (opts.dot !== true && (prev.type === 'slash' || prev.type === 'bos')) {
293✔
754
        push({ type: 'qmark', value, output: QMARK_NO_DOT });
129✔
755
        continue;
129✔
756
      }
757

758
      push({ type: 'qmark', value, output: QMARK });
164✔
759
      continue;
164✔
760
    }
761

762
    /**
763
     * Exclamation
764
     */
765

766
    if (value === '!') {
28,251✔
767
      if (opts.noextglob !== true && peek() === '(') {
1,919✔
768
        if (peek(2) !== '?' || !/[!=<:]/.test(peek(3))) {
1,574✔
769
          extglobOpen('negate', value);
1,569✔
770
          continue;
1,569✔
771
        }
772
      }
773

774
      if (opts.nonegate !== true && state.index === 0) {
350✔
775
        negate();
327✔
776
        continue;
327✔
777
      }
778
    }
779

780
    /**
781
     * Plus
782
     */
783

784
    if (value === '+') {
26,355✔
785
      if (opts.noextglob !== true && peek() === '(' && peek(2) !== '?') {
718✔
786
        extglobOpen('plus', value);
461✔
787
        continue;
461✔
788
      }
789

790
      if ((prev && prev.value === '(') || opts.regex === false) {
257✔
791
        push({ type: 'plus', value, output: PLUS_LITERAL });
2✔
792
        continue;
2✔
793
      }
794

795
      if (
255✔
796
        (prev &&
930✔
797
          (prev.type === 'bracket' ||
798
            prev.type === 'paren' ||
799
            prev.type === 'brace')) ||
800
        state.parens > 0
801
      ) {
802
        push({ type: 'plus', value });
217✔
803
        continue;
217✔
804
      }
805

806
      push({ type: 'plus', value: PLUS_LITERAL });
38✔
807
      continue;
38✔
808
    }
809

810
    /**
811
     * Plain text
812
     */
813

814
    if (value === '@') {
25,637✔
815
      if (opts.noextglob !== true && peek() === '(' && peek(2) !== '?') {
563✔
816
        push({ type: 'at', extglob: true, value, output: '' });
558✔
817
        continue;
558✔
818
      }
819

820
      push({ type: 'text', value });
5✔
821
      continue;
5✔
822
    }
823

824
    /**
825
     * Plain text
826
     */
827

828
    if (value !== '*') {
25,074✔
829
      if (value === '$' || value === '^') {
11,622✔
830
        value = `\\${value}`;
75✔
831
      }
832

833
      const match = REGEX_NON_SPECIAL_CHARS.exec(remaining());
11,622✔
834
      if (match) {
11,622✔
835
        value += match[0];
3,009✔
836
        state.index += match[0].length;
3,009✔
837
      }
838

839
      push({ type: 'text', value });
11,622✔
840
      continue;
11,622✔
841
    }
842

843
    /**
844
     * Stars
845
     */
846

847
    if (prev && (prev.type === 'globstar' || prev.star === true)) {
13,452✔
848
      prev.type = 'star';
97✔
849
      prev.star = true;
97✔
850
      prev.value += value;
97✔
851
      prev.output = star;
97✔
852
      state.backtrack = true;
97✔
853
      state.globstar = true;
97✔
854
      consume(value);
97✔
855
      continue;
97✔
856
    }
857

858
    let rest = remaining();
13,355✔
859
    if (opts.noextglob !== true && /^\([^?]/.test(rest)) {
13,355✔
860
      extglobOpen('star', value);
1,196✔
861
      continue;
1,196✔
862
    }
863

864
    if (prev.type === 'star') {
12,159✔
865
      if (opts.noglobstar === true) {
2,516✔
866
        consume(value);
3✔
867
        continue;
3✔
868
      }
869

870
      const prior = prev.prev;
2,513✔
871
      const before = prior.prev;
2,513✔
872
      const isStart = prior.type === 'slash' || prior.type === 'bos';
2,513✔
873
      const afterStar =
874
        before && (before.type === 'star' || before.type === 'globstar');
2,513✔
875

876
      if (opts.bash === true && (!isStart || (rest[0] && rest[0] !== '/'))) {
2,513✔
877
        push({ type: 'star', value, output: '' });
3✔
878
        continue;
3✔
879
      }
880

881
      const isBrace =
882
        state.braces > 0 && (prior.type === 'comma' || prior.type === 'brace');
2,510✔
883
      const isExtglob =
884
        extglobs.length && (prior.type === 'pipe' || prior.type === 'paren');
2,510✔
885
      if (!isStart && prior.type !== 'paren' && !isBrace && !isExtglob) {
2,510✔
886
        push({ type: 'star', value, output: '' });
346✔
887
        continue;
346✔
888
      }
889

890
      // strip consecutive `/**/`
891
      while (rest.slice(0, 3) === '/**') {
2,164✔
892
        const after = input[state.index + 4];
488✔
893
        if (after && after !== '/') {
488✔
894
          break;
216✔
895
        }
896
        rest = rest.slice(3);
272✔
897
        consume('/**', 3);
272✔
898
      }
899

900
      if (prior.type === 'bos' && eos()) {
2,164✔
901
        prev.type = 'globstar';
24✔
902
        prev.value += value;
24✔
903
        prev.output = globstar(opts);
24✔
904
        state.output = prev.output;
24✔
905
        state.globstar = true;
24✔
906
        consume(value);
24✔
907
        continue;
24✔
908
      }
909

910
      if (
2,140✔
911
        prior.type === 'slash' &&
5,591✔
912
        prior.prev.type !== 'bos' &&
913
        !afterStar &&
914
        eos()
915
      ) {
916
        state.output = state.output.slice(
371✔
917
          0,
918
          -(prior.output + prev.output).length
919
        );
920
        prior.output = `(?:${prior.output}`;
371✔
921

922
        prev.type = 'globstar';
371✔
923
        prev.output = globstar(opts) + (opts.strictSlashes ? ')' : '|$)');
371✔
924
        prev.value += value;
371✔
925
        state.globstar = true;
371✔
926
        state.output += prior.output + prev.output;
371✔
927
        consume(value);
371✔
928
        continue;
371✔
929
      }
930

931
      if (
1,769✔
932
        prior.type === 'slash' &&
3,646✔
933
        prior.prev.type !== 'bos' &&
934
        rest[0] === '/'
935
      ) {
936
        const end = rest[1] !== void 0 ? '|$' : '';
495✔
937

938
        state.output = state.output.slice(
495✔
939
          0,
940
          -(prior.output + prev.output).length
941
        );
942
        prior.output = `(?:${prior.output}`;
495✔
943

944
        prev.type = 'globstar';
495✔
945
        prev.output = `${globstar(opts)}${SLASH_LITERAL}|${SLASH_LITERAL}${end})`;
495✔
946
        prev.value += value;
495✔
947

948
        state.output += prior.output + prev.output;
495✔
949
        state.globstar = true;
495✔
950

951
        consume(value + advance());
495✔
952

953
        push({ type: 'slash', value: '/', output: '' });
495✔
954
        continue;
495✔
955
      }
956

957
      if (prior.type === 'bos' && rest[0] === '/') {
1,274✔
958
        prev.type = 'globstar';
746✔
959
        prev.value += value;
746✔
960
        prev.output = `(?:^|${SLASH_LITERAL}|${globstar(opts)}${SLASH_LITERAL})`;
746✔
961
        state.output = prev.output;
746✔
962
        state.globstar = true;
746✔
963
        consume(value + advance());
746✔
964
        push({ type: 'slash', value: '/', output: '' });
746✔
965
        continue;
746✔
966
      }
967

968
      // remove single star from output
969
      state.output = state.output.slice(0, -prev.output.length);
528✔
970

971
      // reset previous token to globstar
972
      prev.type = 'globstar';
528✔
973
      prev.output = globstar(opts);
528✔
974
      prev.value += value;
528✔
975

976
      // reset output with globstar
977
      state.output += prev.output;
528✔
978
      state.globstar = true;
528✔
979
      consume(value);
528✔
980
      continue;
528✔
981
    }
982

983
    const token = { type: 'star', value, output: star };
9,643✔
984

985
    if (opts.bash === true) {
9,643✔
986
      token.output = '.*?';
625✔
987
      if (prev.type === 'bos' || prev.type === 'slash') {
625✔
988
        token.output = nodot + token.output;
269✔
989
      }
990
      push(token);
625✔
991
      continue;
625✔
992
    }
993

994
    if (
9,018✔
995
      prev &&
27,585✔
996
      (prev.type === 'bracket' || prev.type === 'paren') &&
997
      opts.regex === true
998
    ) {
999
      token.output = value;
18✔
1000
      push(token);
18✔
1001
      continue;
18✔
1002
    }
1003

1004
    if (
9,000✔
1005
      state.index === state.start ||
18,042✔
1006
      prev.type === 'slash' ||
1007
      prev.type === 'dot'
1008
    ) {
1009
      if (prev.type === 'dot') {
7,185✔
1010
        state.output += NO_DOT_SLASH;
355✔
1011
        prev.output += NO_DOT_SLASH;
355✔
1012
      } else if (opts.dot === true) {
6,830✔
1013
        state.output += NO_DOTS_SLASH;
1,072✔
1014
        prev.output += NO_DOTS_SLASH;
1,072✔
1015
      } else {
1016
        state.output += nodot;
5,758✔
1017
        prev.output += nodot;
5,758✔
1018
      }
1019

1020
      if (peek() !== '*') {
7,185✔
1021
        state.output += ONE_CHAR;
4,939✔
1022
        prev.output += ONE_CHAR;
4,939✔
1023
      }
1024
    }
1025

1026
    push(token);
9,000✔
1027
  }
1028

1029
  while (state.brackets > 0) {
7,735✔
1030
    if (opts.strictBrackets === true)
3!
NEW
UNCOV
1031
      throw new SyntaxError(syntaxError('closing', ']'));
×
1032
    state.output = utils.escapeLast(state.output, '[');
3✔
1033
    decrement('brackets');
3✔
1034
  }
1035

1036
  while (state.parens > 0) {
7,735✔
1037
    if (opts.strictBrackets === true)
35✔
1038
      throw new SyntaxError(syntaxError('closing', ')'));
2✔
1039
    state.output = utils.escapeLast(state.output, '(');
33✔
1040
    decrement('parens');
33✔
1041
  }
1042

1043
  while (state.braces > 0) {
7,733✔
NEW
UNCOV
1044
    if (opts.strictBrackets === true)
×
NEW
UNCOV
1045
      throw new SyntaxError(syntaxError('closing', '}'));
×
UNCOV
1046
    state.output = utils.escapeLast(state.output, '{');
×
UNCOV
1047
    decrement('braces');
×
1048
  }
1049

1050
  if (
7,733✔
1051
    opts.strictSlashes !== true &&
20,144✔
1052
    (prev.type === 'star' || prev.type === 'bracket')
1053
  ) {
1054
    push({ type: 'maybe_slash', value: '', output: `${SLASH_LITERAL}?` });
1,770✔
1055
  }
1056

1057
  // rebuild the output if we had to backtrack at any point
1058
  if (state.backtrack === true) {
7,733✔
1059
    state.output = '';
276✔
1060

1061
    for (const token of state.tokens) {
276✔
1062
      state.output += token.output != null ? token.output : token.value;
994✔
1063

1064
      if (token.suffix) {
994!
UNCOV
1065
        state.output += token.suffix;
×
1066
      }
1067
    }
1068
  }
1069

1070
  return state;
7,733✔
1071
};
1072

1073
/**
1074
 * Fast paths for creating regular expressions for common glob patterns.
1075
 * This can significantly speed up processing and has very little downside
1076
 * impact when none of the fast paths match.
1077
 */
1078

1079
parse.fastpaths = (input, options) => {
1✔
1080
  const opts = { ...options };
3,548✔
1081
  const max =
1082
    typeof opts.maxLength === 'number'
3,548!
1083
      ? Math.min(MAX_LENGTH, opts.maxLength)
1084
      : MAX_LENGTH;
1085
  const len = input.length;
3,548✔
1086
  if (len > max) {
3,548✔
1087
    throw new SyntaxError(
1✔
1088
      `Input length: ${len}, exceeds maximum allowed length: ${max}`
1089
    );
1090
  }
1091

1092
  input = REPLACEMENTS[input] || input;
3,547✔
1093

1094
  // create constants based on platform, for windows or posix
1095
  const {
1096
    DOT_LITERAL,
1097
    SLASH_LITERAL,
1098
    ONE_CHAR,
1099
    DOTS_SLASH,
1100
    NO_DOT,
1101
    NO_DOTS,
1102
    NO_DOTS_SLASH,
1103
    STAR,
1104
    START_ANCHOR
1105
  } = constants.globChars(opts.windows);
3,547✔
1106

1107
  const nodot = opts.dot ? NO_DOTS : NO_DOT;
3,547✔
1108
  const slashDot = opts.dot ? NO_DOTS_SLASH : NO_DOT;
3,547✔
1109
  const capture = opts.capture ? '' : '?:';
3,547!
1110
  const state = { negated: false, prefix: '' };
3,547✔
1111
  let star = opts.bash === true ? '.*?' : STAR;
3,547✔
1112

1113
  if (opts.capture) {
3,547!
UNCOV
1114
    star = `(${star})`;
×
1115
  }
1116

1117
  const globstar = opts => {
3,547✔
1118
    if (opts.noglobstar === true) return star;
302✔
1119
    return `(${capture}(?:(?!${START_ANCHOR}${opts.dot ? DOTS_SLASH : DOT_LITERAL}).)*?)`;
301✔
1120
  };
1121

1122
  const create = str => {
3,547✔
1123
    switch (str) {
3,784!
1124
      case '*':
1125
        return `${nodot}${ONE_CHAR}${star}`;
213✔
1126

1127
      case '.*':
1128
        return `${DOT_LITERAL}${ONE_CHAR}${star}`;
12✔
1129

1130
      case '*.*':
1131
        return `${nodot}${star}${DOT_LITERAL}${ONE_CHAR}${star}`;
10✔
1132

1133
      case '*/*':
1134
        return `${nodot}${star}${SLASH_LITERAL}${ONE_CHAR}${slashDot}${star}`;
159✔
1135

1136
      case '**':
1137
        return nodot + globstar(opts);
200✔
1138

1139
      case '**/*':
1140
        return `(?:${nodot}${globstar(opts)}${SLASH_LITERAL})?${slashDot}${ONE_CHAR}${star}`;
95✔
1141

1142
      case '**/*.*':
UNCOV
1143
        return `(?:${nodot}${globstar(opts)}${SLASH_LITERAL})?${slashDot}${star}${DOT_LITERAL}${ONE_CHAR}${star}`;
×
1144

1145
      case '**/.*':
1146
        return `(?:${nodot}${globstar(opts)}${SLASH_LITERAL})?${DOT_LITERAL}${ONE_CHAR}${star}`;
7✔
1147

1148
      default: {
1149
        const match = /^(.*?)\.(\w+)$/.exec(str);
3,088✔
1150
        if (!match) return;
3,088✔
1151

1152
        const source = create(match[1]);
237✔
1153
        if (!source) return;
237✔
1154

1155
        return source + DOT_LITERAL + match[2];
125✔
1156
      }
1157
    }
1158
  };
1159

1160
  const output = utils.removePrefix(input, state);
3,547✔
1161
  let source = create(output);
3,547✔
1162

1163
  if (source && opts.strictSlashes !== true) {
3,547✔
1164
    source += `${SLASH_LITERAL}?`;
618✔
1165
  }
1166

1167
  return source;
3,547✔
1168
};
1169

1170
module.exports = parse;
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc