Skip to content

Commit b3d97cf

Browse files
authoredAug 6, 2021
fix: try to automatically fix and warn about invalid brace patterns (#992)
* refactor: use arrow function * fix: correctly handle incorrect patterns with braces of only single value Technically brace patterns like `*.{js}` are invalid because braces should always contain at least one comma or a sequence, for example `*.{js,ts}` or `file{1..10}.js`. The `micromatch` library used to match patterns to files will silently ignore such invalid braces and thus lead to the pattern always matching zero files. This is a unintuitive, so lint-staged should normalize such cases by simply removing the unnecessary braces before matching files. * test: add test for matching pattern with bracket sequence * refactor: less nesting in validateConfig * refactor: move pattern fixing to validateConfig * refactor: move error to messages * refactor: throw early for empty configuration * fix: improve configuration error output * refactor: move formatConfig into validateConfig * test: update snapshot serializer to remove absolute file path * fix: do not pass unused logger argument * refactor: move config validation logging to validateConfig * fix: do not match escaped curly braces (`\{` or `\}`) * test: better error message in snapshot * test: add missing assertions * fix: match braces with escaped comma inside * fix: do not match braces when they start with the dollar sign (`$`) * docs: update BRACES_REGEXP comment * refactor: move brace pattern validation to separate file * fix: correctly handle nested braces * refactor: clean up code * test: add some more tests
1 parent f8807d7 commit b3d97cf

19 files changed

+504
-401
lines changed
 

‎lib/formatConfig.js

-7
This file was deleted.

‎lib/generateTasks.js

+24-26
Original file line numberDiff line numberDiff line change
@@ -16,43 +16,41 @@ const debug = require('debug')('lint-staged:gen-tasks')
1616
* @param {boolean} [options.files] - Staged filepaths
1717
* @param {boolean} [options.relative] - Whether filepaths to should be relative to gitDir
1818
*/
19-
module.exports = function generateTasks({
20-
config,
21-
cwd = process.cwd(),
22-
gitDir,
23-
files,
24-
relative = false,
25-
}) {
19+
const generateTasks = ({ config, cwd = process.cwd(), gitDir, files, relative = false }) => {
2620
debug('Generating linter tasks')
2721

2822
const absoluteFiles = files.map((file) => normalize(path.resolve(gitDir, file)))
2923
const relativeFiles = absoluteFiles.map((file) => normalize(path.relative(cwd, file)))
3024

31-
return Object.entries(config).map(([pattern, commands]) => {
25+
return Object.entries(config).map(([rawPattern, commands]) => {
26+
let pattern = rawPattern
27+
3228
const isParentDirPattern = pattern.startsWith('../')
3329

34-
const fileList = micromatch(
35-
relativeFiles
36-
// Only worry about children of the CWD unless the pattern explicitly
37-
// specifies that it concerns a parent directory.
38-
.filter((file) => {
39-
if (isParentDirPattern) return true
40-
return !file.startsWith('..') && !path.isAbsolute(file)
41-
}),
42-
pattern,
43-
{
44-
cwd,
45-
dot: true,
46-
// If pattern doesn't look like a path, enable `matchBase` to
47-
// match against filenames in every directory. This makes `*.js`
48-
// match both `test.js` and `subdirectory/test.js`.
49-
matchBase: !pattern.includes('/'),
50-
}
51-
).map((file) => normalize(relative ? file : path.resolve(cwd, file)))
30+
// Only worry about children of the CWD unless the pattern explicitly
31+
// specifies that it concerns a parent directory.
32+
const filteredFiles = relativeFiles.filter((file) => {
33+
if (isParentDirPattern) return true
34+
return !file.startsWith('..') && !path.isAbsolute(file)
35+
})
36+
37+
const matches = micromatch(filteredFiles, pattern, {
38+
cwd,
39+
dot: true,
40+
// If the pattern doesn't look like a path, enable `matchBase` to
41+
// match against filenames in every directory. This makes `*.js`
42+
// match both `test.js` and `subdirectory/test.js`.
43+
matchBase: !pattern.includes('/'),
44+
strictBrackets: true,
45+
})
46+
47+
const fileList = matches.map((file) => normalize(relative ? file : path.resolve(cwd, file)))
5248

5349
const task = { pattern, commands, fileList }
5450
debug('Generated task: \n%O', task)
5551

5652
return task
5753
})
5854
}
55+
56+
module.exports = generateTasks

‎lib/index.js

+62-92
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
'use strict'
22

3-
const dedent = require('dedent')
43
const { cosmiconfig } = require('cosmiconfig')
54
const debugLog = require('debug')('lint-staged')
65
const stringifyObject = require('stringify-object')
@@ -13,9 +12,7 @@ const {
1312
ConfigNotFoundError,
1413
GetBackupStashError,
1514
GitError,
16-
InvalidOptionsError,
1715
} = require('./symbols')
18-
const formatConfig = require('./formatConfig')
1916
const validateConfig = require('./validateConfig')
2017
const validateOptions = require('./validateOptions')
2118

@@ -85,105 +82,78 @@ const lintStaged = async (
8582
} = {},
8683
logger = console
8784
) => {
88-
try {
89-
await validateOptions({ shell }, logger)
85+
await validateOptions({ shell }, logger)
9086

91-
debugLog('Loading config using `cosmiconfig`')
87+
debugLog('Loading config using `cosmiconfig`')
9288

93-
const resolved = configObject
94-
? { config: configObject, filepath: '(input)' }
95-
: await loadConfig(configPath)
89+
const resolved = configObject
90+
? { config: configObject, filepath: '(input)' }
91+
: await loadConfig(configPath)
9692

97-
if (resolved == null) {
98-
throw ConfigNotFoundError
99-
}
93+
if (resolved == null) {
94+
logger.error(`${ConfigNotFoundError.message}.`)
95+
throw ConfigNotFoundError
96+
}
10097

101-
debugLog('Successfully loaded config from `%s`:\n%O', resolved.filepath, resolved.config)
102-
103-
// resolved.config is the parsed configuration object
104-
// resolved.filepath is the path to the config file that was found
105-
const formattedConfig = formatConfig(resolved.config)
106-
const config = validateConfig(formattedConfig)
107-
108-
if (debug) {
109-
// Log using logger to be able to test through `consolemock`.
110-
logger.log('Running lint-staged with the following config:')
111-
logger.log(stringifyObject(config, { indent: ' ' }))
112-
} else {
113-
// We might not be in debug mode but `DEBUG=lint-staged*` could have
114-
// been set.
115-
debugLog('lint-staged config:\n%O', config)
116-
}
98+
debugLog('Successfully loaded config from `%s`:\n%O', resolved.filepath, resolved.config)
11799

118-
// Unset GIT_LITERAL_PATHSPECS to not mess with path interpretation
119-
debugLog('Unset GIT_LITERAL_PATHSPECS (was `%s`)', process.env.GIT_LITERAL_PATHSPECS)
120-
delete process.env.GIT_LITERAL_PATHSPECS
121-
122-
try {
123-
const ctx = await runAll(
124-
{
125-
allowEmpty,
126-
concurrent,
127-
config,
128-
cwd,
129-
debug,
130-
maxArgLength,
131-
quiet,
132-
relative,
133-
shell,
134-
stash,
135-
verbose,
136-
},
137-
logger
138-
)
139-
debugLog('Tasks were executed successfully!')
140-
printTaskOutput(ctx, logger)
141-
return true
142-
} catch (runAllError) {
143-
if (runAllError && runAllError.ctx && runAllError.ctx.errors) {
144-
const { ctx } = runAllError
145-
if (ctx.errors.has(ApplyEmptyCommitError)) {
146-
logger.warn(PREVENTED_EMPTY_COMMIT)
147-
} else if (ctx.errors.has(GitError) && !ctx.errors.has(GetBackupStashError)) {
148-
logger.error(GIT_ERROR)
149-
if (ctx.shouldBackup) {
150-
// No sense to show this if the backup stash itself is missing.
151-
logger.error(RESTORE_STASH_EXAMPLE)
152-
}
153-
}
100+
// resolved.config is the parsed configuration object
101+
// resolved.filepath is the path to the config file that was found
102+
const config = validateConfig(resolved.config, logger)
154103

155-
printTaskOutput(ctx, logger)
156-
return false
157-
}
104+
if (debug) {
105+
// Log using logger to be able to test through `consolemock`.
106+
logger.log('Running lint-staged with the following config:')
107+
logger.log(stringifyObject(config, { indent: ' ' }))
108+
} else {
109+
// We might not be in debug mode but `DEBUG=lint-staged*` could have
110+
// been set.
111+
debugLog('lint-staged config:\n%O', config)
112+
}
158113

159-
// Probably a compilation error in the config js file. Pass it up to the outer error handler for logging.
160-
throw runAllError
161-
}
162-
} catch (lintStagedError) {
163-
/** throw early because `validateOptions` options contains own logging */
164-
if (lintStagedError === InvalidOptionsError) {
165-
throw InvalidOptionsError
166-
}
114+
// Unset GIT_LITERAL_PATHSPECS to not mess with path interpretation
115+
debugLog('Unset GIT_LITERAL_PATHSPECS (was `%s`)', process.env.GIT_LITERAL_PATHSPECS)
116+
delete process.env.GIT_LITERAL_PATHSPECS
167117

168-
/** @todo move logging to `validateConfig` and remove this try/catch block */
169-
if (lintStagedError === ConfigNotFoundError) {
170-
logger.error(`${lintStagedError.message}.`)
171-
} else {
172-
// It was probably a parsing error
173-
logger.error(dedent`
174-
Could not parse lint-staged config.
118+
try {
119+
const ctx = await runAll(
120+
{
121+
allowEmpty,
122+
concurrent,
123+
config,
124+
cwd,
125+
debug,
126+
maxArgLength,
127+
quiet,
128+
relative,
129+
shell,
130+
stash,
131+
verbose,
132+
},
133+
logger
134+
)
135+
debugLog('Tasks were executed successfully!')
136+
printTaskOutput(ctx, logger)
137+
return true
138+
} catch (runAllError) {
139+
if (runAllError && runAllError.ctx && runAllError.ctx.errors) {
140+
const { ctx } = runAllError
141+
if (ctx.errors.has(ApplyEmptyCommitError)) {
142+
logger.warn(PREVENTED_EMPTY_COMMIT)
143+
} else if (ctx.errors.has(GitError) && !ctx.errors.has(GetBackupStashError)) {
144+
logger.error(GIT_ERROR)
145+
if (ctx.shouldBackup) {
146+
// No sense to show this if the backup stash itself is missing.
147+
logger.error(RESTORE_STASH_EXAMPLE)
148+
}
149+
}
175150

176-
${lintStagedError}
177-
`)
151+
printTaskOutput(ctx, logger)
152+
return false
178153
}
179-
logger.error() // empty line
180-
// Print helpful message for all errors
181-
logger.error(dedent`
182-
Please make sure you have created it correctly.
183-
See https://github.com/okonet/lint-staged#configuration.
184-
`)
185-
186-
throw lintStagedError
154+
155+
// Probably a compilation error in the config js file. Pass it up to the outer error handler for logging.
156+
throw runAllError
187157
}
188158
}
189159

‎lib/makeCmdTasks.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
const cliTruncate = require('cli-truncate')
44
const debug = require('debug')('lint-staged:make-cmd-tasks')
55

6+
const { configurationError } = require('./messages')
67
const resolveTaskFn = require('./resolveTaskFn')
7-
const { createError } = require('./validateConfig')
88

99
const STDOUT_COLUMNS_DEFAULT = 80
1010

@@ -51,7 +51,7 @@ const makeCmdTasks = async ({ commands, files, gitDir, renderer, shell, verbose
5151
// Do the validation here instead of `validateConfig` to skip evaluating the function multiple times
5252
if (isFn && typeof command !== 'string') {
5353
throw new Error(
54-
createError(
54+
configurationError(
5555
'[Function]',
5656
'Function task should return a string or an array of strings',
5757
resolved

‎lib/messages.js

+18-1
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,26 @@
22

33
const chalk = require('chalk')
44
const { error, info, warning } = require('log-symbols')
5+
const format = require('stringify-object')
6+
7+
const configurationError = (opt, helpMsg, value) =>
8+
`${chalk.redBright(`${error} Validation Error:`)}
9+
10+
Invalid value for '${chalk.bold(opt)}': ${chalk.bold(
11+
format(value, { inlineCharacterLimit: Number.POSITIVE_INFINITY })
12+
)}
13+
14+
${helpMsg}`
515

616
const NOT_GIT_REPO = chalk.redBright(`${error} Current directory is not a git directory!`)
717

818
const FAILED_GET_STAGED_FILES = chalk.redBright(`${error} Failed to get staged files!`)
919

20+
const incorrectBraces = (before, after) => `${warning} ${chalk.yellow(
21+
`Detected incorrect braces with only single value: \`${before}\`. Reformatted as: \`${after}\``
22+
)}
23+
`
24+
1025
const NO_STAGED_FILES = `${info} No staged files found.`
1126

1227
const NO_TASKS = `${info} No staged files match any configured task.`
@@ -29,7 +44,7 @@ const GIT_ERROR = `\n ${error} ${chalk.red(`lint-staged failed due to a git err
2944

3045
const invalidOption = (name, value, message) => `${chalk.redBright(`${error} Validation Error:`)}
3146
32-
Invalid value for option ${chalk.bold(name)}: ${chalk.bold(value)}
47+
Invalid value for option '${chalk.bold(name)}': ${chalk.bold(value)}
3348
3449
${message}
3550
@@ -51,9 +66,11 @@ const CONFIG_STDIN_ERROR = 'Error: Could not read config from stdin.'
5166

5267
module.exports = {
5368
CONFIG_STDIN_ERROR,
69+
configurationError,
5470
DEPRECATED_GIT_ADD,
5571
FAILED_GET_STAGED_FILES,
5672
GIT_ERROR,
73+
incorrectBraces,
5774
invalidOption,
5875
NO_STAGED_FILES,
5976
NO_TASKS,

‎lib/validateBraces.js

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
const { incorrectBraces } = require('./messages')
2+
3+
/**
4+
* A correctly-formed brace expansion must contain unquoted opening and closing braces,
5+
* and at least one unquoted comma or a valid sequence expression.
6+
* Any incorrectly formed brace expansion is left unchanged.
7+
*
8+
* @see https://www.gnu.org/software/bash/manual/html_node/Brace-Expansion.html
9+
*
10+
* Lint-staged uses `micromatch` for brace expansion, and its behavior is to treat
11+
* invalid brace expansions as literal strings, which means they (typically) do not match
12+
* anything.
13+
*
14+
* This RegExp tries to match most cases of invalid brace expansions, so that they can be
15+
* detected, warned about, and re-formatted by removing the braces and thus hopefully
16+
* matching the files as intended by the user. The only real fix is to remove the incorrect
17+
* braces from user configuration, but this is left to the user (after seeing the warning).
18+
*
19+
* @example <caption>Globs with brace expansions</caption>
20+
* - *.{js,tx} // expanded as *.js, *.ts
21+
* - *.{{j,t}s,css} // expanded as *.js, *.ts, *.css
22+
* - file_{1..10}.css // expanded as file_1.css, file_2.css, …, file_10.css
23+
*
24+
* @example <caption>Globs with incorrect brace expansions</caption>
25+
* - *.{js} // should just be *.js
26+
* - *.{js,{ts}} // should just be *.{js,ts}
27+
* - *.\{js\} // escaped braces, so they're treated literally
28+
* - *.${js} // dollar-sign inhibits expansion, so treated literally
29+
* - *.{js\,ts} // the comma is escaped, so treated literally
30+
*/
31+
const BRACES_REGEXP = /(?<![\\$])({)(?:(?!(?<!\\),|\.\.|\{|\}).)*?(?<!\\)(})/g
32+
33+
/**
34+
* @param {string} pattern
35+
* @returns {string}
36+
*/
37+
const withoutIncorrectBraces = (pattern) => {
38+
let output = `${pattern}`
39+
let match = null
40+
41+
while ((match = BRACES_REGEXP.exec(pattern))) {
42+
const fullMatch = match[0]
43+
const withoutBraces = fullMatch.replace(/{/, '').replace(/}/, '')
44+
output = output.replace(fullMatch, withoutBraces)
45+
}
46+
47+
return output
48+
}
49+
50+
/**
51+
* Validate and remove incorrect brace expansions from glob pattern.
52+
* For example `*.{js}` is incorrect because it doesn't contain a `,` or `..`,
53+
* and will be reformatted as `*.js`.
54+
*
55+
* @param {string} pattern the glob pattern
56+
* @param {*} logger
57+
* @returns {string}
58+
*/
59+
const validateBraces = (pattern, logger) => {
60+
const fixedPattern = withoutIncorrectBraces(pattern)
61+
62+
if (fixedPattern !== pattern) {
63+
logger.warn(incorrectBraces(pattern, fixedPattern))
64+
}
65+
66+
return fixedPattern
67+
}
68+
69+
module.exports = validateBraces
70+
71+
module.exports.BRACES_REGEXP = BRACES_REGEXP

‎lib/validateConfig.js

+71-56
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
'use strict'
44

5-
const chalk = require('chalk')
6-
const format = require('stringify-object')
7-
85
const debug = require('debug')('lint-staged:cfg')
96

7+
const { configurationError } = require('./messages')
8+
const validateBraces = require('./validateBraces')
9+
1010
const TEST_DEPRECATED_KEYS = new Map([
1111
['concurrent', (key) => typeof key === 'boolean'],
1212
['chunkSize', (key) => typeof key === 'number'],
@@ -18,76 +18,91 @@ const TEST_DEPRECATED_KEYS = new Map([
1818
['relative', (key) => typeof key === 'boolean'],
1919
])
2020

21-
const formatError = (helpMsg) => `● Validation Error:
22-
23-
${helpMsg}
24-
25-
Please refer to https://github.com/okonet/lint-staged#configuration for more information...`
26-
27-
const createError = (opt, helpMsg, value) =>
28-
formatError(`Invalid value for '${chalk.bold(opt)}'.
29-
30-
${helpMsg}.
31-
32-
Configured value is: ${chalk.bold(
33-
format(value, { inlineCharacterLimit: Number.POSITIVE_INFINITY })
34-
)}`)
35-
3621
/**
3722
* Runs config validation. Throws error if the config is not valid.
3823
* @param config {Object}
3924
* @returns config {Object}
4025
*/
41-
module.exports = function validateConfig(config) {
26+
const validateConfig = (config, logger) => {
4227
debug('Validating config')
4328

44-
const errors = []
29+
if (!config || (typeof config !== 'object' && typeof config !== 'function')) {
30+
throw new Error('Configuration should be an object or a function!')
31+
}
4532

46-
if (!config || typeof config !== 'object') {
47-
errors.push('Configuration should be an object!')
48-
} else {
49-
const entries = Object.entries(config)
33+
/**
34+
* Function configurations receive all staged files as their argument.
35+
* They are not further validated here to make sure the function gets
36+
* evaluated only once.
37+
*
38+
* @see makeCmdTasks
39+
*/
40+
if (typeof config === 'function') {
41+
return { '*': config }
42+
}
5043

51-
if (entries.length === 0) {
52-
errors.push('Configuration should not be empty!')
53-
}
44+
if (Object.entries(config).length === 0) {
45+
throw new Error('Configuration should not be empty!')
46+
}
5447

55-
entries.forEach(([pattern, task]) => {
56-
if (TEST_DEPRECATED_KEYS.has(pattern)) {
57-
const testFn = TEST_DEPRECATED_KEYS.get(pattern)
58-
if (testFn(task)) {
59-
errors.push(
60-
createError(
61-
pattern,
62-
'Advanced configuration has been deprecated. For more info, please visit: https://github.com/okonet/lint-staged',
63-
task
64-
)
65-
)
66-
}
67-
}
48+
const errors = []
6849

69-
if (
70-
(!Array.isArray(task) ||
71-
task.some((item) => typeof item !== 'string' && typeof item !== 'function')) &&
72-
typeof task !== 'string' &&
73-
typeof task !== 'function'
74-
) {
50+
/**
51+
* Create a new validated config because the keys (patterns) might change.
52+
* Since the Object.reduce method already loops through each entry in the config,
53+
* it can be used for validating the values at the same time.
54+
*/
55+
const validatedConfig = Object.entries(config).reduce((collection, [pattern, task]) => {
56+
/** Versions < 9 had more complex configuration options that are no longer supported. */
57+
if (TEST_DEPRECATED_KEYS.has(pattern)) {
58+
const testFn = TEST_DEPRECATED_KEYS.get(pattern)
59+
if (testFn(task)) {
7560
errors.push(
76-
createError(
77-
pattern,
78-
'Should be a string, a function, or an array of strings and functions',
79-
task
80-
)
61+
configurationError(pattern, 'Advanced configuration has been deprecated.', task)
8162
)
8263
}
83-
})
84-
}
64+
65+
/** Return early for deprecated keys to skip validating their (deprecated) values */
66+
return collection
67+
}
68+
69+
if (
70+
(!Array.isArray(task) ||
71+
task.some((item) => typeof item !== 'string' && typeof item !== 'function')) &&
72+
typeof task !== 'string' &&
73+
typeof task !== 'function'
74+
) {
75+
errors.push(
76+
configurationError(
77+
pattern,
78+
'Should be a string, a function, or an array of strings and functions.',
79+
task
80+
)
81+
)
82+
}
83+
84+
/**
85+
* A typical configuration error is using invalid brace expansion, like `*.{js}`.
86+
* These are automatically fixed and warned about.
87+
*/
88+
const fixedPattern = validateBraces(pattern, logger)
89+
90+
return { ...collection, [fixedPattern]: task }
91+
}, {})
8592

8693
if (errors.length) {
87-
throw new Error(errors.join('\n'))
94+
const message = errors.join('\n\n')
95+
96+
logger.error(`Could not parse lint-staged config.
97+
98+
${message}
99+
100+
See https://github.com/okonet/lint-staged#configuration.`)
101+
102+
throw new Error(message)
88103
}
89104

90-
return config
105+
return validatedConfig
91106
}
92107

93-
module.exports.createError = createError
108+
module.exports = validateConfig

‎package.json

-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
"commander": "^7.2.0",
3333
"cosmiconfig": "^7.0.0",
3434
"debug": "^4.3.1",
35-
"dedent": "^0.7.0",
3635
"enquirer": "^2.3.6",
3736
"execa": "^5.0.0",
3837
"listr2": "^3.8.2",

‎test/__snapshots__/index2.spec.js.snap

-11
This file was deleted.
+62-100
Original file line numberDiff line numberDiff line change
@@ -1,154 +1,116 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3-
exports[`validateConfig should not throw and should print nothing for function config 1`] = `""`;
3+
exports[`validateConfig should throw and should print validation errors for invalid config 1`] = `
4+
"× Validation Error:
45
5-
exports[`validateConfig should not throw and should print nothing for function task 1`] = `""`;
6+
Invalid value for 'foo': false
67
7-
exports[`validateConfig should not throw and should print nothing for valid config 1`] = `""`;
8+
Should be a string, a function, or an array of strings and functions."
9+
`;
810

9-
exports[`validateConfig should not throw when config contains deprecated key but with valid task 1`] = `""`;
11+
exports[`validateConfig should throw and should print validation errors for invalid config 1 1`] = `"Configuration should be an object or a function!"`;
1012

11-
exports[`validateConfig should throw and should print validation errors for invalid config 1`] = `
12-
" Validation Error:
13+
exports[`validateConfig should throw when detecting deprecated advanced configuration 1`] = `
14+
"× Validation Error:
1315
14-
Invalid value for 'foo'.
16+
Invalid value for 'chunkSize': 10
1517
16-
Should be a string, a function, or an array of strings and functions.
17-
18-
Configured value is: false
18+
Advanced configuration has been deprecated.
1919
20-
Please refer to https://github.com/okonet/lint-staged#configuration for more information..."
21-
`;
20+
× Validation Error:
2221
23-
exports[`validateConfig should throw and should print validation errors for invalid config 1 1`] = `"Configuration should be an object!"`;
22+
Invalid value for 'concurrent': false
2423
25-
exports[`validateConfig should throw when detecting deprecated advanced configuration 1`] = `
26-
"● Validation Error:
24+
Advanced configuration has been deprecated.
2725
28-
Invalid value for 'chunkSize'.
26+
× Validation Error:
2927
30-
Advanced configuration has been deprecated. For more info, please visit: https://github.com/okonet/lint-staged.
31-
32-
Configured value is: 10
28+
Invalid value for 'globOptions': {matchBase: false}
3329
34-
Please refer to https://github.com/okonet/lint-staged#configuration for more information...
35-
● Validation Error:
30+
Advanced configuration has been deprecated.
3631
37-
Invalid value for 'chunkSize'.
32+
× Validation Error:
3833
39-
Should be a string, a function, or an array of strings and functions.
40-
41-
Configured value is: 10
34+
Invalid value for 'ignore': ['test.js']
4235
43-
Please refer to https://github.com/okonet/lint-staged#configuration for more information...
44-
● Validation Error:
36+
Advanced configuration has been deprecated.
4537
46-
Invalid value for 'concurrent'.
38+
× Validation Error:
4739
48-
Advanced configuration has been deprecated. For more info, please visit: https://github.com/okonet/lint-staged.
49-
50-
Configured value is: false
40+
Invalid value for 'linters': {'*.js': ['eslint']}
5141
52-
Please refer to https://github.com/okonet/lint-staged#configuration for more information...
53-
● Validation Error:
42+
Advanced configuration has been deprecated.
5443
55-
Invalid value for 'concurrent'.
44+
× Validation Error:
5645
57-
Should be a string, a function, or an array of strings and functions.
58-
59-
Configured value is: false
46+
Invalid value for 'relative': true
6047
61-
Please refer to https://github.com/okonet/lint-staged#configuration for more information...
62-
● Validation Error:
48+
Advanced configuration has been deprecated.
6349
64-
Invalid value for 'globOptions'.
50+
× Validation Error:
6551
66-
Advanced configuration has been deprecated. For more info, please visit: https://github.com/okonet/lint-staged.
67-
68-
Configured value is: {matchBase: false}
52+
Invalid value for 'renderer': 'silent'
6953
70-
Please refer to https://github.com/okonet/lint-staged#configuration for more information...
71-
● Validation Error:
54+
Advanced configuration has been deprecated.
7255
73-
Invalid value for 'globOptions'.
56+
× Validation Error:
7457
75-
Should be a string, a function, or an array of strings and functions.
76-
77-
Configured value is: {matchBase: false}
58+
Invalid value for 'subTaskConcurrency': 10
7859
79-
Please refer to https://github.com/okonet/lint-staged#configuration for more information...
80-
● Validation Error:
60+
Advanced configuration has been deprecated."
61+
`;
8162

82-
Invalid value for 'ignore'.
63+
exports[`validateConfig should throw when detecting deprecated advanced configuration 2`] = `
64+
"
65+
ERROR Could not parse lint-staged config.
8366
84-
Advanced configuration has been deprecated. For more info, please visit: https://github.com/okonet/lint-staged.
85-
86-
Configured value is: ['test.js']
67+
× Validation Error:
8768
88-
Please refer to https://github.com/okonet/lint-staged#configuration for more information...
89-
● Validation Error:
69+
Invalid value for 'chunkSize': 10
9070
91-
Invalid value for 'linters'.
71+
Advanced configuration has been deprecated.
9272
93-
Advanced configuration has been deprecated. For more info, please visit: https://github.com/okonet/lint-staged.
94-
95-
Configured value is: {'*.js': ['eslint']}
73+
× Validation Error:
9674
97-
Please refer to https://github.com/okonet/lint-staged#configuration for more information...
98-
● Validation Error:
75+
Invalid value for 'concurrent': false
9976
100-
Invalid value for 'linters'.
77+
Advanced configuration has been deprecated.
10178
102-
Should be a string, a function, or an array of strings and functions.
103-
104-
Configured value is: {'*.js': ['eslint']}
79+
× Validation Error:
10580
106-
Please refer to https://github.com/okonet/lint-staged#configuration for more information...
107-
● Validation Error:
81+
Invalid value for 'globOptions': {matchBase: false}
10882
109-
Invalid value for 'relative'.
83+
Advanced configuration has been deprecated.
11084
111-
Advanced configuration has been deprecated. For more info, please visit: https://github.com/okonet/lint-staged.
112-
113-
Configured value is: true
85+
× Validation Error:
11486
115-
Please refer to https://github.com/okonet/lint-staged#configuration for more information...
116-
● Validation Error:
87+
Invalid value for 'ignore': ['test.js']
11788
118-
Invalid value for 'relative'.
89+
Advanced configuration has been deprecated.
11990
120-
Should be a string, a function, or an array of strings and functions.
121-
122-
Configured value is: true
91+
× Validation Error:
12392
124-
Please refer to https://github.com/okonet/lint-staged#configuration for more information...
125-
● Validation Error:
93+
Invalid value for 'linters': {'*.js': ['eslint']}
12694
127-
Invalid value for 'renderer'.
95+
Advanced configuration has been deprecated.
12896
129-
Advanced configuration has been deprecated. For more info, please visit: https://github.com/okonet/lint-staged.
130-
131-
Configured value is: 'silent'
97+
× Validation Error:
13298
133-
Please refer to https://github.com/okonet/lint-staged#configuration for more information...
134-
● Validation Error:
99+
Invalid value for 'relative': true
135100
136-
Invalid value for 'subTaskConcurrency'.
101+
Advanced configuration has been deprecated.
137102
138-
Advanced configuration has been deprecated. For more info, please visit: https://github.com/okonet/lint-staged.
139-
140-
Configured value is: 10
103+
× Validation Error:
141104
142-
Please refer to https://github.com/okonet/lint-staged#configuration for more information...
143-
● Validation Error:
105+
Invalid value for 'renderer': 'silent'
144106
145-
Invalid value for 'subTaskConcurrency'.
107+
Advanced configuration has been deprecated.
146108
147-
Should be a string, a function, or an array of strings and functions.
148-
149-
Configured value is: 10
109+
× Validation Error:
150110
151-
Please refer to https://github.com/okonet/lint-staged#configuration for more information..."
152-
`;
111+
Invalid value for 'subTaskConcurrency': 10
153112
154-
exports[`validateConfig should throw when detecting deprecated advanced configuration 2`] = `""`;
113+
Advanced configuration has been deprecated.
114+
115+
See https://github.com/okonet/lint-staged#configuration."
116+
`;

‎test/formatConfig.spec.js

-17
This file was deleted.

‎test/generateTasks.spec.js

+23-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import os from 'os'
21
import normalize from 'normalize-path'
2+
import os from 'os'
33
import path from 'path'
44

55
import generateTasks from '../lib/generateTasks'
@@ -15,7 +15,7 @@ const files = [
1515
'.hidden/test.js',
1616

1717
'test.css',
18-
'deeper/test.css',
18+
'deeper/test1.css',
1919
'deeper/test2.css',
2020
'even/deeper/test.css',
2121
'.hidden/test.css',
@@ -145,14 +145,33 @@ describe('generateTasks', () => {
145145
`${gitDir}/even/deeper/test.js`,
146146
`${gitDir}/.hidden/test.js`,
147147
`${gitDir}/test.css`,
148-
`${gitDir}/deeper/test.css`,
148+
`${gitDir}/deeper/test1.css`,
149149
`${gitDir}/deeper/test2.css`,
150150
`${gitDir}/even/deeper/test.css`,
151151
`${gitDir}/.hidden/test.css`,
152152
].map(normalizePath),
153153
})
154154
})
155155

156+
it('should match pattern "test{1..2}.css"', async () => {
157+
const result = await generateTasks({
158+
config: {
159+
'test{1..2}.css': 'lint',
160+
},
161+
cwd,
162+
gitDir,
163+
files,
164+
})
165+
166+
const linter = result.find((item) => item.pattern === 'test{1..2}.css')
167+
168+
expect(linter).toEqual({
169+
pattern: 'test{1..2}.css',
170+
commands: 'lint',
171+
fileList: [`${gitDir}/deeper/test1.css`, `${gitDir}/deeper/test2.css`].map(normalizePath),
172+
})
173+
})
174+
156175
it('should not match files in parent directory by default', async () => {
157176
const result = await generateTasks({
158177
config,
@@ -196,7 +215,7 @@ describe('generateTasks', () => {
196215
'even/deeper/test.js',
197216
'.hidden/test.js',
198217
'test.css',
199-
'deeper/test.css',
218+
'deeper/test1.css',
200219
'deeper/test2.css',
201220
'even/deeper/test.css',
202221
'.hidden/test.css',

‎test/index.spec.js

+6-33
Original file line numberDiff line numberDiff line change
@@ -70,15 +70,7 @@ describe('lintStaged', () => {
7070
`[Error: Configuration should not be empty!]`
7171
)
7272

73-
expect(mockedConsole.printHistory()).toMatchInlineSnapshot(`
74-
"
75-
ERROR Could not parse lint-staged config.
76-
77-
Error: Configuration should not be empty!
78-
ERROR
79-
ERROR Please make sure you have created it correctly.
80-
See https://github.com/okonet/lint-staged#configuration."
81-
`)
73+
expect(mockedConsole.printHistory()).toMatchInlineSnapshot(`""`)
8274

8375
console = previousConsole
8476
})
@@ -133,15 +125,7 @@ describe('lintStaged', () => {
133125
`[Error: Configuration should not be empty!]`
134126
)
135127

136-
expect(logger.printHistory()).toMatchInlineSnapshot(`
137-
"
138-
ERROR Could not parse lint-staged config.
139-
140-
Error: Configuration should not be empty!
141-
ERROR
142-
ERROR Please make sure you have created it correctly.
143-
See https://github.com/okonet/lint-staged#configuration."
144-
`)
128+
expect(logger.printHistory()).toMatchInlineSnapshot(`""`)
145129
})
146130

147131
it('should load config file when specified', async () => {
@@ -224,10 +208,7 @@ describe('lintStaged', () => {
224208

225209
expect(logger.printHistory()).toMatchInlineSnapshot(`
226210
"
227-
ERROR Config could not be found.
228-
ERROR
229-
ERROR Please make sure you have created it correctly.
230-
See https://github.com/okonet/lint-staged#configuration."
211+
ERROR Config could not be found."
231212
`)
232213
})
233214

@@ -239,23 +220,15 @@ describe('lintStaged', () => {
239220
// Serialize Windows, Linux and MacOS paths consistently
240221
expect.addSnapshotSerializer(
241222
replaceSerializer(
242-
/Error: ENOENT: no such file or directory, open '([^']+)'/,
243-
`Error: ENOENT: no such file or directory, open '${nonExistentConfig}'`
223+
/ENOENT: no such file or directory, open '([^']+)'/,
224+
`ENOENT: no such file or directory, open '${nonExistentConfig}'`
244225
)
245226
)
246227

247228
await expect(
248229
lintStaged({ configPath: nonExistentConfig, quiet: true }, logger)
249230
).rejects.toThrowError()
250231

251-
expect(logger.printHistory()).toMatchInlineSnapshot(`
252-
253-
ERROR Could not parse lint-staged config.
254-
255-
Error: ENOENT: no such file or directory, open 'fake-config-file.yml'
256-
ERROR
257-
ERROR Please make sure you have created it correctly.
258-
See https://github.com/okonet/lint-staged#configuration.
259-
`)
232+
expect(logger.printHistory()).toMatchInlineSnapshot(`""`)
260233
})
261234
})

‎test/index2.spec.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ describe('lintStaged', () => {
7777
await expect(lintStaged({ config }, logger)).rejects.toThrowErrorMatchingInlineSnapshot(
7878
`"failed config"`
7979
)
80-
expect(logger.printHistory()).toMatchSnapshot()
80+
81+
expect(logger.printHistory()).toMatchInlineSnapshot(`""`)
8182
})
8283
})

‎test/makeCmdTasks.spec.js

+4-8
Original file line numberDiff line numberDiff line change
@@ -109,16 +109,12 @@ describe('makeCmdTasks', () => {
109109
it("should throw when function task doesn't return string | string[]", async () => {
110110
await expect(makeCmdTasks({ commands: () => null, gitDir, files: ['test.js'] })).rejects
111111
.toThrowErrorMatchingInlineSnapshot(`
112-
"● Validation Error:
112+
Validation Error:
113113
114-
Invalid value for '[Function]'.
114+
Invalid value for '[Function]': null
115115
116-
Function task should return a string or an array of strings.
117-
118-
Configured value is: null
119-
120-
Please refer to https://github.com/okonet/lint-staged#configuration for more information..."
121-
`)
116+
Function task should return a string or an array of strings"
117+
`)
122118
})
123119

124120
it('should truncate task title', async () => {

‎test/validateBraces.spec.js

+124
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import makeConsoleMock from 'consolemock'
2+
3+
import validateBraces, { BRACES_REGEXP } from '../lib/validateBraces'
4+
5+
describe('BRACES_REGEXP', () => {
6+
it(`should match '*.{js}'`, () => {
7+
expect('*.{js}'.match(BRACES_REGEXP)).toBeTruthy()
8+
})
9+
10+
it(`should match 'file_{10}'`, () => {
11+
expect('file_{test}'.match(BRACES_REGEXP)).toBeTruthy()
12+
})
13+
14+
it(`should match '*.{spec\\.js}'`, () => {
15+
expect('*.{spec\\.js}'.match(BRACES_REGEXP)).toBeTruthy()
16+
})
17+
18+
it(`should match '*.{js\\,ts}'`, () => {
19+
expect('*.{js\\,ts}'.match(BRACES_REGEXP)).toBeTruthy()
20+
})
21+
22+
it("should not match '*.${js}'", () => {
23+
expect('*.${js}'.match(BRACES_REGEXP)).not.toBeTruthy()
24+
})
25+
26+
it(`should not match '.{js,ts}'`, () => {
27+
expect('.{js,ts}'.match(BRACES_REGEXP)).not.toBeTruthy()
28+
})
29+
30+
it(`should not match 'file_{1..10}'`, () => {
31+
expect('file_{1..10}'.match(BRACES_REGEXP)).not.toBeTruthy()
32+
})
33+
34+
it(`should not match '*.\\{js\\}'`, () => {
35+
expect('*.\\{js\\}'.match(BRACES_REGEXP)).not.toBeTruthy()
36+
})
37+
38+
it(`should not match '*.\\{js}'`, () => {
39+
expect('*.\\{js}'.match(BRACES_REGEXP)).not.toBeTruthy()
40+
})
41+
42+
it(`should not match '*.{js\\}'`, () => {
43+
expect('*.{js\\}'.match(BRACES_REGEXP)).not.toBeTruthy()
44+
})
45+
})
46+
47+
describe('validateBraces', () => {
48+
it('should warn about `*.{js}` and return fixed pattern', () => {
49+
const logger = makeConsoleMock()
50+
51+
const fixedBraces = validateBraces('*.{js}', logger)
52+
53+
expect(fixedBraces).toEqual('*.js')
54+
expect(logger.printHistory()).toMatchInlineSnapshot(`
55+
"
56+
WARN ‼ Detected incorrect braces with only single value: \`*.{js}\`. Reformatted as: \`*.js\`
57+
"
58+
`)
59+
})
60+
61+
it('should warn about `*.{ts}{x}` and return fixed pattern', () => {
62+
const logger = makeConsoleMock()
63+
64+
const fixedBraces = validateBraces('*.{ts}{x}', logger)
65+
66+
expect(fixedBraces).toEqual('*.tsx')
67+
expect(logger.printHistory()).toMatchInlineSnapshot(`
68+
"
69+
WARN ‼ Detected incorrect braces with only single value: \`*.{ts}{x}\`. Reformatted as: \`*.tsx\`
70+
"
71+
`)
72+
})
73+
74+
it('should warn about `*.{js,{ts}}` and return fixed pattern', () => {
75+
const logger = makeConsoleMock()
76+
77+
const fixedBraces = validateBraces('*.{js,{ts}}', logger)
78+
79+
expect(fixedBraces).toEqual('*.{js,ts}')
80+
expect(logger.printHistory()).toMatchInlineSnapshot(`
81+
"
82+
WARN ‼ Detected incorrect braces with only single value: \`*.{js,{ts}}\`. Reformatted as: \`*.{js,ts}\`
83+
"
84+
`)
85+
})
86+
87+
/**
88+
* @todo This isn't correctly detected even though the outer braces are invalid.
89+
*/
90+
it.skip('should warn about `*.{{js,ts}}` and return fixed pattern', () => {
91+
const logger = makeConsoleMock()
92+
93+
const fixedBraces = validateBraces('*.{{js,ts}}', logger)
94+
95+
expect(fixedBraces).toEqual('*.{js,ts}')
96+
expect(logger.printHistory()).toMatchInlineSnapshot(`
97+
"
98+
WARN ‼ Detected incorrect braces with only single value: \`*.{{js,ts}}\`. Reformatted as: \`*.{js,ts}\`
99+
"
100+
`)
101+
})
102+
103+
it('should warn about `*.{{js,ts},{css}}` and return fixed pattern', () => {
104+
const logger = makeConsoleMock()
105+
106+
const fixedBraces = validateBraces('*.{{js,ts},{css}}', logger)
107+
108+
expect(fixedBraces).toEqual('*.{{js,ts},css}')
109+
expect(logger.printHistory()).toMatchInlineSnapshot(`
110+
"
111+
WARN ‼ Detected incorrect braces with only single value: \`*.{{js,ts},{css}}\`. Reformatted as: \`*.{{js,ts},css}\`
112+
"
113+
`)
114+
})
115+
116+
it('should not warn about `*.\\{js\\}` and return the same pattern', () => {
117+
const logger = makeConsoleMock()
118+
119+
const fixedBraces = validateBraces('*.\\{js\\}', logger)
120+
121+
expect(fixedBraces).toEqual('*.\\{js\\}')
122+
expect(logger.printHistory()).toMatchInlineSnapshot(`""`)
123+
})
124+
})

‎test/validateConfig.spec.js

+32-34
Original file line numberDiff line numberDiff line change
@@ -2,59 +2,56 @@ import makeConsoleMock from 'consolemock'
22

33
import validateConfig from '../lib/validateConfig'
44

5-
import formatConfig from '../lib/formatConfig'
6-
75
describe('validateConfig', () => {
8-
const originalConsole = global.console
9-
beforeAll(() => {
10-
global.console = makeConsoleMock()
11-
})
6+
let logger
127

138
beforeEach(() => {
14-
global.console.clearHistory()
15-
})
16-
17-
afterAll(() => {
18-
global.console = originalConsole
9+
logger = makeConsoleMock()
1910
})
2011

2112
it('should throw and should print validation errors for invalid config 1', () => {
2213
const invalidConfig = 'test'
23-
expect(() => validateConfig(invalidConfig)).toThrowErrorMatchingSnapshot()
14+
15+
expect(() => validateConfig(invalidConfig, logger)).toThrowErrorMatchingSnapshot()
2416
})
2517

2618
it('should throw and should print validation errors for invalid config', () => {
2719
const invalidConfig = {
2820
foo: false,
2921
}
30-
expect(() => validateConfig(invalidConfig)).toThrowErrorMatchingSnapshot()
22+
23+
expect(() => validateConfig(invalidConfig, logger)).toThrowErrorMatchingSnapshot()
24+
})
25+
26+
it('should wrap function config into object', () => {
27+
const functionConfig = (stagedFiles) => [`eslint --fix ${stagedFiles}', 'git add`]
28+
29+
expect(validateConfig(functionConfig, logger)).toEqual({
30+
'*': functionConfig,
31+
})
32+
expect(logger.printHistory()).toEqual('')
3133
})
3234

3335
it('should not throw and should print nothing for valid config', () => {
3436
const validSimpleConfig = {
3537
'*.js': ['eslint --fix', 'git add'],
3638
}
37-
expect(() => validateConfig(validSimpleConfig)).not.toThrow()
38-
expect(console.printHistory()).toMatchSnapshot()
39-
})
4039

41-
it('should not throw and should print nothing for function config', () => {
42-
const functionConfig = (stagedFiles) => [`eslint ${stagedFiles.join(' ')}`]
43-
expect(() => validateConfig(formatConfig(functionConfig))).not.toThrow()
44-
expect(console.printHistory()).toMatchSnapshot()
40+
expect(() => validateConfig(validSimpleConfig, logger)).not.toThrow()
41+
expect(logger.printHistory()).toEqual('')
4542
})
4643

4744
it('should not throw and should print nothing for function task', () => {
48-
expect(() =>
49-
validateConfig({
50-
'*.js': (filenames) => {
51-
const files = filenames.join(' ')
52-
return `eslint --fix ${files} && git add ${files}`
53-
},
54-
'*.css': [(filenames) => filenames.map((filename) => `eslint --fix ${filename}`)],
55-
})
56-
).not.toThrow()
57-
expect(console.printHistory()).toMatchSnapshot()
45+
const functionTask = {
46+
'*.js': (filenames) => {
47+
const files = filenames.join(' ')
48+
return `eslint --fix ${files} && git add ${files}`
49+
},
50+
'*.css': [(filenames) => filenames.map((filename) => `eslint --fix ${filename}`)],
51+
}
52+
53+
expect(() => validateConfig(functionTask, logger)).not.toThrow()
54+
expect(logger.printHistory()).toEqual('')
5855
})
5956

6057
it('should throw when detecting deprecated advanced configuration', () => {
@@ -71,15 +68,16 @@ describe('validateConfig', () => {
7168
subTaskConcurrency: 10,
7269
}
7370

74-
expect(() => validateConfig(advancedConfig)).toThrowErrorMatchingSnapshot()
75-
expect(console.printHistory()).toMatchSnapshot()
71+
expect(() => validateConfig(advancedConfig, logger)).toThrowErrorMatchingSnapshot()
72+
expect(logger.printHistory()).toMatchSnapshot()
7673
})
7774

7875
it('should not throw when config contains deprecated key but with valid task', () => {
7976
const stillValidConfig = {
8077
concurrent: 'my command',
8178
}
82-
expect(() => validateConfig(stillValidConfig)).not.toThrow()
83-
expect(console.printHistory()).toMatchSnapshot()
79+
80+
expect(() => validateConfig(stillValidConfig, logger)).not.toThrow()
81+
expect(logger.printHistory()).toEqual('')
8482
})
8583
})

‎test/validateOptions.spec.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ describe('validateOptions', () => {
4141

4242
const logger = makeConsoleMock()
4343

44-
mockAccess.mockImplementationOnce(() => new Promise.reject())
44+
mockAccess.mockImplementationOnce(() => Promise.reject(new Error('Failed')))
4545

4646
await expect(validateOptions({ shell: '/bin/sh' }, logger)).rejects.toThrowError(
4747
InvalidOptionsError
@@ -55,9 +55,9 @@ describe('validateOptions', () => {
5555
"
5656
ERROR × Validation Error:
5757
58-
Invalid value for option shell: /bin/sh
58+
Invalid value for option 'shell': /bin/sh
5959
60-
Promise.reject is not a constructor
60+
Failed
6161
6262
See https://github.com/okonet/lint-staged#command-line-flags"
6363
`)

‎yarn.lock

-5
Original file line numberDiff line numberDiff line change
@@ -2490,11 +2490,6 @@ decode-uri-component@^0.2.0:
24902490
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
24912491
integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=
24922492

2493-
dedent@^0.7.0:
2494-
version "0.7.0"
2495-
resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c"
2496-
integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=
2497-
24982493
deep-is@^0.1.3, deep-is@~0.1.3:
24992494
version "0.1.3"
25002495
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"

0 commit comments

Comments
 (0)
Please sign in to comment.