Skip to content

Commit dd0c58f

Browse files
authored
feat: Swap out Globby for custom globbing solution. (#16369)
* feat: Swap out Globby for custom globbing solution. Removes globby due to numerous issues with ignoring files and returning the correct files. Fixes #16354 Fixes #16340 Fixes #16300 * Fix failing test * Add test for 16300 * Make more tests pass * Fix another test * Add another test * Remove old test * Fix cli tests * Bump up Mocha timeout for Node 18 * Revert timeout bump * Manually use stream * clean up comment * Set concurrency back to default * Fix remaining tests * Cleanup requires
1 parent 232d291 commit dd0c58f

11 files changed

Lines changed: 217 additions & 40 deletions

File tree

lib/config/default-config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ exports.defaultConfig = [
5252
{
5353
ignores: [
5454
"**/node_modules/**",
55-
".git/**"
55+
".git/"
5656
]
5757
},
5858

lib/eslint/eslint-helpers.js

Lines changed: 136 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,17 @@ const path = require("path");
1313
const fs = require("fs");
1414
const fsp = fs.promises;
1515
const isGlob = require("is-glob");
16-
const globby = require("globby");
1716
const hash = require("../cli-engine/hash");
1817
const minimatch = require("minimatch");
18+
const util = require("util");
19+
const fswalk = require("@nodelib/fs.walk");
20+
21+
//-----------------------------------------------------------------------------
22+
// Fixup references
23+
//-----------------------------------------------------------------------------
24+
25+
const doFsWalk = util.promisify(fswalk.walk);
26+
const Minimatch = minimatch.Minimatch;
1927

2028
//-----------------------------------------------------------------------------
2129
// Errors
@@ -97,6 +105,120 @@ function isGlobPattern(pattern) {
97105
return isGlob(path.sep === "\\" ? normalizeToPosix(pattern) : pattern);
98106
}
99107

108+
/**
109+
* Searches a directory looking for matching glob patterns. This uses
110+
* the config array's logic to determine if a directory or file should
111+
* be ignored, so it is consistent with how ignoring works throughout
112+
* ESLint.
113+
* @param {Object} options The options for this function.
114+
* @param {string} options.cwd The directory to search.
115+
* @param {Array<string>} options.patterns An array of glob patterns
116+
* to match.
117+
* @param {FlatConfigArray} options.configs The config array to use for
118+
* determining what to ignore.
119+
* @returns {Promise<Array<string>>} An array of matching file paths
120+
* or an empty array if there are no matches.
121+
*/
122+
async function globSearch({ cwd, patterns, configs }) {
123+
124+
if (patterns.length === 0) {
125+
return [];
126+
}
127+
128+
const matchers = patterns.map(pattern => {
129+
const patternToUse = path.isAbsolute(pattern)
130+
? normalizeToPosix(path.relative(cwd, pattern))
131+
: pattern;
132+
133+
return new minimatch.Minimatch(patternToUse);
134+
});
135+
136+
return (await doFsWalk(cwd, {
137+
138+
deepFilter(entry) {
139+
const relativePath = normalizeToPosix(path.relative(cwd, entry.path));
140+
const matchesPattern = matchers.some(matcher => matcher.match(relativePath, true));
141+
142+
return matchesPattern && !configs.isDirectoryIgnored(entry.path);
143+
},
144+
entryFilter(entry) {
145+
const relativePath = normalizeToPosix(path.relative(cwd, entry.path));
146+
147+
// entries may be directories or files so filter out directories
148+
if (entry.dirent.isDirectory()) {
149+
return false;
150+
}
151+
152+
const matchesPattern = matchers.some(matcher => matcher.match(relativePath));
153+
154+
return matchesPattern && !configs.isFileIgnored(entry.path);
155+
}
156+
})).map(entry => entry.path);
157+
158+
}
159+
160+
/**
161+
* Determines if a given glob pattern will return any results.
162+
* Used primarily to help with useful error messages.
163+
* @param {Object} options The options for the function.
164+
* @param {string} options.cwd The directory to search.
165+
* @param {string} options.pattern A glob pattern to match.
166+
* @returns {Promise<boolean>} True if there is a glob match, false if not.
167+
*/
168+
function globMatch({ cwd, pattern }) {
169+
170+
let found = false;
171+
const patternToUse = path.isAbsolute(pattern)
172+
? normalizeToPosix(path.relative(cwd, pattern))
173+
: pattern;
174+
175+
const matcher = new Minimatch(patternToUse);
176+
177+
const fsWalkSettings = {
178+
179+
deepFilter(entry) {
180+
const relativePath = normalizeToPosix(path.relative(cwd, entry.path));
181+
182+
return !found && matcher.match(relativePath, true);
183+
},
184+
185+
entryFilter(entry) {
186+
if (found || entry.dirent.isDirectory()) {
187+
return false;
188+
}
189+
190+
const relativePath = normalizeToPosix(path.relative(cwd, entry.path));
191+
192+
if (matcher.match(relativePath)) {
193+
found = true;
194+
return true;
195+
}
196+
197+
return false;
198+
}
199+
};
200+
201+
return new Promise(resolve => {
202+
203+
// using a stream so we can exit early because we just need one match
204+
const globStream = fswalk.walkStream(cwd, fsWalkSettings);
205+
206+
globStream.on("data", () => {
207+
globStream.destroy();
208+
resolve(true);
209+
});
210+
211+
// swallow errors as they're not important here
212+
globStream.on("error", () => {});
213+
214+
globStream.on("end", () => {
215+
resolve(false);
216+
});
217+
globStream.read();
218+
});
219+
220+
}
221+
100222
/**
101223
* Finds all files matching the options specified.
102224
* @param {Object} args The arguments objects.
@@ -142,7 +264,7 @@ async function findFiles({
142264
if (stat.isFile()) {
143265
results.push({
144266
filePath,
145-
ignored: configs.isIgnored(filePath)
267+
ignored: configs.isFileIgnored(filePath)
146268
});
147269
}
148270

@@ -226,32 +348,34 @@ async function findFiles({
226348
});
227349

228350
// note: globbyPatterns can be an empty array
229-
const globbyResults = (await globby(globbyPatterns, {
351+
const globbyResults = await globSearch({
230352
cwd,
231-
absolute: true,
232-
ignore: configs.ignores.filter(matcher => typeof matcher === "string")
233-
}));
353+
patterns: globbyPatterns,
354+
configs,
355+
shouldIgnore: true
356+
});
234357

235358
// if there are no results, tell the user why
236359
if (!results.length && !globbyResults.length) {
237360

238361
// try globby without ignoring anything
239-
/* eslint-disable no-unreachable-loop -- We want to exit early. */
240362
for (const globbyPattern of globbyPatterns) {
241363

242-
/* eslint-disable-next-line no-unused-vars -- Want to exit early. */
243-
for await (const filePath of globby.stream(globbyPattern, { cwd, absolute: true })) {
364+
// check if there are any matches at all
365+
const patternHasMatch = await globMatch({
366+
cwd,
367+
pattern: globbyPattern
368+
});
244369

245-
// files were found but ignored
370+
if (patternHasMatch) {
246371
throw new AllFilesIgnoredError(globbyPattern);
247372
}
248373

249-
// no files were found
374+
// otherwise no files were found
250375
if (errorOnUnmatchedPattern) {
251376
throw new NoFilesFoundError(globbyPattern, globInputPaths);
252377
}
253378
}
254-
/* eslint-enable no-unreachable-loop -- Go back to normal. */
255379

256380
}
257381

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,9 @@
5656
"bugs": "https://github.com/eslint/eslint/issues/",
5757
"dependencies": {
5858
"@eslint/eslintrc": "^1.3.3",
59-
"@humanwhocodes/config-array": "^0.10.5",
59+
"@humanwhocodes/config-array": "^0.11.2",
6060
"@humanwhocodes/module-importer": "^1.0.1",
61+
"@nodelib/fs.walk": "^1.2.8",
6162
"ajv": "^6.10.0",
6263
"chalk": "^4.0.0",
6364
"cross-spawn": "^7.0.2",
@@ -75,7 +76,6 @@
7576
"find-up": "^5.0.0",
7677
"glob-parent": "^6.0.1",
7778
"globals": "^13.15.0",
78-
"globby": "^11.1.0",
7979
"grapheme-splitter": "^1.0.4",
8080
"ignore": "^5.2.0",
8181
"import-fresh": "^3.0.0",
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports = {
2+
ignores: ["subdir/subsubdir"]
3+
};

tests/fixtures/ignores-directory/subdir/subsubdir/a.js

Whitespace-only changes.

tests/fixtures/ignores-relative/a.js

Whitespace-only changes.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module.exports = [
2+
{
3+
ignores: ["a.js"]
4+
}
5+
];

tests/fixtures/ignores-relative/subdir/a.js

Whitespace-only changes.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports = {
2+
ignores: ["**/ignores-self/**"]
3+
};

tests/lib/cli.js

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -160,17 +160,6 @@ describe("cli", () => {
160160
});
161161
});
162162

163-
describe("when given a config file and a directory of files", () => {
164-
it(`should load and execute without error with configType:${configType}`, async () => {
165-
const configPath = getFixturePath("configurations", "semi-error.js");
166-
const filePath = getFixturePath("formatters");
167-
const code = `--config ${configPath} ${filePath}`;
168-
const exitStatus = await cli.execute(code, null, useFlatConfig);
169-
170-
assert.strictEqual(exitStatus, 0);
171-
});
172-
});
173-
174163
describe("when there is a local config file", () => {
175164

176165
it(`should load the local config file with configType:${configType}`, async () => {
@@ -460,6 +449,17 @@ describe("cli", () => {
460449
process.cwd = originalCwd;
461450
});
462451

452+
describe("when given a config file and a directory of files", () => {
453+
it(`should load and execute without error with configType:${configType}`, async () => {
454+
const configPath = getFixturePath("configurations", "semi-error.js");
455+
const filePath = getFixturePath("formatters");
456+
const code = `--no-ignore --config ${configPath} ${filePath}`;
457+
const exitStatus = await cli.execute(code, null, useFlatConfig);
458+
459+
assert.strictEqual(exitStatus, 0);
460+
});
461+
});
462+
463463
describe("when executing with global flag", () => {
464464

465465
it(`should default defined variables to read-only with configType:${configType}`, async () => {
@@ -755,22 +755,22 @@ describe("cli", () => {
755755
});
756756

757757
if (useFlatConfig) {
758-
it("should not ignore files if the pattern is a path to a directory (with trailing slash)", async () => {
758+
it("should ignore files if the pattern is a path to a directory (with trailing slash)", async () => {
759759
const filePath = getFixturePath("cli/syntax-error.js");
760760
const exit = await cli.execute(`--ignore-pattern cli/ ${filePath}`, null, true);
761761

762762
// parsing error causes exit code 1
763763
assert.isTrue(log.info.called);
764-
assert.strictEqual(exit, 1);
764+
assert.strictEqual(exit, 0);
765765
});
766766

767-
it("should not ignore files if the pattern is a path to a directory (without trailing slash)", async () => {
767+
it("should ignore files if the pattern is a path to a directory (without trailing slash)", async () => {
768768
const filePath = getFixturePath("cli/syntax-error.js");
769769
const exit = await cli.execute(`--ignore-pattern cli ${filePath}`, null, true);
770770

771771
// parsing error causes exit code 1
772772
assert.isTrue(log.info.called);
773-
assert.strictEqual(exit, 1);
773+
assert.strictEqual(exit, 0);
774774
});
775775
}
776776
});

0 commit comments

Comments
 (0)