@@ -10,7 +10,7 @@ const isExtglobType = (c) => types.has(c);
1010// entire string, or just a single path portion, to prevent dots
1111// and/or traversal patterns, when needed.
1212// Exts don't need the ^ or / bit, because the root binds that already.
13- const startNoTraversal = '(?!\\.\\.?(?:$|/))' ;
13+ const startNoTraversal = '(?!(?:^|/) \\.\\.?(?:$|/))' ;
1414const startNoDot = '(?!\\.)' ;
1515// characters that indicate a start of pattern needs the "no dots" bit,
1616// because a dot *might* be matched. ( is not in the list, because in
@@ -407,7 +407,8 @@ class AST {
407407 // - Since the start for a join is eg /(?!\.) and the start for a part
408408 // is ^(?!\.), we can just prepend (?!\.) to the pattern (either root
409409 // or start or whatever) and prepend ^ or / at the Regexp construction.
410- toRegExpSource ( ) {
410+ toRegExpSource ( allowDot ) {
411+ const dot = allowDot ?? ! ! this . #options. dot ;
411412 if ( this . #root === this )
412413 this . #fillNegs( ) ;
413414 if ( ! this . type ) {
@@ -416,7 +417,7 @@ class AST {
416417 . map ( p => {
417418 const [ re , _ , hasMagic , uflag ] = typeof p === 'string'
418419 ? AST . #parseGlob( p , this . #hasMagic, noEmpty )
419- : p . toRegExpSource ( ) ;
420+ : p . toRegExpSource ( allowDot ) ;
420421 this . #hasMagic = this . #hasMagic || hasMagic ;
421422 this . #uflag = this . #uflag || uflag ;
422423 return re ;
@@ -436,14 +437,14 @@ class AST {
436437 // and prevent that.
437438 const needNoTrav =
438439 // dots are allowed, and the pattern starts with [ or .
439- ( this . #options . dot && aps . has ( src . charAt ( 0 ) ) ) ||
440+ ( dot && aps . has ( src . charAt ( 0 ) ) ) ||
440441 // the pattern starts with \., and then [ or .
441442 ( src . startsWith ( '\\.' ) && aps . has ( src . charAt ( 2 ) ) ) ||
442443 // the pattern starts with \.\., and then [ or .
443444 ( src . startsWith ( '\\.\\.' ) && aps . has ( src . charAt ( 4 ) ) ) ;
444445 // no need to prevent dots if it can't match a dot, or if a
445446 // sub-pattern will be preventing it anyway.
446- const needNoDot = ! this . #options . dot && aps . has ( src . charAt ( 0 ) ) ;
447+ const needNoDot = ! dot && ! allowDot && aps . has ( src . charAt ( 0 ) ) ;
447448 start = needNoTrav ? startNoTraversal : needNoDot ? startNoDot : '' ;
448449 }
449450 }
@@ -463,23 +464,13 @@ class AST {
463464 this . #uflag,
464465 ] ;
465466 }
467+ // We need to calculate the body *twice* if it's a repeat pattern
468+ // at the start, once in nodot mode, then again in dot mode, so a
469+ // pattern like *(?) can match 'x.y'
470+ const repeated = this . type === '*' || this . type === '+' ;
466471 // some kind of extglob
467472 const start = this . type === '!' ? '(?:(?!(?:' : '(?:' ;
468- const body = this . #parts
469- . map ( p => {
470- // extglob ASTs should only contain parent ASTs
471- /* c8 ignore start */
472- if ( typeof p === 'string' ) {
473- throw new Error ( 'string type in extglob ast??' ) ;
474- }
475- /* c8 ignore stop */
476- // can ignore hasMagic, because extglobs are already always magic
477- const [ re , _ , _hasMagic , uflag ] = p . toRegExpSource ( ) ;
478- this . #uflag = this . #uflag || uflag ;
479- return re ;
480- } )
481- . filter ( p => ! ( this . isStart ( ) && this . isEnd ( ) ) || ! ! p )
482- . join ( '|' ) ;
473+ let body = this . #partsToRegExp( dot ) ;
483474 if ( this . isStart ( ) && this . isEnd ( ) && ! body && this . type !== '!' ) {
484475 // invalid extglob, has to at least be *something* present, if it's
485476 // the entire path portion.
@@ -489,22 +480,37 @@ class AST {
489480 this . #hasMagic = undefined ;
490481 return [ s , ( 0 , unescape_js_1 . unescape ) ( this . toString ( ) ) , false , false ] ;
491482 }
483+ // XXX abstract out this map method
484+ let bodyDotAllowed = ! repeated || allowDot || dot || ! startNoDot
485+ ? ''
486+ : this . #partsToRegExp( true ) ;
487+ if ( bodyDotAllowed === body ) {
488+ bodyDotAllowed = '' ;
489+ }
490+ if ( bodyDotAllowed ) {
491+ body = `(?:${ body } )(?:${ bodyDotAllowed } )*?` ;
492+ }
492493 // an empty !() is exactly equivalent to a starNoEmpty
493494 let final = '' ;
494495 if ( this . type === '!' && this . #emptyExt) {
495- final =
496- ( this . isStart ( ) && ! this . #options. dot ? startNoDot : '' ) + starNoEmpty ;
496+ final = ( this . isStart ( ) && ! dot ? startNoDot : '' ) + starNoEmpty ;
497497 }
498498 else {
499499 const close = this . type === '!'
500500 ? // !() must match something,but !(x) can match ''
501501 '))' +
502- ( this . isStart ( ) && ! this . #options . dot ? startNoDot : '' ) +
502+ ( this . isStart ( ) && ! dot && ! allowDot ? startNoDot : '' ) +
503503 star +
504504 ')'
505505 : this . type === '@'
506506 ? ')'
507- : `)${ this . type } ` ;
507+ : this . type === '?'
508+ ? ')?'
509+ : this . type === '+' && bodyDotAllowed
510+ ? ')'
511+ : this . type === '*' && bodyDotAllowed
512+ ? `)?`
513+ : `)${ this . type } ` ;
508514 final = start + body + close ;
509515 }
510516 return [
@@ -514,6 +520,23 @@ class AST {
514520 this . #uflag,
515521 ] ;
516522 }
523+ #partsToRegExp( dot ) {
524+ return this . #parts
525+ . map ( p => {
526+ // extglob ASTs should only contain parent ASTs
527+ /* c8 ignore start */
528+ if ( typeof p === 'string' ) {
529+ throw new Error ( 'string type in extglob ast??' ) ;
530+ }
531+ /* c8 ignore stop */
532+ // can ignore hasMagic, because extglobs are already always magic
533+ const [ re , _ , _hasMagic , uflag ] = p . toRegExpSource ( dot ) ;
534+ this . #uflag = this . #uflag || uflag ;
535+ return re ;
536+ } )
537+ . filter ( p => ! ( this . isStart ( ) && this . isEnd ( ) ) || ! ! p )
538+ . join ( '|' ) ;
539+ }
517540 static #parseGlob( glob , hasMagic , noEmpty = false ) {
518541 let escaping = false ;
519542 let re = '' ;
0 commit comments