Skip to content

Commit 3836bb4

Browse files
authored
fix: do not crash on error in fs.walk filter (#18886)
1 parent 2dec349 commit 3836bb4

File tree

2 files changed

+112
-45
lines changed

2 files changed

+112
-45
lines changed

lib/eslint/eslint-helpers.js

Lines changed: 79 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ const fsp = fs.promises;
1515
const isGlob = require("is-glob");
1616
const hash = require("../cli-engine/hash");
1717
const minimatch = require("minimatch");
18-
const util = require("util");
1918
const fswalk = require("@nodelib/fs.walk");
2019
const globParent = require("glob-parent");
2120
const isPathInside = require("is-path-inside");
@@ -24,7 +23,6 @@ const isPathInside = require("is-path-inside");
2423
// Fixup references
2524
//-----------------------------------------------------------------------------
2625

27-
const doFsWalk = util.promisify(fswalk.walk);
2826
const Minimatch = minimatch.Minimatch;
2927
const MINIMATCH_OPTIONS = { dot: true };
3028

@@ -270,56 +268,92 @@ async function globSearch({
270268
*/
271269
const unmatchedPatterns = new Set([...relativeToPatterns.keys()]);
272270

273-
const filePaths = (await doFsWalk(basePath, {
271+
const filePaths = (await new Promise((resolve, reject) => {
274272

275-
deepFilter(entry) {
276-
const relativePath = normalizeToPosix(path.relative(basePath, entry.path));
277-
const matchesPattern = matchers.some(matcher => matcher.match(relativePath, true));
278-
279-
return matchesPattern && !configs.isDirectoryIgnored(entry.path);
280-
},
281-
entryFilter(entry) {
282-
const relativePath = normalizeToPosix(path.relative(basePath, entry.path));
273+
let promiseRejected = false;
283274

284-
// entries may be directories or files so filter out directories
285-
if (entry.dirent.isDirectory()) {
275+
/**
276+
* Wraps a boolean-returning filter function. The wrapped function will reject the promise if an error occurs.
277+
* @param {Function} filter A filter function to wrap.
278+
* @returns {Function} A function similar to the wrapped filter that rejects the promise if an error occurs.
279+
*/
280+
function wrapFilter(filter) {
281+
return (...args) => {
282+
283+
// No need to run the filter if an error has been thrown.
284+
if (!promiseRejected) {
285+
try {
286+
return filter(...args);
287+
} catch (error) {
288+
promiseRejected = true;
289+
reject(error);
290+
}
291+
}
286292
return false;
287-
}
293+
};
294+
}
288295

289-
/*
290-
* Optimization: We need to track when patterns are left unmatched
291-
* and so we use `unmatchedPatterns` to do that. There is a bit of
292-
* complexity here because the same file can be matched by more than
293-
* one pattern. So, when we start, we actually need to test every
294-
* pattern against every file. Once we know there are no remaining
295-
* unmatched patterns, then we can switch to just looking for the
296-
* first matching pattern for improved speed.
297-
*/
298-
const matchesPattern = unmatchedPatterns.size > 0
299-
? matchers.reduce((previousValue, matcher) => {
300-
const pathMatches = matcher.match(relativePath);
296+
fswalk.walk(
297+
basePath,
298+
{
299+
deepFilter: wrapFilter(entry => {
300+
const relativePath = normalizeToPosix(path.relative(basePath, entry.path));
301+
const matchesPattern = matchers.some(matcher => matcher.match(relativePath, true));
302+
303+
return matchesPattern && !configs.isDirectoryIgnored(entry.path);
304+
}),
305+
entryFilter: wrapFilter(entry => {
306+
const relativePath = normalizeToPosix(path.relative(basePath, entry.path));
307+
308+
// entries may be directories or files so filter out directories
309+
if (entry.dirent.isDirectory()) {
310+
return false;
311+
}
301312

302313
/*
303-
* We updated the unmatched patterns set only if the path
304-
* matches and the file isn't ignored. If the file is
305-
* ignored, that means there wasn't a match for the
306-
* pattern so it should not be removed.
307-
*
308-
* Performance note: isFileIgnored() aggressively caches
309-
* results so there is no performance penalty for calling
310-
* it twice with the same argument.
314+
* Optimization: We need to track when patterns are left unmatched
315+
* and so we use `unmatchedPatterns` to do that. There is a bit of
316+
* complexity here because the same file can be matched by more than
317+
* one pattern. So, when we start, we actually need to test every
318+
* pattern against every file. Once we know there are no remaining
319+
* unmatched patterns, then we can switch to just looking for the
320+
* first matching pattern for improved speed.
311321
*/
312-
if (pathMatches && !configs.isFileIgnored(entry.path)) {
313-
unmatchedPatterns.delete(matcher.pattern);
314-
}
315-
316-
return pathMatches || previousValue;
317-
}, false)
318-
: matchers.some(matcher => matcher.match(relativePath));
319-
320-
return matchesPattern && !configs.isFileIgnored(entry.path);
321-
}
322-
322+
const matchesPattern = unmatchedPatterns.size > 0
323+
? matchers.reduce((previousValue, matcher) => {
324+
const pathMatches = matcher.match(relativePath);
325+
326+
/*
327+
* We updated the unmatched patterns set only if the path
328+
* matches and the file isn't ignored. If the file is
329+
* ignored, that means there wasn't a match for the
330+
* pattern so it should not be removed.
331+
*
332+
* Performance note: isFileIgnored() aggressively caches
333+
* results so there is no performance penalty for calling
334+
* it twice with the same argument.
335+
*/
336+
if (pathMatches && !configs.isFileIgnored(entry.path)) {
337+
unmatchedPatterns.delete(matcher.pattern);
338+
}
339+
340+
return pathMatches || previousValue;
341+
}, false)
342+
: matchers.some(matcher => matcher.match(relativePath));
343+
344+
return matchesPattern && !configs.isFileIgnored(entry.path);
345+
})
346+
},
347+
(error, entries) => {
348+
349+
// If the promise is already rejected, calling `resolve` or `reject` will do nothing.
350+
if (error) {
351+
reject(error);
352+
} else {
353+
resolve(entries);
354+
}
355+
}
356+
);
323357
})).map(entry => entry.path);
324358

325359
// now check to see if we have any unmatched patterns

tests/lib/eslint/flat-eslint.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4541,7 +4541,40 @@ describe("FlatESLint", () => {
45414541
});
45424542
});
45434543

4544+
describe("Error while globbing", () => {
45444545

4546+
it("should throw an error with a glob pattern if an invalid config was provided", async () => {
4547+
4548+
const cwd = getFixturePath("files");
4549+
4550+
eslint = new FlatESLint({
4551+
cwd,
4552+
overrideConfig: [{ invalid: "foobar" }]
4553+
});
4554+
4555+
await assert.rejects(eslint.lintFiles("*.js"));
4556+
});
4557+
4558+
it("should throw an error with a glob pattern if an error occurs traversing a directory", async () => {
4559+
4560+
const fsWalk = require("@nodelib/fs.walk");
4561+
const error = new Error("Boom!");
4562+
4563+
sinon
4564+
.stub(fsWalk, "walk")
4565+
.value(sinon.stub().yieldsRight(error)); // call the callback passed to `fs.walk` with an error
4566+
4567+
const cwd = getFixturePath("files");
4568+
4569+
eslint = new FlatESLint({
4570+
cwd,
4571+
overrideConfigFile: true
4572+
});
4573+
4574+
await assert.rejects(eslint.lintFiles("*.js"), error);
4575+
});
4576+
4577+
});
45454578
});
45464579

45474580
describe("Fix Types", () => {

0 commit comments

Comments
 (0)