diff --git a/.eslintrc.json b/.eslintrc.json index 72926e7..c9e75d3 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -4,16 +4,17 @@ ], "env": { - "es6": true, + "es2021": true, "node": true }, - "parserOptions":{ - "ecmaVersion": 9 + "parserOptions": { + "ecmaVersion": 12 }, "rules": { "accessor-pairs": 2, + "arrow-parens": [2, "as-needed"], "arrow-spacing": [2, { "before": true, "after": true }], "block-spacing": [2, "always"], "brace-style": [2, "1tbs", { "allowSingleLine": true }], @@ -26,7 +27,7 @@ "eol-last": 2, "eqeqeq": [2, "allow-null"], "generator-star-spacing": [2, { "before": true, "after": true }], - "handle-callback-err": [2, "^(err|error)$" ], + "handle-callback-err": [2, "^(err|error)$"], "indent": [2, 2, { "SwitchCase": 1 }], "key-spacing": [2, { "beforeColon": false, "afterColon": true }], "keyword-spacing": [2, { "before": true, "after": true }], @@ -36,7 +37,6 @@ "no-caller": 2, "no-class-assign": 2, "no-cond-assign": 2, - "no-console": 0, "no-const-assign": 2, "no-control-regex": 2, "no-debugger": 2, @@ -104,13 +104,13 @@ "one-var": [2, { "initialized": "never" }], "operator-linebreak": [0, "after", { "overrides": { "?": "before", ":": "before" } }], "padded-blocks": [0, "never"], - "prefer-const": 2, + "prefer-const": [2, { "destructuring": "all", "ignoreReadBeforeAssign": false }], "quotes": [2, "single", "avoid-escape"], "radix": 2, "semi": [2, "always"], "semi-spacing": [2, { "before": false, "after": true }], "space-before-blocks": [2, "always"], - "space-before-function-paren": [2, "never"], + "space-before-function-paren": [2, { "anonymous": "never", "named": "never", "asyncArrow": "always" }], "space-in-parens": [2, "never"], "space-infix-ops": 2, "space-unary-ops": [2, { "words": true, "nonwords": false }], diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 583be86..f928bf9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,7 +20,7 @@ jobs: - name: Clone repository uses: actions/checkout@v2 - - name: Set up Node.js + - name: Setup Node.js uses: actions/setup-node@v2 with: node-version: ${{ matrix.node }} diff --git a/examples/extglob-negated.js b/examples/extglob-negated.js new file mode 100644 index 0000000..78c7c5a --- /dev/null +++ b/examples/extglob-negated.js @@ -0,0 +1,15 @@ +'use strict'; + +const picomatch = require('..'); + +const fixtures = [ + ['/file.d.ts', false], + ['/file.ts', true], + ['/file.d.something.ts', true], + ['/file.dhello.ts', true] +]; + +const pattern = '/!(*.d).ts'; +const isMatch = picomatch(pattern); + +console.log(fixtures.map(f => [isMatch(f[0]), f[1]])); diff --git a/examples/regex-quantifier.js b/examples/regex-quantifier.js new file mode 100644 index 0000000..38306b7 --- /dev/null +++ b/examples/regex-quantifier.js @@ -0,0 +1,43 @@ +'use strict'; + +const pico = require('..'); + +/** + * See: https://github.com/gulpjs/glob-parent/issues/39#issuecomment-794075641 + */ + +const files = [ + 'data/100-123a_files/0/', + 'data/100-123a_files/1/', + 'data/100-123a_files/2/', + 'data/100-123a_files/3/', + 'data/100-123b_files/0/', + 'data/100-123b_files/1/', + 'data/100-123b_files/2/', + 'data/100-123b_files/3/', + 'data/100-123a_files/4/', + 'data/100-123ax_files/0/', + 'data/100-123A_files/0/', + 'data/100-123A_files/1/', + 'data/100-123A_files/2/', + 'data/100-123A_files/3/', + 'data/100-123B_files/0/', + 'data/100-123B_files/1/', + 'data/100-123B_files/2/', + 'data/100-123B_files/3/', + 'data/100-123A_files/4/', + 'data/100-123AX_files/0/' +]; + +// ? is a wildcard for matching one character +// by escaping \\{0,3}, and then using `{ unescape: true }, we tell +// picomatch to treat those characters as a regex quantifier, versus +// a brace pattern. + +const isMatch = pico('data/100-123?\\{0,3}_files/{0..3}/', { unescape: true }); +console.log(files.filter(name => isMatch(name))); + +// Alternatively, we can use a regex character class to be more specific +// In the following example, we'll only match uppercase alpha characters +const isMatch2 = pico('data/100-123[A-Z]*_files/{0..3}/', { unescape: true }); +console.log(files.filter(name => isMatch2(name))); diff --git a/lib/parse.js b/lib/parse.js index bb8e043..c16d59d 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -92,7 +92,7 @@ const parse = (input, options) => { START_ANCHOR } = PLATFORM_CHARS; - const globstar = (opts) => { + const globstar = opts => { return `(${capture}(?:(?!${START_ANCHOR}${opts.dot ? DOTS_SLASH : DOT_LITERAL}).)*?)`; }; @@ -142,12 +142,13 @@ const parse = (input, options) => { const eos = () => state.index === len - 1; const peek = state.peek = (n = 1) => input[state.index + n]; - const advance = state.advance = () => input[++state.index]; + const advance = state.advance = () => input[++state.index] || ''; const remaining = () => input.slice(state.index + 1); const consume = (value = '', num = 0) => { state.consumed += value; state.index += num; }; + const append = token => { state.output += token.output != null ? token.output : token.value; consume(token.value); @@ -203,7 +204,7 @@ const parse = (input, options) => { } } - if (extglobs.length && tok.type !== 'paren' && !EXTGLOB_CHARS[tok.value]) { + if (extglobs.length && tok.type !== 'paren') { extglobs[extglobs.length - 1].inner += tok.value; } @@ -235,6 +236,7 @@ const parse = (input, options) => { const extglobClose = token => { let output = token.close + (opts.capture ? ')' : ''); + let rest; if (token.type === 'negate') { let extglobStar = star; @@ -247,6 +249,10 @@ const parse = (input, options) => { output = token.close = `)$))${extglobStar}`; } + if (token.inner.includes('*') && (rest = remaining()) && /^\.[^\\/.]+$/.test(rest)) { + output = token.close = `)${rest})${extglobStar})`; + } + if (token.prev.type === 'bos') { state.negatedExtglob = true; } @@ -356,9 +362,9 @@ const parse = (input, options) => { } if (opts.unescape === true) { - value = advance() || ''; + value = advance(); } else { - value += advance() || ''; + value += advance(); } if (state.brackets === 0) { @@ -1022,7 +1028,7 @@ parse.fastpaths = (input, options) => { star = `(${star})`; } - const globstar = (opts) => { + const globstar = opts => { if (opts.noglobstar === true) return star; return `(${capture}(?:(?!${START_ANCHOR}${opts.dot ? DOTS_SLASH : DOT_LITERAL}).)*?)`; }; diff --git a/lib/picomatch.js b/lib/picomatch.js index df7438a..782d809 100644 --- a/lib/picomatch.js +++ b/lib/picomatch.js @@ -231,68 +231,71 @@ picomatch.parse = (pattern, options) => { picomatch.scan = (input, options) => scan(input, options); /** - * Create a regular expression from a parsed glob pattern. - * - * ```js - * const picomatch = require('picomatch'); - * const state = picomatch.parse('*.js'); - * // picomatch.compileRe(state[, options]); + * Compile a regular expression from the `state` object returned by the + * [parse()](#parse) method. * - * console.log(picomatch.compileRe(state)); - * //=> /^(?:(?!\.)(?=.)[^/]*?\.js)$/ - * ``` - * @param {String} `state` The object returned from the `.parse` method. + * @param {Object} `state` * @param {Object} `options` - * @return {RegExp} Returns a regex created from the given pattern. + * @param {Boolean} `returnOutput` Intended for implementors, this argument allows you to return the raw output from the parser. + * @param {Boolean} `returnState` Adds the state to a `state` property on the returned regex. Useful for implementors and debugging. + * @return {RegExp} * @api public */ -picomatch.compileRe = (parsed, options, returnOutput = false, returnState = false) => { +picomatch.compileRe = (state, options, returnOutput = false, returnState = false) => { if (returnOutput === true) { - return parsed.output; + return state.output; } const opts = options || {}; const prepend = opts.contains ? '' : '^'; const append = opts.contains ? '' : '$'; - let source = `${prepend}(?:${parsed.output})${append}`; - if (parsed && parsed.negated === true) { + let source = `${prepend}(?:${state.output})${append}`; + if (state && state.negated === true) { source = `^(?!${source}).*$`; } const regex = picomatch.toRegex(source, options); if (returnState === true) { - regex.state = parsed; + regex.state = state; } return regex; }; -picomatch.makeRe = (input, options, returnOutput = false, returnState = false) => { +/** + * Create a regular expression from a parsed glob pattern. + * + * ```js + * const picomatch = require('picomatch'); + * const state = picomatch.parse('*.js'); + * // picomatch.compileRe(state[, options]); + * + * console.log(picomatch.compileRe(state)); + * //=> /^(?:(?!\.)(?=.)[^/]*?\.js)$/ + * ``` + * @param {String} `state` The object returned from the `.parse` method. + * @param {Object} `options` + * @param {Boolean} `returnOutput` Implementors may use this argument to return the compiled output, instead of a regular expression. This is not exposed on the options to prevent end-users from mutating the result. + * @param {Boolean} `returnState` Implementors may use this argument to return the state from the parsed glob with the returned regular expression. + * @return {RegExp} Returns a regex created from the given pattern. + * @api public + */ + +picomatch.makeRe = (input, options = {}, returnOutput = false, returnState = false) => { if (!input || typeof input !== 'string') { throw new TypeError('Expected a non-empty string'); } - const opts = options || {}; let parsed = { negated: false, fastpaths: true }; - let prefix = ''; - let output; - - if (input.startsWith('./')) { - input = input.slice(2); - prefix = parsed.prefix = './'; - } - if (opts.fastpaths !== false && (input[0] === '.' || input[0] === '*')) { - output = parse.fastpaths(input, options); + if (options.fastpaths !== false && (input[0] === '.' || input[0] === '*')) { + parsed.output = parse.fastpaths(input, options); } - if (output === undefined) { + if (!parsed.output) { parsed = parse(input, options); - parsed.prefix = prefix + (parsed.prefix || ''); - } else { - parsed.output = output; } return picomatch.compileRe(parsed, options, returnOutput, returnState); diff --git a/test/api.picomatch.js b/test/api.picomatch.js index e877ad8..c659a64 100644 --- a/test/api.picomatch.js +++ b/test/api.picomatch.js @@ -5,8 +5,7 @@ const picomatch = require('..'); const { isMatch } = picomatch; const assertTokens = (actual, expected) => { - const keyValuePairs = actual.map((token) => [token.type, token.value]); - + const keyValuePairs = actual.map(token => [token.type, token.value]); assert.deepStrictEqual(keyValuePairs, expected); }; @@ -214,7 +213,7 @@ describe('picomatch', () => { assert(!isMatch('zzjs', '*z.js')); }); - it('issue #24', () => { + it('issue #24 - should match zero or more directories', () => { assert(!isMatch('a/b/c/d/', 'a/b/**/f')); assert(isMatch('a', 'a/**')); assert(isMatch('a', '**')); @@ -249,6 +248,7 @@ describe('picomatch', () => { assert(!isMatch('deep/foo/bar/baz', '**/bar/*/')); assert(!isMatch('deep/foo/bar/baz/', '**/bar/*', { strictSlashes: true })); assert(isMatch('deep/foo/bar/baz/', '**/bar/*')); + assert(isMatch('deep/foo/bar/baz', '**/bar/*')); assert(isMatch('foo', 'foo/**')); assert(isMatch('deep/foo/bar/baz/', '**/bar/*{,/}')); assert(isMatch('a/b/j/c/z/x.md', 'a/**/j/**/z/*.md')); diff --git a/test/extglobs-temp.js b/test/extglobs-temp.js index f5ea7ba..991fb50 100644 --- a/test/extglobs-temp.js +++ b/test/extglobs-temp.js @@ -6,6 +6,7 @@ const { isMatch } = require('..'); /** * Some of tests were converted from bash 4.3, 4.4, and minimatch unit tests. + * This is called "temp" as a reminder to reorganize these test and remove duplicates. */ describe('extglobs', () => { diff --git a/test/extglobs.js b/test/extglobs.js index 29bcb06..7966d19 100644 --- a/test/extglobs.js +++ b/test/extglobs.js @@ -54,6 +54,19 @@ describe('extglobs', () => { assert(isMatch('abc', 'a!(.)c')); }); + // See https://github.com/micromatch/picomatch/issues/83 + it('should support stars in negation extglobs', () => { + assert(!isMatch('/file.d.ts', '/!(*.d).ts')); + assert(isMatch('/file.ts', '/!(*.d).ts')); + assert(isMatch('/file.d.something.ts', '/!(*.d).ts')); + assert(isMatch('/file.dhello.ts', '/!(*.d).ts')); + + assert(!isMatch('/file.d.ts', '**/!(*.d).ts')); + assert(isMatch('/file.ts', '**/!(*.d).ts')); + assert(isMatch('/file.d.something.ts', '**/!(*.d).ts')); + assert(isMatch('/file.dhello.ts', '**/!(*.d).ts')); + }); + it('should support negation extglobs in patterns with slashes', () => { assert(!isMatch('foo/abc', 'foo/!(abc)')); assert(isMatch('foo/bar', 'foo/!(abc)')); @@ -271,7 +284,7 @@ describe('extglobs', () => { assert(isMatch('ab.md', '?(a|ab|b).md')); assert(isMatch('b.md', '?(a|ab|b).md')); - // see https://github.com/micromatch/micromatch/issues/186 + // See https://github.com/micromatch/micromatch/issues/186 assert(isMatch('ab', '+(a)?(b)')); assert(isMatch('aab', '+(a)?(b)')); assert(isMatch('aa', '+(a)?(b)'));