diff --git a/.eslintignore.json b/.eslintignore.json new file mode 100644 index 0000000..5d31e4d --- /dev/null +++ b/.eslintignore.json @@ -0,0 +1,3 @@ +{ + "ignorePatterns": ["examples/**", "bench/**"] +} diff --git a/.eslintrc.json b/.eslintrc.json index 72926e7..31658fc 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,19 +1,18 @@ { - "extends": [ - "eslint:recommended" - ], + "extends": ["eslint:recommended"], "env": { "es6": true, "node": true }, - "parserOptions":{ + "parserOptions": { "ecmaVersion": 9 }, "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 +25,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 }], @@ -104,13 +103,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/.verb.md b/.verb.md index a8cabdd..aefeab5 100644 --- a/.verb.md +++ b/.verb.md @@ -122,13 +122,15 @@ 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"). | -| `posixSlashes` | `boolean` | `undefined` | Convert all slashes in file paths to forward slashes. This does not convert slashes in the glob pattern itself | | `prepend` | `boolean` | `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. | +| `strictBraces` | `boolean` | `undefined` | Treat brace patterns with no sets or ranges as literals. For example, `{abc}` would be a literal. | +| `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. | | `unescape` | `boolean` | `undefined` | Remove backslashes preceding escaped characters in the glob pattern. By default, backslashes are retained. | | `unixify` | `boolean` | `undefined` | Alias for `posixSlashes`, for backwards compatibility. | +| `windows` | `boolean` | `undefined` | Force picomatch to act like the platform is Windows. When true, converts all slashes file paths (or whatever input strings are passed) to forward (posix) slashes. Note that this does not convert slashes in **glob patterns**, only in the strings to be matched. | + ### Scan Options diff --git a/bench/glob-parent.js b/bench/glob-parent.js index 2790415..9b3e770 100644 --- a/bench/glob-parent.js +++ b/bench/glob-parent.js @@ -47,11 +47,6 @@ bench('*.js') .add(' glob-parent', () => parent('*.js')) .run(); -bench('foo/bar/baz') - .add('picomatch.scan', () => scan('foo/bar/baz')) - .add(' glob-parent', () => parent('foo/bar/baz')) - .run(); - bench('foo/*.js') .add('picomatch.scan', () => scan('foo/*.js')) .add(' glob-parent', () => parent('foo/*.js')) @@ -62,6 +57,11 @@ bench('foo/{a,b}/*.js') .add(' glob-parent', () => parent('foo/{a,b}/*.js')) .run(); +bench('foo/bar/baz') + .add('picomatch.scan', () => scan('foo/bar/baz')) + .add(' glob-parent', () => parent('foo/bar/baz')) + .run(); + bench('*.js { parts: true, tokens: true }') .add('picomatch.scan', () => scan('*.js', { parts: true, tokens: true })) .add(' glob-parent', () => parent('*.js')) diff --git a/bench/index.js b/bench/index.js index cf12e43..2e45255 100644 --- a/bench/index.js +++ b/bench/index.js @@ -44,31 +44,36 @@ const bench = (name, options) => { }; bench(`${red('.makeRe')} star`) - .add('picomatch', () => pm.makeRe('*')) + .add('picomatch', () => pm.makeRe('*', { fastpaths: false })) .add('minimatch', () => mm.makeRe('*')) .run(); -bench(`${red('.makeRe')} star; dot=true`) - .add('picomatch', () => pm.makeRe('*', { dot: true })) +bench(`${red('.makeRe')} leading star`) + .add('picomatch', () => pm.makeRe('*.txt', { fastpaths: false })) + .add('minimatch', () => mm.makeRe('*.txt')) + .run(); + +bench(`${red('.makeRe')} path with star`) + .add('picomatch', () => pm.makeRe('foo/*.js', { fastpaths: false })) + .add('minimatch', () => mm.makeRe('foo/*.js')) + .run(); + +bench(`${red('.makeRe')} star w/ { dot: true }`) + .add('picomatch', () => pm.makeRe('*', { dot: true, fastpaths: false })) .add('minimatch', () => mm.makeRe('*', { dot: true })) .run(); bench(`${red('.makeRe')} globstar`) - .add('picomatch', () => pm.makeRe('**')) + .add('picomatch', () => pm.makeRe('**', { fastpaths: false })) .add('minimatch', () => mm.makeRe('**')) .run(); -bench(`${red('.makeRe')} globstars`) - .add('picomatch', () => pm.makeRe('**/**/**')) +bench(`${red('.makeRe')} multiple globstars`) + .add('picomatch', () => pm.makeRe('**/**/**', { fastpaths: false })) .add('minimatch', () => mm.makeRe('**/**/**')) .run(); -bench(`${red('.makeRe')} with leading star`) - .add('picomatch', () => pm.makeRe('*.txt')) - .add('minimatch', () => mm.makeRe('*.txt')) - .run(); - -bench(`${red('.makeRe')} - basic braces`) - .add('picomatch', () => pm.makeRe('{a,b,c}*.txt')) - .add('minimatch', () => mm.makeRe('{a,b,c}*.txt')) +bench(`${red('.makeRe')} basic braces`) + .add('picomatch', () => pm.makeRe('foo/{a,b,c}*.txt', { fastpaths: false })) + .add('minimatch', () => mm.makeRe('foo/{a,b,c}*.txt')) .run(); diff --git a/bench/load-time.js b/bench/load-time.js index b5e5c3a..2b710f2 100644 --- a/bench/load-time.js +++ b/bench/load-time.js @@ -1,10 +1,19 @@ 'use strict'; +const libs = { + pm() { + return require('..'); + }, + mm() { + return require('minimatch'); + } +}; + console.log('# Load time'); console.time('picomatch'); -exports.pm = require('..'); +libs.pm(); console.timeEnd('picomatch'); console.time('minimatch'); -exports.mm = require('minimatch'); +libs.mm(); console.timeEnd('minimatch'); console.log(); diff --git a/bench/package.json b/bench/package.json index 317c6bc..af52fab 100644 --- a/bench/package.json +++ b/bench/package.json @@ -4,12 +4,12 @@ "private": true, "main": "index.js", "dependencies": { - "ansi-colors": "^3.0.3", + "ansi-colors": "^4.1.1", "benchmark": "^2.1.4", "minimist": "^1.2.0" }, "devDependencies": { - "glob-parent": "^3.1.0", + "glob-parent": "^5.1.0", "minimatch": "^3.0.4" }, "lintDeps": { diff --git a/examples/scan.js b/examples/scan.js index 962422d..55674a4 100644 --- a/examples/scan.js +++ b/examples/scan.js @@ -15,3 +15,58 @@ console.log(pm.scan('foo/**/*.js')); console.log(pm.scan('foo/bar/*.js')); console.log(pm.scan('foo/*.js')); console.log(pm.scan('/foo')); + +const braces = require('braces'); + +const scan = (pattern, options) => { + // const matchers = {}; + const patterns = braces.expand(pattern, options); + const result = patterns.map(p => pm.scan(p, options)); + + for (let i = 0; i < result.length; i++) { + const state = result[i]; + if (state.maxDepth === Infinity) continue; + + // const matcher = matchers[state.base] || (matchers[state.base] = {}); + let foundGlob = false; + + for (const token of state.tokens) { + if (token.isGlob === true) { + foundGlob = true; + } + + if (foundGlob === false) { + continue; + } + + if (token.isGlob === false) { + token.matcher = name => token.value === name; + } else { + token.matcher = function glob() {}; + } + + } + console.log(state); + } + + return result; +}; + +scan('{one/two,foo}/*/abc/{bar,**/*}.js', { parts: true, tokens: true }); + +// scan('./foo/**/*/*.js', { parts: true, tokens: true }); +// scan('**/bar.js', { parts: true, tokens: true }); +// scan('foo/**/bar.js', { parts: true, tokens: true }); +// scan('foo/**/{bar,*/*}.js', { parts: true, tokens: true }); +// scan('foo/**/{bar,*/*}/*.js', { parts: true, tokens: true }); +// const { tokens } = scan('foo/*/{bar,*/*}/*.js', { parts: true, tokens: true }); +// for (const token of tokens) { +// console.log(token); +// } + +// console.log(scan('./foo/**/*/*.js', { parts: true, tokens: true })); +// console.log(scan('foo/**/bar.js', { parts: true, tokens: true })); +// console.log(scan('foo/**/{bar,*/*}.js', { parts: true, tokens: true })); +// console.log(scan('foo/**/{bar,*/*}/*.js', { parts: true, tokens: true })); +// console.log(scan('!./foo/*.js')); +// console.log(scan('!./foo/*.js', { parts: true, tokens: true })); diff --git a/lib/parse.js b/lib/parse.js index 34bdc2f..e91ea1d 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}).)*?)`; }; @@ -127,9 +127,6 @@ const parse = (input, options) => { tokens }; - input = utils.removePrefix(input, state); - len = input.length; - const extglobs = []; const braces = []; const stack = []; @@ -144,7 +141,7 @@ const parse = (input, options) => { const peek = state.peek = (n = 1) => input[state.index + n]; const advance = state.advance = () => input[++state.index]; const remaining = () => input.slice(state.index + 1); - const consume = (value = '', num = 0) => { + const consume = (value, num = 0) => { state.consumed += value; state.index += num; }; @@ -214,6 +211,10 @@ const parse = (input, options) => { return; } + if (tok.type === 'text') { + tok.output = tok.value; + } + tok.prev = prev; tokens.push(tok); prev = tok; @@ -260,7 +261,7 @@ const parse = (input, options) => { * Fast paths */ - if (opts.fastpaths !== false && !/(^[*!]|[/()[\]{}"])/.test(input)) { + if (opts.fastpaths !== false && !/(^[*!]|(?!<\\)[/()[\]{}"])/.test(input)) { let backslashes = false; let output = input.replace(REGEX_SPECIAL_CHARS_BACKREF, (m, esc, chars, first, rest, index) => { @@ -297,7 +298,7 @@ const parse = (input, options) => { output = output.replace(/\\/g, ''); } else { output = output.replace(/\\+/g, m => { - return m.length % 2 === 0 ? '\\\\' : (m ? '\\' : ''); + return m.length % 2 === 0 ? '\\\\' : '\\'; }); } } @@ -356,9 +357,9 @@ const parse = (input, options) => { } if (opts.unescape === true) { - value = advance() || ''; + value = advance(); } else { - value += advance() || ''; + value += advance(); } if (state.brackets === 0) { @@ -444,7 +445,10 @@ const parse = (input, options) => { if (value === '(') { increment('parens'); - push({ type: 'paren', value }); + const rest = remaining(); + const idx = rest.indexOf(')'); + const output = (idx > -1 && rest[idx - 1] !== '\\') ? '\\(?(' : '\\('; + push({ type: 'paren', value, output }); continue; } @@ -459,7 +463,16 @@ const parse = (input, options) => { continue; } - push({ type: 'paren', value, output: state.parens ? ')' : '\\)' }); + const rest = remaining(); + const special = rest[0] === '?' || rest[0] === '+'; + const paren = `)${special ? rest[0] : ''}\\)?`; + + if (special) { + consume(rest[0], 1); + } + + const output = state.parens ? paren : '\\)'; + push({ type: 'paren', value, output }); decrement('parens'); continue; } @@ -562,16 +575,21 @@ const parse = (input, options) => { let output = ')'; if (brace.dots === true) { - const arr = tokens.slice(); - const range = []; + const idx = tokens.lastIndexOf(brace); + const arr = tokens.slice(idx); + const range = ['', '']; + let dots = false; for (let i = arr.length - 1; i >= 0; i--) { tokens.pop(); - if (arr[i].type === 'brace') { - break; + + if (arr[i].type === 'dots') { + dots = true; + continue; } - if (arr[i].type !== 'dots') { - range.unshift(arr[i].value); + + if (arr[i] !== brace) { + range[dots ? 0 : 1] += arr[i].value; } } @@ -579,7 +597,7 @@ const parse = (input, options) => { state.backtrack = true; } - if (brace.comma !== true && brace.dots !== true) { + if (opts.strictBraces && brace.comma !== true && brace.dots !== true) { const out = state.output.slice(0, brace.outputIndex); const toks = state.tokens.slice(brace.tokensIndex); brace.value = brace.output = '\\{'; @@ -604,6 +622,15 @@ const parse = (input, options) => { if (extglobs.length > 0) { extglobs[extglobs.length - 1].conditions++; } + + // See: https://github.com/micromatch/picomatch/issues/59 + if (opts.strictBraces !== true && braces.length > 0) { + const brace = braces[braces.length - 1]; + if (stack[stack.length - 1] === 'braces') { + brace.comma = true; + } + } + push({ type: 'text', value }); continue; } @@ -829,9 +856,7 @@ const parse = (input, options) => { // strip consecutive `/**/` while (rest.slice(0, 3) === '/**') { const after = input[state.index + 4]; - if (after && after !== '/') { - break; - } + if (after && after !== '/') break; rest = rest.slice(3); consume('/**', 3); } @@ -925,11 +950,9 @@ const parse = (input, options) => { if (prev.type === 'dot') { state.output += NO_DOT_SLASH; prev.output += NO_DOT_SLASH; - } else if (opts.dot === true) { state.output += NO_DOTS_SLASH; prev.output += NO_DOTS_SLASH; - } else { state.output += nodot; prev.output += nodot; @@ -1022,7 +1045,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}).)*?)`; }; @@ -1065,8 +1088,7 @@ parse.fastpaths = (input, options) => { } }; - const output = utils.removePrefix(input, state); - let source = create(output); + let source = create(input); if (source && opts.strictSlashes !== true) { source += `${SLASH_LITERAL}?`; diff --git a/lib/picomatch.js b/lib/picomatch.js index df7438a..ebaa235 100644 --- a/lib/picomatch.js +++ b/lib/picomatch.js @@ -59,7 +59,7 @@ const picomatch = (glob, options, returnState = false) => { let isIgnored = () => false; if (opts.ignore) { - const ignoreOpts = { ...options, ignore: null, onMatch: null, onResult: null }; + const ignoreOpts = { ...options, ...opts.ignoreOptions, ignore: null, onMatch: null, onResult: null }; isIgnored = picomatch(opts.ignore, ignoreOpts, returnState); } @@ -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); @@ -326,11 +329,12 @@ picomatch.toRegex = (source, options) => { }; /** - * Picomatch constants. + * Picomatch utils and constants. * @return {Object} */ picomatch.constants = constants; +picomatch.utils = utils; /** * Expose "picomatch" diff --git a/lib/scan.js b/lib/scan.js index f291890..3fcaf7f 100644 --- a/lib/scan.js +++ b/lib/scan.js @@ -188,16 +188,38 @@ const scan = (input, options) => { if (code === CHAR_RIGHT_PARENTHESES) { isGlob = token.isGlob = true; - finished = true; break; } } continue; } + break; } } + if (opts.noparen !== true && code === CHAR_LEFT_PARENTHESES) { + while (eos() !== true && (code = advance())) { + if (code === CHAR_BACKWARD_SLASH) { + backslashes = token.backslashes = true; + code = advance(); + continue; + } + + if (code === CHAR_RIGHT_PARENTHESES) { + isGlob = token.isGlob = true; + break; + } + } + + if (scanToEnd === true) { + continue; + } + + finished = isGlob; + break; + } + if (code === CHAR_ASTERISK) { if (prev === CHAR_ASTERISK) isGlobstar = token.isGlobstar = true; isGlob = token.isGlob = true; @@ -246,26 +268,6 @@ const scan = (input, options) => { continue; } - if (opts.noparen !== true && code === CHAR_LEFT_PARENTHESES) { - while (eos() !== true && (code = advance())) { - if (code === CHAR_BACKWARD_SLASH) { - backslashes = token.backslashes = true; - code = advance(); - continue; - } - - if (code === CHAR_RIGHT_PARENTHESES) { - isGlob = token.isGlob = true; - finished = true; - - if (scanToEnd === true) { - continue; - } - break; - } - } - } - if (isGlob === true) { finished = true; @@ -332,33 +334,43 @@ const scan = (input, options) => { if (opts.tokens === true) { state.maxDepth = 0; - if (!isPathSeparator(code)) { - tokens.push(token); - } + tokens.push(token); state.tokens = tokens; + parts.push(token.value); } if (opts.parts === true || opts.tokens === true) { let prevIndex; - for (let idx = 0; idx < slashes.length; idx++) { + for (let i = 0; i < slashes.length; i++) { const n = prevIndex ? prevIndex + 1 : start; - const i = slashes[idx]; - const value = input.slice(n, i); + const slashIndex = slashes[i]; + + if (n === 0 && slashIndex === 0 && slashes.length === 1) { + parts.push('', input.slice(1)); + break; + } + + let value = input.slice(n, slashIndex); + if (value[0] === '/') value = value.slice(1); + if (opts.tokens) { - if (idx === 0 && start !== 0) { - tokens[idx].isPrefix = true; - tokens[idx].value = prefix; + if (i === 0 && start !== 0) { + tokens[i].isPrefix = true; + tokens[i].value = prefix; } else { - tokens[idx].value = value; + tokens[i].value = value; } - depth(tokens[idx]); - state.maxDepth += tokens[idx].depth; + + depth(tokens[i]); + state.maxDepth += tokens[i].depth; } - if (idx !== 0 || value !== '') { + + if (i !== 0 || prefix !== './') { parts.push(value); } - prevIndex = i; + + prevIndex = slashIndex; } if (prevIndex && prevIndex + 1 < input.length) { @@ -370,6 +382,9 @@ const scan = (input, options) => { depth(tokens[tokens.length - 1]); state.maxDepth += tokens[tokens.length - 1].depth; } + + } else if ((str && parts.length === 0) || input[input.length - 1] === '/') { + parts.push(''); } state.slashes = slashes; diff --git a/lib/utils.js b/lib/utils.js index c3ca766..d578ee6 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -9,6 +9,10 @@ const { REGEX_SPECIAL_CHARS_GLOBAL } = require('./constants'); +/** + * isObject, isWindows, and toPosixSlashes are used by micromatch. + */ + exports.isObject = val => val !== null && typeof val === 'object' && !Array.isArray(val); exports.hasRegexChars = str => REGEX_SPECIAL_CHARS.test(str); exports.isRegexChar = str => str.length === 1 && exports.hasRegexChars(str); @@ -21,9 +25,9 @@ exports.removeBackslashes = str => { }); }; -exports.supportsLookbehinds = () => { - const segs = process.version.slice(1).split('.').map(Number); - if (segs.length === 3 && segs[0] >= 9 || (segs[0] === 8 && segs[1] >= 10)) { +exports.supportsLookbehinds = (version = process.version) => { + const segs = version.replace(/^v/, '').split('.').map(Number); + if (segs.length === 3 && (segs[0] >= 9 || (segs[0] === 8 && segs[1] >= 10))) { return true; } return false; @@ -43,15 +47,6 @@ exports.escapeLast = (input, char, lastIdx) => { return `${input.slice(0, idx)}\\${input.slice(idx)}`; }; -exports.removePrefix = (input, state = {}) => { - let output = input; - if (output.startsWith('./')) { - output = output.slice(2); - state.prefix = './'; - } - return output; -}; - exports.wrapOutput = (input, state = {}, options = {}) => { const prepend = options.contains ? '' : '^'; const append = options.contains ? '' : '$'; diff --git a/package.json b/package.json index 609b4d3..3f82c7b 100644 --- a/package.json +++ b/package.json @@ -23,13 +23,14 @@ "mocha": "mocha --reporter dot", "test": "npm run lint && npm run mocha", "test:ci": "npm run test:cover", - "test:cover": "nyc npm run mocha" + "test:cover": "npm run mocha" }, "devDependencies": { + "braces": "^3.0.2", "eslint": "^6.8.0", "fill-range": "^7.0.1", "gulp-format-md": "^2.0.0", - "mocha": "^6.2.2", + "mocha": "^7.0.1", "nyc": "^15.0.0", "time-require": "github:jonschlinkert/time-require" }, @@ -64,6 +65,8 @@ "related": { "list": [ "braces", + "fill-range", + "to-regex-range", "micromatch" ] }, diff --git a/test/api.picomatch.js b/test/api.picomatch.js index 0d17dba..fc48728 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/api.scan.js b/test/api.scan.js index d23731b..1296990 100644 --- a/test/api.scan.js +++ b/test/api.scan.js @@ -15,8 +15,18 @@ const both = (...args) => { * and both libraries use path.dirname. Picomatch does not. */ -describe('picomatch', () => { - describe('.scan', () => { +describe('.scan', () => { + describe('when', () => { + it('should ', () => { + assert.deepStrictEqual(scan('/a', { parts: true }).parts, ['', 'a']); + assert.deepStrictEqual(scan('/a/b', { parts: true }).parts, ['', 'a', 'b']); + assert.deepStrictEqual(scan('/a/b/', { parts: true }).parts, ['', 'a', 'b', '']); + assert.deepStrictEqual(scan('(!(b/a))', { parts: true }).parts, ['']); + assert.deepStrictEqual(scan('(a|b)/c', { parts: true }).parts, ['(a|b)', 'c']); + }); + }); + + describe('base', () => { it('should get the "base" and "glob" from a pattern', () => { assert.deepStrictEqual(both('foo/bar'), ['foo/bar', '']); assert.deepStrictEqual(both('foo/@bar'), ['foo/@bar', '']); diff --git a/test/dotfiles.js b/test/dotfiles.js index e72e0f6..f90abb8 100644 --- a/test/dotfiles.js +++ b/test/dotfiles.js @@ -224,7 +224,7 @@ describe('dotfiles', () => { assert(isMatch('abc/../abc', '*/../*')); }); - it('should not match double dots when not defined in pattern', async() => { + it('should not match double dots when not defined in pattern', async () => { assert(!isMatch('../abc', '**/*')); assert(!isMatch('../abc', '**/**/**')); assert(!isMatch('../abc', '**/**/abc')); @@ -291,7 +291,7 @@ describe('dotfiles', () => { assert(!isMatch('abc/abc/..', 'abc/*/**/*', { strictSlashes: true })); }); - it('should not match single exclusive dots when not defined in pattern', async() => { + it('should not match single exclusive dots when not defined in pattern', async () => { assert(!isMatch('.', '**')); assert(!isMatch('abc/./abc', '**')); assert(!isMatch('abc/abc/.', '**')); diff --git a/test/extglobs.js b/test/extglobs.js index 29bcb06..78aacdd 100644 --- a/test/extglobs.js +++ b/test/extglobs.js @@ -15,6 +15,21 @@ describe('extglobs', () => { assert.throws(() => makeRe('a)b', opts), /Missing opening: "\("/i); }); + it('should support options.capture with extglobs', () => { + assert(isMatch('cbz', 'c*(b)z', { capture: true })); + assert(!isMatch('cbz', 'c*(a)z', { capture: true })); + assert(!isMatch('cbz', 'c*(*x)', { capture: true })); + }); + + it('should support globstars in extglobs', () => { + assert(!isMatch('cbz', 'c!(**)z')); + assert(!isMatch('c/bz', 'c!(/**)z')); + assert(isMatch('cbz', 'c!(/**)z')); + assert(!isMatch('cbz', 'c!(a|**)z')); + assert(!isMatch('cbz', 'c!(**|a)z')); + assert(!isMatch('cbz', 'c!(**|b)z')); + }); + it('should escape special characters immediately following opening parens', () => { assert(isMatch('cbz', 'c!(.)z')); assert(!isMatch('cbz', 'c!(*)z')); @@ -22,10 +37,11 @@ describe('extglobs', () => { assert(isMatch('cbz', 'c!(+)z')); assert(isMatch('cbz', 'c!(?)z')); assert(isMatch('cbz', 'c!(@)z')); + assert(!isMatch('cbz', 'c!(b)z')); }); it('should not convert capture groups to extglobs', () => { - assert.strictEqual(makeRe('c!(?:foo)?z').source, '^(?:c!(?:foo)?z)$'); + assert.strictEqual(makeRe('c!(?:foo)?z').source, '^(?:c!\\(?(?:foo)?\\)?z)$'); assert(!isMatch('c/z', 'c!(?:foo)?z')); assert(isMatch('c!fooz', 'c!(?:foo)?z')); assert(isMatch('c!z', 'c!(?:foo)?z')); diff --git a/test/extglobs-temp.js b/test/extglobs2.js similarity index 99% rename from test/extglobs-temp.js rename to test/extglobs2.js index f5ea7ba..3237b34 100644 --- a/test/extglobs-temp.js +++ b/test/extglobs2.js @@ -8,7 +8,7 @@ const { isMatch } = require('..'); * Some of tests were converted from bash 4.3, 4.4, and minimatch unit tests. */ -describe('extglobs', () => { +describe('extglobs2', () => { beforeEach(() => support.windowsPathSep()); afterEach(() => support.resetPathSep()); diff --git a/test/malicious.js b/test/malicious.js index 33cf6b8..20a2f86 100644 --- a/test/malicious.js +++ b/test/malicious.js @@ -13,7 +13,9 @@ describe('handling of potential regex exploits', () => { if (process.platform !== 'win32') { assert(isMatch('\\A', `${repeat(65500)}A`), 'within the limits, and valid match'); } - assert(isMatch('A', `!${repeat(65500)}A`), 'within the limits, and valid match'); + assert(isMatch('A', `${repeat(65499)}A`), 'within the limits, and invalid match'); + assert(!isMatch('A', `!${repeat(65499)}A`), 'within the limits, and invalid match'); + assert(!isMatch('A', `${repeat(65500)}A`), 'within the limits, and valid match'); assert(isMatch('A', `!(${repeat(65500)}A)`), 'within the limits, and valid match'); assert(!isMatch('A', `[!(${repeat(65500)}A`), 'within the limits, but invalid regex'); }); diff --git a/test/options.expandRange.js b/test/options.expandRange.js index 0eb05fc..71b9e78 100644 --- a/test/options.expandRange.js +++ b/test/options.expandRange.js @@ -5,6 +5,10 @@ const fill = require('fill-range'); const { isMatch } = require('..'); describe('options.expandRange', () => { + it('should test against a range', () => { + assert(!isMatch('a/z', 'a/{[]](]..ca0}')); + }); + it('should support a custom function for expanding ranges in brace patterns', () => { assert(isMatch('a/c', 'a/{a..c}', { expandRange: (a, b) => `([${a}-${b}])` })); assert(!isMatch('a/z', 'a/{a..c}', { expandRange: (a, b) => `([${a}-${b}])` })); diff --git a/test/parens.js b/test/parens.js index d73bf63..9398e43 100644 --- a/test/parens.js +++ b/test/parens.js @@ -15,6 +15,18 @@ describe('parens (non-extglobs)', () => { assert(isMatch('aaabbb', '(a|b)*')); }); + it('should match literal parens', () => { + assert(isMatch('(a)', '(a)*')); + assert(isMatch('(az)', '(a)*')); + assert(!isMatch('(zz)', '(a)*')); + assert(isMatch('(ab)', '(a|b)*')); + assert(isMatch('(abc)', '(a|b)*')); + assert(isMatch('(aa)', '(a)*')); + assert(isMatch('(aaab)', '(a|b)*')); + assert(isMatch('(aaabbb)', '(a|b)*')); + assert(!isMatch('(zaabbb)', '(a|b)*')); + }); + it('should not match slashes with single stars', () => { assert(!isMatch('a/b', '(a)*')); assert(!isMatch('a/b', '(a|b)*')); diff --git a/test/parse.js b/test/parse.js new file mode 100644 index 0000000..c4ed927 --- /dev/null +++ b/test/parse.js @@ -0,0 +1,35 @@ +'use strict'; + +const assert = require('assert'); +const { parse } = require('..'); + +describe('parse', () => { + it('should ignore \u0000', () => { + assert.equal(parse('\u0000*').consumed, '*'); + }); + + it('should parse ./ from string', () => { + assert.equal(parse('./*', { fastpaths: false }).consumed, '*'); + }); + + it('should parse a glob', () => { + assert.equal(parse('*').consumed, '*'); + if (process.platform === 'win32') { + assert.equal(parse('*').output, '(?!\\.)(?=.)[^\\\\/]*?[\\\\/]?'); + } else { + assert.equal(parse('*').output, '(?!\\.)(?=.)[^/]*?\\/?'); + } + }); + + it('should support capture', () => { + if (process.platform === 'win32') { + assert.equal(parse('*', { capture: true }).output, '(?!\\.)(?=.)([^\\\\/]*?)[\\\\/]?'); + } else { + assert.equal(parse('*', { capture: true }).output, '(?!\\.)(?=.)([^/]*?)\\/?'); + } + }); + + it('should throw an error when value is not a string', () => { + assert.throws(() => parse({}), /expected a string/i); + }); +}); diff --git a/test/slashes-posix.js b/test/slashes-posix.js index 50abfd6..7092285 100644 --- a/test/slashes-posix.js +++ b/test/slashes-posix.js @@ -9,6 +9,15 @@ describe('slash handling - posix', () => { after(() => support.resetPathSep()); afterEach(() => support.resetPathSep()); + it('should handle backslashes', () => { + assert(isMatch('\\\\\\a', '\\\\\\a')); + assert(!isMatch('\\\\\\a', '\\\\a')); + assert(isMatch('\\', '\\', { fastpaths: false })); + if (process.platform !== 'win32') { + assert(isMatch('\\\\\\a', '\\\\a', { contains: true })); + } + }); + it('should match a literal string', () => { assert(!isMatch('a/a', '(a/b)')); assert(isMatch('a/b', '(a/b)')); diff --git a/test/special-characters.js b/test/special-characters.js index 7ff8920..8ff260a 100644 --- a/test/special-characters.js +++ b/test/special-characters.js @@ -232,7 +232,7 @@ describe('special characters', () => { assert(isMatch('foo(bar)baz', 'foo*baz')); }); - it('should match literal parens with brackets', async() => { + it('should match literal parens with brackets', async () => { assert(isMatch('foo(bar)baz', 'foo[bar()]+baz')); }); diff --git a/test/stars.js b/test/stars.js index 910b9f2..12bbd0c 100644 --- a/test/stars.js +++ b/test/stars.js @@ -360,6 +360,8 @@ describe('stars', () => { it('should optionally match trailing slashes with braces', () => { assert(isMatch('foo', '**/*')); + assert(isMatch('foo', '*{,/}')); + assert(isMatch('foo/', '*{,/}')); assert(isMatch('foo', '**/*{,/}')); assert(isMatch('foo/', '**/*{,/}')); assert(isMatch('foo/bar', '**/*{,/}')); diff --git a/test/utils.js b/test/utils.js new file mode 100644 index 0000000..571de55 --- /dev/null +++ b/test/utils.js @@ -0,0 +1,53 @@ +'use strict'; + +const path = require('path'); +const assert = require('assert'); +const utils = require('../lib/utils'); + +/** + * isObject, isWindows, and toPosixSlashes are used by micromatch. + */ + +describe('utils', () => { + it('.isRegexChar', () => { + assert.equal(utils.isRegexChar('*'), true); + assert.equal(utils.isRegexChar('?'), true); + assert.equal(utils.isRegexChar('a'), false); + }); + + it('.isWindows', () => { + assert.equal(utils.isWindows(), process.platform === 'win32'); + // this hack is only used to allow us to do better unit tests on windows paths + const sep = path.sep; + path.sep = '\\'; + assert.equal(utils.isWindows(), true); + path.sep = sep; + }); + + it('.isRegexChar', () => { + assert.equal(utils.isRegexChar('*'), true); + assert.equal(utils.isRegexChar('?'), true); + assert.equal(utils.isRegexChar('a'), false); + }); + + it('.isObject', () => { + assert.equal(utils.isObject('*'), false); + assert.equal(utils.isObject({}), true); + }); + + it('.wrapOutput', () => { + assert.equal(utils.wrapOutput('foo'), '^(?:foo)$'); + assert.equal(utils.wrapOutput('foo', {}, { contains: true }), '(?:foo)'); + assert.equal(utils.wrapOutput('foo', { negated: true }), '(?:^(?!^(?:foo)$).*$)'); + }); + + it('.toPosixSlashes', () => { + assert.equal(utils.toPosixSlashes('a\\b\\c'), 'a/b/c'); + }); + + it('.supportsLookbehinds', () => { + assert.equal(utils.supportsLookbehinds('8.9.0'), false); + assert.equal(utils.supportsLookbehinds('8.10.0'), true); + assert.equal(utils.supportsLookbehinds('9.0.0'), true); + }); +});