diff --git a/lib/generateTasks.js b/lib/generateTasks.js index 1611d5d64..bec0c33df 100644 --- a/lib/generateTasks.js +++ b/lib/generateTasks.js @@ -6,6 +6,19 @@ const path = require('path') const debug = require('debug')('lint-staged:gen-tasks') +const { incorrectBraces } = require('./messages') + +// Braces with a single value like `*.{js}` are invalid +// and thus ignored by micromatch. This regex matches all occurrences of +// two curly braces without a `,` or `..` between them, to make sure +// users can still accidentally use them without +// some linters never matching anything. +// +// For example `.{js,ts}` or `file_{1..10}` are valid but `*.{js}` is not. +// +// See: https://www.gnu.org/software/bash/manual/html_node/Brace-Expansion.html +const BRACES_REGEXP = /({)(?:(?!,|\.\.).)*?(})/g + /** * Generates all task commands, and filelist * @@ -16,33 +29,51 @@ const debug = require('debug')('lint-staged:gen-tasks') * @param {boolean} [options.files] - Staged filepaths * @param {boolean} [options.relative] - Whether filepaths to should be relative to gitDir */ -const generateTasks = ({ config, cwd = process.cwd(), gitDir, files, relative = false }) => { +const generateTasks = ( + { config, cwd = process.cwd(), gitDir, files, relative = false }, + logger +) => { debug('Generating linter tasks') const absoluteFiles = files.map((file) => normalize(path.resolve(gitDir, file))) const relativeFiles = absoluteFiles.map((file) => normalize(path.relative(cwd, file))) - return Object.entries(config).map(([pattern, commands]) => { - const isParentDirPattern = pattern.startsWith('../') - - const fileList = micromatch( - relativeFiles - // Only worry about children of the CWD unless the pattern explicitly - // specifies that it concerns a parent directory. - .filter((file) => { - if (isParentDirPattern) return true - return !file.startsWith('..') && !path.isAbsolute(file) - }), - pattern, - { - cwd, - dot: true, - // If pattern doesn't look like a path, enable `matchBase` to - // match against filenames in every directory. This makes `*.js` - // match both `test.js` and `subdirectory/test.js`. - matchBase: !pattern.includes('/'), - } - ).map((file) => normalize(relative ? file : path.resolve(cwd, file))) + return Object.entries(config).map(([rawPattern, commands]) => { + let pattern = rawPattern + + const isParentDirPattern = rawPattern.startsWith('../') + + // Only worry about children of the CWD unless the pattern explicitly + // specifies that it concerns a parent directory. + const filteredFiles = relativeFiles.filter((file) => { + if (isParentDirPattern) return true + return !file.startsWith('..') && !path.isAbsolute(file) + }) + + // Remove "extra" brackets when they contain only a single value + let hadIncorrectBraces = false + while (BRACES_REGEXP.exec(rawPattern)) { + hadIncorrectBraces = true + pattern = pattern.replace(/{/, '') + pattern = pattern.replace(/}/, '') + } + + // Warn the user about incorrect brackets usage + if (hadIncorrectBraces) { + logger.warn(incorrectBraces(rawPattern, pattern)) + } + + const matches = micromatch(filteredFiles, pattern, { + cwd, + dot: true, + // If the pattern doesn't look like a path, enable `matchBase` to + // match against filenames in every directory. This makes `*.js` + // match both `test.js` and `subdirectory/test.js`. + matchBase: !pattern.includes('/'), + strictBrackets: true, + }) + + const fileList = matches.map((file) => normalize(relative ? file : path.resolve(cwd, file))) const task = { pattern, commands, fileList } debug('Generated task: \n%O', task) diff --git a/lib/messages.js b/lib/messages.js index 13d001434..01d24d02e 100644 --- a/lib/messages.js +++ b/lib/messages.js @@ -21,6 +21,11 @@ const DEPRECATED_GIT_ADD = `${warning} ${chalk.yellow( )} ` +const incorrectBraces = (before, after) => `${warning} ${chalk.yellow( + `Detected incorrect braces with only single value: \`${before}\`. Reformatted as: \`${after}\`` +)} +` + const TASK_ERROR = 'Skipped because of errors from tasks.' const SKIPPED_GIT_ERROR = 'Skipped because of previous git error.' @@ -44,6 +49,7 @@ const CONFIG_STDIN_ERROR = 'Error: Could not read config from stdin.' module.exports = { NOT_GIT_REPO, FAILED_GET_STAGED_FILES, + incorrectBraces, NO_STAGED_FILES, NO_TASKS, skippingBackup, diff --git a/lib/runAll.js b/lib/runAll.js index ba76bdf13..a6839dc8c 100644 --- a/lib/runAll.js +++ b/lib/runAll.js @@ -129,7 +129,7 @@ const runAll = async ( const matchedFiles = new Set() for (const [index, files] of stagedFileChunks.entries()) { - const chunkTasks = generateTasks({ config, cwd, gitDir, files, relative }) + const chunkTasks = generateTasks({ config, cwd, gitDir, files, relative }, logger) const chunkListrTasks = [] for (const task of chunkTasks) { diff --git a/test/generateTasks.spec.js b/test/generateTasks.spec.js index d2f9540f0..0b96f7f11 100644 --- a/test/generateTasks.spec.js +++ b/test/generateTasks.spec.js @@ -1,5 +1,6 @@ -import os from 'os' +import makeConsoleMock from 'consolemock' import normalize from 'normalize-path' +import os from 'os' import path from 'path' import generateTasks from '../lib/generateTasks' @@ -153,6 +154,74 @@ describe('generateTasks', () => { }) }) + it('should match pattern "*.{js}" and show warning', async () => { + const logger = makeConsoleMock() + const result = await generateTasks( + { + config: { + '*.{js}': 'lint', + }, + cwd, + gitDir, + files, + }, + logger + ) + const linter = result.find((item) => item.pattern === '*.js') + + expect(linter).toEqual({ + pattern: '*.js', + commands: 'lint', + fileList: [ + `${gitDir}/test.js`, + `${gitDir}/deeper/test.js`, + `${gitDir}/deeper/test2.js`, + `${gitDir}/even/deeper/test.js`, + `${gitDir}/.hidden/test.js`, + ].map(normalizePath), + }) + + expect(logger.printHistory()).toMatchInlineSnapshot(` + " + WARN ‼ Detected incorrect braces with only single value: \`*.{js}\`. Reformatted as: \`*.js\` + " + `) + }) + + it('should match pattern "*.{c}{s}{s}" and show warning', async () => { + const logger = makeConsoleMock() + const result = await generateTasks( + { + config: { + '*.{c}{s}{s}': 'lint', + }, + cwd, + gitDir, + files, + }, + logger + ) + const linter = result.find((item) => item.pattern === '*.css') + + expect(linter).toEqual({ + pattern: '*.css', + commands: 'lint', + fileList: [ + `${gitDir}/test.css`, + `${gitDir}/deeper/test.css`, + `${gitDir}/deeper/test2.css`, + `${gitDir}/even/deeper/test.css`, + `${gitDir}/.hidden/test.css`, + ].map(normalizePath), + }) + + expect(logger.printHistory()).toMatchInlineSnapshot(` + " + WARN ‼ Detected incorrect braces with only single value: \`*.{c}{s}{s}\`. Reformatted as: \`*.css\` + " + `) + }) + it('should not match files in parent directory by default', async () => { const result = await generateTasks({ config,