Skip to content

Commit

Permalink
Prevent dots only at the start of repeat patterns
Browse files Browse the repository at this point in the history
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
  • Loading branch information
isaacs committed Jun 23, 2023
1 parent f1b11e7 commit 37f6df6
Show file tree
Hide file tree
Showing 6 changed files with 1,037 additions and 161 deletions.
84 changes: 55 additions & 29 deletions src/ast.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// parse a single path portion

import { MinimatchOptions, MMRegExp } from './index.js'
import { parseClass } from './brace-expressions.js'
import { MinimatchOptions, MMRegExp } from './index.js'
import { unescape } from './unescape.js'

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

// characters that indicate a start of pattern needs the "no dots" bit,
Expand Down Expand Up @@ -467,12 +467,10 @@ export class AST {
// - Since the start for a join is eg /(?!\.) and the start for a part
// is ^(?!\.), we can just prepend (?!\.) to the pattern (either root
// or start or whatever) and prepend ^ or / at the Regexp construction.
toRegExpSource(): [
re: string,
body: string,
hasMagic: boolean,
uflag: boolean
] {
toRegExpSource(
allowDot?: boolean
): [re: string, body: string, hasMagic: boolean, uflag: boolean] {
const dot = allowDot ?? !!this.#options.dot
if (this.#root === this) this.#fillNegs()
if (!this.type) {
const noEmpty = this.isStart() && this.isEnd()
Expand All @@ -481,7 +479,7 @@ export class AST {
const [re, _, hasMagic, uflag] =
typeof p === 'string'
? AST.#parseGlob(p, this.#hasMagic, noEmpty)
: p.toRegExpSource()
: p.toRegExpSource(allowDot)
this.#hasMagic = this.#hasMagic || hasMagic
this.#uflag = this.#uflag || uflag
return re
Expand All @@ -504,14 +502,14 @@ export class AST {
// and prevent that.
const needNoTrav =
// dots are allowed, and the pattern starts with [ or .
(this.#options.dot && aps.has(src.charAt(0))) ||
(dot && aps.has(src.charAt(0))) ||
// the pattern starts with \., and then [ or .
(src.startsWith('\\.') && aps.has(src.charAt(2))) ||
// the pattern starts with \.\., and then [ or .
(src.startsWith('\\.\\.') && aps.has(src.charAt(4)))
// no need to prevent dots if it can't match a dot, or if a
// sub-pattern will be preventing it anyway.
const needNoDot = !this.#options.dot && aps.has(src.charAt(0))
const needNoDot = !dot && !allowDot && aps.has(src.charAt(0))

start = needNoTrav ? startNoTraversal : needNoDot ? startNoDot : ''
}
Expand All @@ -536,23 +534,15 @@ export class AST {
]
}

// We need to calculate the body *twice* if it's a repeat pattern
// at the start, once in nodot mode, then again in dot mode, so a
// pattern like *(?) can match 'x.y'

const repeated = this.type === '*' || this.type === '+'
// some kind of extglob
const start = this.type === '!' ? '(?:(?!(?:' : '(?:'
const body = this.#parts
.map(p => {
// extglob ASTs should only contain parent ASTs
/* c8 ignore start */
if (typeof p === 'string') {
throw new Error('string type in extglob ast??')
}
/* c8 ignore stop */
// can ignore hasMagic, because extglobs are already always magic
const [re, _, _hasMagic, uflag] = p.toRegExpSource()
this.#uflag = this.#uflag || uflag
return re
})
.filter(p => !(this.isStart() && this.isEnd()) || !!p)
.join('|')
let body = this.#partsToRegExp(dot)

if (this.isStart() && this.isEnd() && !body && this.type !== '!') {
// invalid extglob, has to at least be *something* present, if it's
// the entire path portion.
Expand All @@ -562,21 +552,39 @@ export class AST {
this.#hasMagic = undefined
return [s, unescape(this.toString()), false, false]
}

// XXX abstract out this map method
let bodyDotAllowed =
!repeated || allowDot || dot || !startNoDot
? ''
: this.#partsToRegExp(true)
if (bodyDotAllowed === body) {
bodyDotAllowed = ''
}
if (bodyDotAllowed) {
body = `(?:${body})(?:${bodyDotAllowed})*?`
}

// an empty !() is exactly equivalent to a starNoEmpty
let final = ''
if (this.type === '!' && this.#emptyExt) {
final =
(this.isStart() && !this.#options.dot ? startNoDot : '') + starNoEmpty
final = (this.isStart() && !dot ? startNoDot : '') + starNoEmpty
} else {
const close =
this.type === '!'
? // !() must match something,but !(x) can match ''
'))' +
(this.isStart() && !this.#options.dot ? startNoDot : '') +
(this.isStart() && !dot && !allowDot ? startNoDot : '') +
star +
')'
: this.type === '@'
? ')'
: this.type === '?'
? ')?'
: this.type === '+' && bodyDotAllowed
? ')'
: this.type === '*' && bodyDotAllowed
? `)?`
: `)${this.type}`
final = start + body + close
}
Expand All @@ -588,6 +596,24 @@ export class AST {
]
}

#partsToRegExp(dot: boolean) {
return this.#parts
.map(p => {
// extglob ASTs should only contain parent ASTs
/* c8 ignore start */
if (typeof p === 'string') {
throw new Error('string type in extglob ast??')
}
/* c8 ignore stop */
// can ignore hasMagic, because extglobs are already always magic
const [re, _, _hasMagic, uflag] = p.toRegExpSource(dot)
this.#uflag = this.#uflag || uflag
return re
})
.filter(p => !(this.isStart() && this.isEnd()) || !!p)
.join('|')
}

static #parseGlob(
glob: string,
hasMagic: boolean | undefined,
Expand Down
Loading

0 comments on commit 37f6df6

Please sign in to comment.