diff --git a/lib/validateBraces.js b/lib/validateBraces.js index 21bdedc80..bc5ef03a4 100644 --- a/lib/validateBraces.js +++ b/lib/validateBraces.js @@ -17,8 +17,8 @@ import { incorrectBraces } from './messages.js' * braces from user configuration, but this is left to the user (after seeing the warning). * * @example Globs with brace expansions - * - *.{js,tx} // expanded as *.js, *.ts - * - *.{{j,t}s,css} // expanded as *.js, *.ts, *.css + * - *.{js,tx} // expanded as *.js, *.ts + * - *.{{j,t}s,css} // expanded as *.js, *.ts, *.css * - file_{1..10}.css // expanded as file_1.css, file_2.css, …, file_10.css * * @example Globs with incorrect brace expansions @@ -28,17 +28,17 @@ import { incorrectBraces } from './messages.js' * - *.${js} // dollar-sign inhibits expansion, so treated literally * - *.{js\,ts} // the comma is escaped, so treated literally */ -export const BRACES_REGEXP = /(? { +const stripIncorrectBraces = (pattern) => { let output = `${pattern}` let match = null - while ((match = BRACES_REGEXP.exec(pattern))) { + while ((match = INCORRECT_BRACES_REGEXP.exec(pattern))) { const fullMatch = match[0] const withoutBraces = fullMatch.replace(/{/, '').replace(/}/, '') output = output.replace(fullMatch, withoutBraces) @@ -47,6 +47,30 @@ const withoutIncorrectBraces = (pattern) => { return output } +/** + * This RegExp matches "duplicate" opening and closing braces, without any other braces + * in between, where the duplication is redundant and should be removed. + * + * @example *.{{js,ts}} // should just be *.{js,ts} + */ +export const DOUBLE_BRACES_REGEXP = /{{[^}{]*}}/ + +/** + * @param {string} pattern + * @returns {string} + */ +const stripDoubleBraces = (pattern) => { + let output = `${pattern}` + const match = DOUBLE_BRACES_REGEXP.exec(pattern)?.[0] + + if (match) { + const withoutBraces = match.replace('{{', '{').replace('}}', '}') + output = output.replace(match, withoutBraces) + } + + return output +} + /** * Validate and remove incorrect brace expansions from glob pattern. * For example `*.{js}` is incorrect because it doesn't contain a `,` or `..`, @@ -57,7 +81,7 @@ const withoutIncorrectBraces = (pattern) => { * @returns {string} */ export const validateBraces = (pattern, logger) => { - const fixedPattern = withoutIncorrectBraces(pattern) + const fixedPattern = stripDoubleBraces(stripIncorrectBraces(pattern)) if (fixedPattern !== pattern) { logger.warn(incorrectBraces(pattern, fixedPattern)) diff --git a/test/unit/validateBraces.spec.js b/test/unit/validateBraces.spec.js index 6872021f5..194935306 100644 --- a/test/unit/validateBraces.spec.js +++ b/test/unit/validateBraces.spec.js @@ -1,46 +1,64 @@ import makeConsoleMock from 'consolemock' -import { validateBraces, BRACES_REGEXP } from '../../lib/validateBraces.js' +import { + validateBraces, + INCORRECT_BRACES_REGEXP, + DOUBLE_BRACES_REGEXP, +} from '../../lib/validateBraces.js' -describe('BRACES_REGEXP', () => { +describe('INCORRECT_BRACES_REGEXP', () => { it(`should match '*.{js}'`, () => { - expect('*.{js}'.match(BRACES_REGEXP)).toBeTruthy() + expect('*.{js}'.match(INCORRECT_BRACES_REGEXP)).toBeTruthy() }) it(`should match 'file_{10}'`, () => { - expect('file_{test}'.match(BRACES_REGEXP)).toBeTruthy() + expect('file_{test}'.match(INCORRECT_BRACES_REGEXP)).toBeTruthy() }) it(`should match '*.{spec\\.js}'`, () => { - expect('*.{spec\\.js}'.match(BRACES_REGEXP)).toBeTruthy() + expect('*.{spec\\.js}'.match(INCORRECT_BRACES_REGEXP)).toBeTruthy() }) it(`should match '*.{js\\,ts}'`, () => { - expect('*.{js\\,ts}'.match(BRACES_REGEXP)).toBeTruthy() + expect('*.{js\\,ts}'.match(INCORRECT_BRACES_REGEXP)).toBeTruthy() }) it("should not match '*.${js}'", () => { - expect('*.${js}'.match(BRACES_REGEXP)).not.toBeTruthy() + expect('*.${js}'.match(INCORRECT_BRACES_REGEXP)).toBeFalsy() }) it(`should not match '.{js,ts}'`, () => { - expect('.{js,ts}'.match(BRACES_REGEXP)).not.toBeTruthy() + expect('.{js,ts}'.match(INCORRECT_BRACES_REGEXP)).toBeFalsy() }) it(`should not match 'file_{1..10}'`, () => { - expect('file_{1..10}'.match(BRACES_REGEXP)).not.toBeTruthy() + expect('file_{1..10}'.match(INCORRECT_BRACES_REGEXP)).toBeFalsy() }) it(`should not match '*.\\{js\\}'`, () => { - expect('*.\\{js\\}'.match(BRACES_REGEXP)).not.toBeTruthy() + expect('*.\\{js\\}'.match(INCORRECT_BRACES_REGEXP)).toBeFalsy() }) it(`should not match '*.\\{js}'`, () => { - expect('*.\\{js}'.match(BRACES_REGEXP)).not.toBeTruthy() + expect('*.\\{js}'.match(INCORRECT_BRACES_REGEXP)).toBeFalsy() }) it(`should not match '*.{js\\}'`, () => { - expect('*.{js\\}'.match(BRACES_REGEXP)).not.toBeTruthy() + expect('*.{js\\}'.match(INCORRECT_BRACES_REGEXP)).toBeFalsy() + }) +}) + +describe('DOUBLE_BRACES_REGEXP', () => { + it(`should match '*.{{js,ts}}'`, () => { + expect('*.{{js,ts}}'.match(DOUBLE_BRACES_REGEXP)).toBeTruthy() + }) + + it(`should not match '*.{{js,ts},{css}}'`, () => { + expect('*.{{js,ts},{css}}'.match(DOUBLE_BRACES_REGEXP)).toBeFalsy() + }) + + it(`should not match '*.{{js,ts},{css}}'`, () => { + expect('*.{{js,ts},{css}}'.match(DOUBLE_BRACES_REGEXP)).toBeFalsy() }) }) @@ -84,10 +102,7 @@ describe('validateBraces', () => { `) }) - /** - * @todo This isn't correctly detected even though the outer braces are invalid. - */ - it.skip('should warn about `*.{{js,ts}}` and return fixed pattern', () => { + it('should warn about `*.{{js,ts}}` and return fixed pattern', () => { const logger = makeConsoleMock() const fixedBraces = validateBraces('*.{{js,ts}}', logger) @@ -95,7 +110,7 @@ describe('validateBraces', () => { expect(fixedBraces).toEqual('*.{js,ts}') expect(logger.printHistory()).toMatchInlineSnapshot(` " - WARN ‼ Detected incorrect braces with only single value: \`*.{{js,ts}}\`. Reformatted as: \`*.{js,ts}\` + WARN ⚠ Detected incorrect braces with only single value: \`*.{{js,ts}}\`. Reformatted as: \`*.{js,ts}\` " `) })