Skip to content

Commit 33668ee

Browse files
authored
fix: Ensure that glob patterns are matched correctly. (#16449)
* fix: Ensure that glob patterns are matched correctly. Fixes #16413 Fixes #16299 * Fix cli tests * Upgrade config-array to fix globbing issues
1 parent 651649b commit 33668ee

10 files changed

Lines changed: 147 additions & 103 deletions

File tree

lib/eslint/eslint-helpers.js

Lines changed: 86 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ const hash = require("../cli-engine/hash");
1717
const minimatch = require("minimatch");
1818
const util = require("util");
1919
const fswalk = require("@nodelib/fs.walk");
20+
const globParent = require("glob-parent");
21+
const isPathInside = require("is-path-inside");
2022

2123
//-----------------------------------------------------------------------------
2224
// Fixup references
@@ -111,38 +113,38 @@ function isGlobPattern(pattern) {
111113
* be ignored, so it is consistent with how ignoring works throughout
112114
* ESLint.
113115
* @param {Object} options The options for this function.
114-
* @param {string} options.cwd The directory to search.
116+
* @param {string} options.basePath The directory to search.
115117
* @param {Array<string>} options.patterns An array of glob patterns
116118
* to match.
117119
* @param {FlatConfigArray} options.configs The config array to use for
118120
* determining what to ignore.
119121
* @returns {Promise<Array<string>>} An array of matching file paths
120122
* or an empty array if there are no matches.
121123
*/
122-
async function globSearch({ cwd, patterns, configs }) {
124+
async function globSearch({ basePath, patterns, configs }) {
123125

124126
if (patterns.length === 0) {
125127
return [];
126128
}
127129

128130
const matchers = patterns.map(pattern => {
129131
const patternToUse = path.isAbsolute(pattern)
130-
? normalizeToPosix(path.relative(cwd, pattern))
132+
? normalizeToPosix(path.relative(basePath, pattern))
131133
: pattern;
132134

133135
return new minimatch.Minimatch(patternToUse);
134136
});
135137

136-
return (await doFsWalk(cwd, {
138+
return (await doFsWalk(basePath, {
137139

138140
deepFilter(entry) {
139-
const relativePath = normalizeToPosix(path.relative(cwd, entry.path));
141+
const relativePath = normalizeToPosix(path.relative(basePath, entry.path));
140142
const matchesPattern = matchers.some(matcher => matcher.match(relativePath, true));
141143

142144
return matchesPattern && !configs.isDirectoryIgnored(entry.path);
143145
},
144146
entryFilter(entry) {
145-
const relativePath = normalizeToPosix(path.relative(cwd, entry.path));
147+
const relativePath = normalizeToPosix(path.relative(basePath, entry.path));
146148

147149
// entries may be directories or files so filter out directories
148150
if (entry.dirent.isDirectory()) {
@@ -157,27 +159,48 @@ async function globSearch({ cwd, patterns, configs }) {
157159

158160
}
159161

162+
/**
163+
* Performs multiple glob searches in parallel.
164+
* @param {Object} options The options for this function.
165+
* @param {Array<{patterns:Array<string>,rawPatterns:Array<string>}>} options.searches
166+
* An array of glob patterns to match.
167+
* @param {FlatConfigArray} options.configs The config array to use for
168+
* determining what to ignore.
169+
* @returns {Promise<Array<string>>} An array of matching file paths
170+
* or an empty array if there are no matches.
171+
*/
172+
async function globMultiSearch({ searches, configs }) {
173+
174+
const results = await Promise.all(
175+
[...searches].map(
176+
([basePath, { patterns }]) => globSearch({ basePath, patterns, configs })
177+
)
178+
);
179+
180+
return [...new Set(results.flat())];
181+
}
182+
160183
/**
161184
* Determines if a given glob pattern will return any results.
162185
* Used primarily to help with useful error messages.
163186
* @param {Object} options The options for the function.
164-
* @param {string} options.cwd The directory to search.
187+
* @param {string} options.basePath The directory to search.
165188
* @param {string} options.pattern A glob pattern to match.
166189
* @returns {Promise<boolean>} True if there is a glob match, false if not.
167190
*/
168-
function globMatch({ cwd, pattern }) {
191+
function globMatch({ basePath, pattern }) {
169192

170193
let found = false;
171194
const patternToUse = path.isAbsolute(pattern)
172-
? normalizeToPosix(path.relative(cwd, pattern))
195+
? normalizeToPosix(path.relative(basePath, pattern))
173196
: pattern;
174197

175198
const matcher = new Minimatch(patternToUse);
176199

177200
const fsWalkSettings = {
178201

179202
deepFilter(entry) {
180-
const relativePath = normalizeToPosix(path.relative(cwd, entry.path));
203+
const relativePath = normalizeToPosix(path.relative(basePath, entry.path));
181204

182205
return !found && matcher.match(relativePath, true);
183206
},
@@ -187,7 +210,7 @@ function globMatch({ cwd, pattern }) {
187210
return false;
188211
}
189212

190-
const relativePath = normalizeToPosix(path.relative(cwd, entry.path));
213+
const relativePath = normalizeToPosix(path.relative(basePath, entry.path));
191214

192215
if (matcher.match(relativePath)) {
193216
found = true;
@@ -201,7 +224,7 @@ function globMatch({ cwd, pattern }) {
201224
return new Promise(resolve => {
202225

203226
// using a stream so we can exit early because we just need one match
204-
const globStream = fswalk.walkStream(cwd, fsWalkSettings);
227+
const globStream = fswalk.walkStream(basePath, fsWalkSettings);
205228

206229
globStream.on("data", () => {
207230
globStream.destroy();
@@ -242,8 +265,10 @@ async function findFiles({
242265
}) {
243266

244267
const results = [];
245-
const globbyPatterns = [];
246268
const missingPatterns = [];
269+
let globbyPatterns = [];
270+
let rawPatterns = [];
271+
const searches = new Map([[cwd, { patterns: globbyPatterns, rawPatterns: [] }]]);
247272

248273
// check to see if we have explicit files and directories
249274
const filePaths = patterns.map(filePath => path.resolve(cwd, filePath));
@@ -271,109 +296,76 @@ async function findFiles({
271296
// directories need extensions attached
272297
if (stat.isDirectory()) {
273298

274-
// filePatterns are all relative to cwd
275-
const filePatterns = configs.files
276-
.filter(filePattern => {
277-
278-
// can only do this for strings, not functions
279-
if (typeof filePattern !== "string") {
280-
return false;
281-
}
282-
283-
// patterns starting with ** always apply
284-
if (filePattern.startsWith("**")) {
285-
return true;
286-
}
287-
288-
// patterns ending with * are not used for file search
289-
if (filePattern.endsWith("*")) {
290-
return false;
291-
}
292-
293-
// not sure how to handle negated patterns yet
294-
if (filePattern.startsWith("!")) {
295-
return false;
296-
}
297-
298-
// check if the pattern would be inside the config base path or not
299-
const fullFilePattern = path.join(cwd, filePattern);
300-
const patternRelativeToConfigBasePath = path.relative(configs.basePath, fullFilePattern);
301-
302-
if (patternRelativeToConfigBasePath.startsWith("..")) {
303-
return false;
304-
}
305-
306-
// check if the pattern matches
307-
if (minimatch(filePath, path.dirname(fullFilePattern), { partial: true })) {
308-
return true;
309-
}
310-
311-
// check if the pattern is inside the directory or not
312-
const patternRelativeToFilePath = path.relative(filePath, fullFilePattern);
313-
314-
if (patternRelativeToFilePath.startsWith("..")) {
315-
return false;
316-
}
317-
318-
return true;
319-
})
320-
.map(filePattern => {
321-
if (filePattern.startsWith("**")) {
322-
return path.join(pattern, filePattern);
323-
}
324-
325-
// adjust the path to be relative to the cwd
326-
return path.relative(
327-
cwd,
328-
path.join(configs.basePath, filePattern)
329-
);
330-
})
331-
.map(normalizeToPosix);
332-
333-
if (filePatterns.length) {
334-
globbyPatterns.push(...filePatterns);
299+
// group everything in cwd together and split out others
300+
if (isPathInside(filePath, cwd)) {
301+
({ patterns: globbyPatterns, rawPatterns } = searches.get(cwd));
302+
} else {
303+
if (!searches.has(filePath)) {
304+
searches.set(filePath, { patterns: [], rawPatterns: [] });
305+
}
306+
({ patterns: globbyPatterns, rawPatterns } = searches.get(filePath));
335307
}
336308

309+
globbyPatterns.push(`${normalizeToPosix(filePath)}/**`);
310+
rawPatterns.push(pattern);
337311
}
338312

339313
return;
340314
}
341315

342316
// save patterns for later use based on whether globs are enabled
343317
if (globInputPaths && isGlobPattern(filePath)) {
344-
globbyPatterns.push(pattern);
318+
319+
const basePath = globParent(filePath);
320+
321+
// group in cwd if possible and split out others
322+
if (isPathInside(basePath, cwd)) {
323+
({ patterns: globbyPatterns, rawPatterns } = searches.get(cwd));
324+
} else {
325+
if (!searches.has(basePath)) {
326+
searches.set(basePath, { patterns: [], rawPatterns: [] });
327+
}
328+
({ patterns: globbyPatterns, rawPatterns } = searches.get(basePath));
329+
}
330+
331+
globbyPatterns.push(filePath);
332+
rawPatterns.push(pattern);
345333
} else {
346334
missingPatterns.push(pattern);
347335
}
348336
});
349337

350-
// note: globbyPatterns can be an empty array
351-
const globbyResults = await globSearch({
352-
cwd,
353-
patterns: globbyPatterns,
354-
configs,
355-
shouldIgnore: true
338+
const globbyResults = await globMultiSearch({
339+
searches,
340+
configs
356341
});
357342

358343
// if there are no results, tell the user why
359344
if (!results.length && !globbyResults.length) {
360345

361-
// try globby without ignoring anything
362-
for (const globbyPattern of globbyPatterns) {
346+
for (const [basePath, { patterns: patternsToCheck, rawPatterns: patternsToReport }] of searches) {
363347

364-
// check if there are any matches at all
365-
const patternHasMatch = await globMatch({
366-
cwd,
367-
pattern: globbyPattern
368-
});
348+
let index = 0;
369349

370-
if (patternHasMatch) {
371-
throw new AllFilesIgnoredError(globbyPattern);
372-
}
350+
// try globby without ignoring anything
351+
for (const patternToCheck of patternsToCheck) {
352+
353+
// check if there are any matches at all
354+
const patternHasMatch = await globMatch({
355+
basePath,
356+
pattern: patternToCheck
357+
});
358+
359+
if (patternHasMatch) {
360+
throw new AllFilesIgnoredError(patternsToReport[index]);
361+
}
362+
363+
// otherwise no files were found
364+
if (errorOnUnmatchedPattern) {
365+
throw new NoFilesFoundError(patternsToReport[index], globInputPaths);
366+
}
373367

374-
// otherwise no files were found
375-
if (errorOnUnmatchedPattern) {
376-
throw new NoFilesFoundError(globbyPattern, globInputPaths);
368+
index++;
377369
}
378370
}
379371

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
"bugs": "https://github.com/eslint/eslint/issues/",
5757
"dependencies": {
5858
"@eslint/eslintrc": "^1.3.3",
59-
"@humanwhocodes/config-array": "^0.11.5",
59+
"@humanwhocodes/config-array": "^0.11.6",
6060
"@humanwhocodes/module-importer": "^1.0.1",
6161
"@nodelib/fs.walk": "^1.2.8",
6262
"ajv": "^6.10.0",
@@ -74,13 +74,14 @@
7474
"fast-deep-equal": "^3.1.3",
7575
"file-entry-cache": "^6.0.1",
7676
"find-up": "^5.0.0",
77-
"glob-parent": "^6.0.1",
77+
"glob-parent": "^6.0.2",
7878
"globals": "^13.15.0",
7979
"grapheme-splitter": "^1.0.4",
8080
"ignore": "^5.2.0",
8181
"import-fresh": "^3.0.0",
8282
"imurmurhash": "^0.1.4",
8383
"is-glob": "^4.0.0",
84+
"is-path-inside": "^3.0.3",
8485
"js-sdsl": "^4.1.4",
8586
"js-yaml": "^4.1.0",
8687
"json-stable-stringify-without-jsonify": "^1.0.1",

tests/fixtures/example-app/app.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
console.log("Running");
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = [];
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = {};
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports = [{
2+
files: ["subdir*/**/*.js"]
3+
}];

tests/fixtures/example-app2/subdir1/a.js

Whitespace-only changes.

tests/fixtures/example-app2/subdir2/b.js

Whitespace-only changes.

tests/lib/cli.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -746,7 +746,7 @@ describe("cli", () => {
746746
: `--ignore-path ${getFixturePath(".eslintignore")}`;
747747
const filePath = getFixturePath("cli");
748748
const expectedMessage = useFlatConfig
749-
? `All files matched by '${filePath.replace(/\\/gu, "/")}/**/*.js' are ignored.`
749+
? `All files matched by '${filePath.replace(/\\/gu, "/")}' are ignored.`
750750
: `All files matched by '${filePath}' are ignored.`;
751751

752752
await stdAssert.rejects(async () => {

0 commit comments

Comments
 (0)