diff --git a/.verb.md b/.verb.md index b49d5cf..12d8c23 100644 --- a/.verb.md +++ b/.verb.md @@ -118,7 +118,7 @@ The following options may be used with the main `picomatch()` function or any of | [onMatch](#optionsonMatch) | `function` | `undefined` | Function to be called on matched items. | | [onResult](#optionsonResult) | `function` | `undefined` | Function to be called on all items, regardless of whether or not they are matched or ignored. | | `posix` | `boolean` | `false` | Support POSIX character classes ("posix brackets"). | -| `prepend` | `boolean` | `undefined` | String to prepend to the generated regex used for matching. | +| `prepend` | `string` | `undefined` | String to prepend to the generated regex used for matching. | | `regex` | `boolean` | `false` | Use regular expression rules for `+` (instead of matching literal `+`), and for stars that follow closing parentheses or brackets (as in `)*` and `]*`). | | `strictBrackets` | `boolean` | `undefined` | Throw an error if brackets, braces, or parens are imbalanced. | | `strictSlashes` | `boolean` | `undefined` | When true, picomatch won't match trailing slashes with single stars. | diff --git a/README.md b/README.md index 158a8ba..31b8b56 100644 --- a/README.md +++ b/README.md @@ -351,7 +351,7 @@ The following options may be used with the main `picomatch()` function or any of | [onMatch](#optionsonMatch) | `function` | `undefined` | Function to be called on matched items. | | [onResult](#optionsonResult) | `function` | `undefined` | Function to be called on all items, regardless of whether or not they are matched or ignored. | | `posix` | `boolean` | `false` | Support POSIX character classes ("posix brackets"). | -| `prepend` | `boolean` | `undefined` | String to prepend to the generated regex used for matching. | +| `prepend` | `string` | `undefined` | String to prepend to the generated regex used for matching. | | `regex` | `boolean` | `false` | Use regular expression rules for `+` (instead of matching literal `+`), and for stars that follow closing parentheses or brackets (as in `)*` and `]*`). | | `strictBrackets` | `boolean` | `undefined` | Throw an error if brackets, braces, or parens are imbalanced. | | `strictSlashes` | `boolean` | `undefined` | When true, picomatch won't match trailing slashes with single stars. | diff --git a/lib/parse.js b/lib/parse.js index 0bb4116..87c84a7 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -415,7 +415,7 @@ const parse = (input, options) => { start: 0, dot: opts.dot === true, consumed: '', - output: '', + output: bos.output, prefix: '', backtrack: false, negated: false, @@ -585,7 +585,8 @@ const parse = (input, options) => { // Suitable patterns: `/!(*.d).ts`, `/!(*.d).{ts,tsx}`, `**/!(*-dbg).@(js)`. // // Disabling the `fastpaths` option due to a problem with parsing strings as `.ts` in the pattern like `**/!(*.d).ts`. - const expression = parse(rest, { ...options, fastpaths: false }).output; + // Clear prepend so the suffix is not prefixed again; the outer bos token already holds it. + const expression = parse(rest, { ...options, fastpaths: false, prepend: '' }).output; output = token.close = `)${expression})${extglobStar})`; } @@ -603,7 +604,8 @@ const parse = (input, options) => { * Fast paths */ - if (opts.fastpaths !== false && !/(^[*!]|[/()[\]{}"])/.test(input)) { + // Fast paths skip tokenization, which is where options.prepend is applied. + if (opts.fastpaths !== false && !opts.prepend && !/(^[*!]|[/()[\]{}"])/.test(input)) { let backslashes = false; let output = input.replace(REGEX_SPECIAL_CHARS_BACKREF, (m, esc, chars, first, rest, index) => { diff --git a/lib/picomatch.js b/lib/picomatch.js index 9c2bfd5..091a12a 100644 --- a/lib/picomatch.js +++ b/lib/picomatch.js @@ -309,7 +309,8 @@ picomatch.makeRe = (input, options = {}, returnOutput = false, returnState = fal let parsed = { negated: false, fastpaths: true }; - if (options.fastpaths !== false && (input[0] === '.' || input[0] === '*')) { + // Fast paths skip tokenization, which is where options.prepend is applied. + if (options.fastpaths !== false && !options.prepend && (input[0] === '.' || input[0] === '*')) { parsed.output = parse.fastpaths(input, options); } diff --git a/test/options.prepend.js b/test/options.prepend.js new file mode 100644 index 0000000..18a631e --- /dev/null +++ b/test/options.prepend.js @@ -0,0 +1,40 @@ +'use strict'; + +const assert = require('assert'); +const { isMatch, makeRe, parse } = require('..'); + +describe('options.prepend', () => { + it('should prepend a string to the parsed regex source (issue #110)', () => { + const state = parse('abc', { prepend: 'foo' }); + assert.strictEqual(state.tokens[0].output, 'foo'); + assert.strictEqual(state.output, 'fooabc'); + }); + + it('should prepend a string to the generated matcher regex', () => { + assert.strictEqual(makeRe('abc', { prepend: 'foo' }).source, '^(?:fooabc)$'); + assert(isMatch('fooabc', 'ab*', { prepend: 'foo' })); + assert(!isMatch('abc', 'ab*', { prepend: 'foo' })); + }); + + it('should prepend a string when compiling common fast-path patterns', () => { + const re = makeRe('*.js', { prepend: 'foo' }); + assert.ok(re.source.indexOf('^(?:foo') === 0); + assert(isMatch('fooa.js', '*.js', { prepend: 'foo' })); + assert(!isMatch('a.js', '*.js', { prepend: 'foo' })); + }); + + it('should not double-prepend when the parser rebuilds output after backtracking', () => { + const re = makeRe('[[:alpha:]]', { prepend: 'foo' }); + assert.ok(re.source.indexOf('^(?:foo') === 0); + assert.strictEqual(re.source.split('foo').length - 1, 1); + assert(isMatch('fooa', '[[:alpha:]]', { prepend: 'foo' })); + assert(!isMatch('a', '[[:alpha:]]', { prepend: 'foo' })); + }); + + it('should not re-apply prepend when parsing a suffix after a negated extglob', () => { + const state = parse('!(*.d).ts', { prepend: 'foo' }); + assert.strictEqual(state.output.split('foo').length - 1, 1); + assert(isMatch('foofile.ts', '!(*.d).ts', { prepend: 'foo' })); + assert(!isMatch('foofile.d.ts', '!(*.d).ts', { prepend: 'foo' })); + }); +});