Skip to content

Commit 37f6df6

Browse files
committed
Prevent dots only at the start of repeat patterns
A pattern like `+(?)` will note that the `?` matches the start of the pattern, and prevent it from matching a dot. However, this is not "anything other than a dot, repeating", but rather "repeating, where the first repetition doesn't start with a dot". With this change, repetitive extglob patterns in the start position are expanded such that the first instance of the pattern may not start with a dot, but any subsequent repetitions may begin with a dot. Fix: #211
1 parent f1b11e7 commit 37f6df6

File tree

6 files changed

+1037
-161
lines changed

6 files changed

+1037
-161
lines changed

src/ast.ts

Lines changed: 55 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// parse a single path portion
22

3-
import { MinimatchOptions, MMRegExp } from './index.js'
43
import { parseClass } from './brace-expressions.js'
4+
import { MinimatchOptions, MMRegExp } from './index.js'
55
import { unescape } from './unescape.js'
66

77
// classes [] are handled by the parseClass method
@@ -50,7 +50,7 @@ const isExtglobType = (c: string): c is ExtglobType =>
5050
// entire string, or just a single path portion, to prevent dots
5151
// and/or traversal patterns, when needed.
5252
// Exts don't need the ^ or / bit, because the root binds that already.
53-
const startNoTraversal = '(?!\\.\\.?(?:$|/))'
53+
const startNoTraversal = '(?!(?:^|/)\\.\\.?(?:$|/))'
5454
const startNoDot = '(?!\\.)'
5555

5656
// characters that indicate a start of pattern needs the "no dots" bit,
@@ -467,12 +467,10 @@ export class AST {
467467
// - Since the start for a join is eg /(?!\.) and the start for a part
468468
// is ^(?!\.), we can just prepend (?!\.) to the pattern (either root
469469
// or start or whatever) and prepend ^ or / at the Regexp construction.
470-
toRegExpSource(): [
471-
re: string,
472-
body: string,
473-
hasMagic: boolean,
474-
uflag: boolean
475-
] {
470+
toRegExpSource(
471+
allowDot?: boolean
472+
): [re: string, body: string, hasMagic: boolean, uflag: boolean] {
473+
const dot = allowDot ?? !!this.#options.dot
476474
if (this.#root === this) this.#fillNegs()
477475
if (!this.type) {
478476
const noEmpty = this.isStart() && this.isEnd()
@@ -481,7 +479,7 @@ export class AST {
481479
const [re, _, hasMagic, uflag] =
482480
typeof p === 'string'
483481
? AST.#parseGlob(p, this.#hasMagic, noEmpty)
484-
: p.toRegExpSource()
482+
: p.toRegExpSource(allowDot)
485483
this.#hasMagic = this.#hasMagic || hasMagic
486484
this.#uflag = this.#uflag || uflag
487485
return re
@@ -504,14 +502,14 @@ export class AST {
504502
// and prevent that.
505503
const needNoTrav =
506504
// dots are allowed, and the pattern starts with [ or .
507-
(this.#options.dot && aps.has(src.charAt(0))) ||
505+
(dot && aps.has(src.charAt(0))) ||
508506
// the pattern starts with \., and then [ or .
509507
(src.startsWith('\\.') && aps.has(src.charAt(2))) ||
510508
// the pattern starts with \.\., and then [ or .
511509
(src.startsWith('\\.\\.') && aps.has(src.charAt(4)))
512510
// no need to prevent dots if it can't match a dot, or if a
513511
// sub-pattern will be preventing it anyway.
514-
const needNoDot = !this.#options.dot && aps.has(src.charAt(0))
512+
const needNoDot = !dot && !allowDot && aps.has(src.charAt(0))
515513

516514
start = needNoTrav ? startNoTraversal : needNoDot ? startNoDot : ''
517515
}
@@ -536,23 +534,15 @@ export class AST {
536534
]
537535
}
538536

537+
// We need to calculate the body *twice* if it's a repeat pattern
538+
// at the start, once in nodot mode, then again in dot mode, so a
539+
// pattern like *(?) can match 'x.y'
540+
541+
const repeated = this.type === '*' || this.type === '+'
539542
// some kind of extglob
540543
const start = this.type === '!' ? '(?:(?!(?:' : '(?:'
541-
const body = this.#parts
542-
.map(p => {
543-
// extglob ASTs should only contain parent ASTs
544-
/* c8 ignore start */
545-
if (typeof p === 'string') {
546-
throw new Error('string type in extglob ast??')
547-
}
548-
/* c8 ignore stop */
549-
// can ignore hasMagic, because extglobs are already always magic
550-
const [re, _, _hasMagic, uflag] = p.toRegExpSource()
551-
this.#uflag = this.#uflag || uflag
552-
return re
553-
})
554-
.filter(p => !(this.isStart() && this.isEnd()) || !!p)
555-
.join('|')
544+
let body = this.#partsToRegExp(dot)
545+
556546
if (this.isStart() && this.isEnd() && !body && this.type !== '!') {
557547
// invalid extglob, has to at least be *something* present, if it's
558548
// the entire path portion.
@@ -562,21 +552,39 @@ export class AST {
562552
this.#hasMagic = undefined
563553
return [s, unescape(this.toString()), false, false]
564554
}
555+
556+
// XXX abstract out this map method
557+
let bodyDotAllowed =
558+
!repeated || allowDot || dot || !startNoDot
559+
? ''
560+
: this.#partsToRegExp(true)
561+
if (bodyDotAllowed === body) {
562+
bodyDotAllowed = ''
563+
}
564+
if (bodyDotAllowed) {
565+
body = `(?:${body})(?:${bodyDotAllowed})*?`
566+
}
567+
565568
// an empty !() is exactly equivalent to a starNoEmpty
566569
let final = ''
567570
if (this.type === '!' && this.#emptyExt) {
568-
final =
569-
(this.isStart() && !this.#options.dot ? startNoDot : '') + starNoEmpty
571+
final = (this.isStart() && !dot ? startNoDot : '') + starNoEmpty
570572
} else {
571573
const close =
572574
this.type === '!'
573575
? // !() must match something,but !(x) can match ''
574576
'))' +
575-
(this.isStart() && !this.#options.dot ? startNoDot : '') +
577+
(this.isStart() && !dot && !allowDot ? startNoDot : '') +
576578
star +
577579
')'
578580
: this.type === '@'
579581
? ')'
582+
: this.type === '?'
583+
? ')?'
584+
: this.type === '+' && bodyDotAllowed
585+
? ')'
586+
: this.type === '*' && bodyDotAllowed
587+
? `)?`
580588
: `)${this.type}`
581589
final = start + body + close
582590
}
@@ -588,6 +596,24 @@ export class AST {
588596
]
589597
}
590598

599+
#partsToRegExp(dot: boolean) {
600+
return this.#parts
601+
.map(p => {
602+
// extglob ASTs should only contain parent ASTs
603+
/* c8 ignore start */
604+
if (typeof p === 'string') {
605+
throw new Error('string type in extglob ast??')
606+
}
607+
/* c8 ignore stop */
608+
// can ignore hasMagic, because extglobs are already always magic
609+
const [re, _, _hasMagic, uflag] = p.toRegExpSource(dot)
610+
this.#uflag = this.#uflag || uflag
611+
return re
612+
})
613+
.filter(p => !(this.isStart() && this.isEnd()) || !!p)
614+
.join('|')
615+
}
616+
591617
static #parseGlob(
592618
glob: string,
593619
hasMagic: boolean | undefined,

0 commit comments

Comments
 (0)