eslint 8.25.0 → 8.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,9 +13,30 @@ const path = require("path");
13
13
  const fs = require("fs");
14
14
  const fsp = fs.promises;
15
15
  const isGlob = require("is-glob");
16
- const globby = require("globby");
17
16
  const hash = require("../cli-engine/hash");
18
17
  const minimatch = require("minimatch");
18
+ const util = require("util");
19
+ const fswalk = require("@nodelib/fs.walk");
20
+ const globParent = require("glob-parent");
21
+ const isPathInside = require("is-path-inside");
22
+
23
+ //-----------------------------------------------------------------------------
24
+ // Fixup references
25
+ //-----------------------------------------------------------------------------
26
+
27
+ const doFsWalk = util.promisify(fswalk.walk);
28
+ const Minimatch = minimatch.Minimatch;
29
+
30
+ //-----------------------------------------------------------------------------
31
+ // Types
32
+ //-----------------------------------------------------------------------------
33
+
34
+ /**
35
+ * @typedef {Object} GlobSearch
36
+ * @property {Array<string>} patterns The normalized patterns to use for a search.
37
+ * @property {Array<string>} rawPatterns The patterns as entered by the user
38
+ * before doing any normalization.
39
+ */
19
40
 
20
41
  //-----------------------------------------------------------------------------
21
42
  // Errors
@@ -37,6 +58,30 @@ class NoFilesFoundError extends Error {
37
58
  }
38
59
  }
39
60
 
61
+ /**
62
+ * The error type when a search fails to match multiple patterns.
63
+ */
64
+ class UnmatchedSearchPatternsError extends Error {
65
+
66
+ /**
67
+ * @param {Object} options The options for the error.
68
+ * @param {string} options.basePath The directory that was searched.
69
+ * @param {Array<string>} options.unmatchedPatterns The glob patterns
70
+ * which were not found.
71
+ * @param {Array<string>} options.patterns The glob patterns that were
72
+ * searched.
73
+ * @param {Array<string>} options.rawPatterns The raw glob patterns that
74
+ * were searched.
75
+ */
76
+ constructor({ basePath, unmatchedPatterns, patterns, rawPatterns }) {
77
+ super(`No files matching '${rawPatterns}' in '${basePath}' were found.`);
78
+ this.basePath = basePath;
79
+ this.patternsToCheck = unmatchedPatterns;
80
+ this.patterns = patterns;
81
+ this.rawPatterns = rawPatterns;
82
+ }
83
+ }
84
+
40
85
  /**
41
86
  * The error type when there are files matched by a glob, but all of them have been ignored.
42
87
  */
@@ -97,6 +142,323 @@ function isGlobPattern(pattern) {
97
142
  return isGlob(path.sep === "\\" ? normalizeToPosix(pattern) : pattern);
98
143
  }
99
144
 
145
+
146
+ /**
147
+ * Determines if a given glob pattern will return any results.
148
+ * Used primarily to help with useful error messages.
149
+ * @param {Object} options The options for the function.
150
+ * @param {string} options.basePath The directory to search.
151
+ * @param {string} options.pattern A glob pattern to match.
152
+ * @returns {Promise<boolean>} True if there is a glob match, false if not.
153
+ */
154
+ function globMatch({ basePath, pattern }) {
155
+
156
+ let found = false;
157
+ const patternToUse = path.isAbsolute(pattern)
158
+ ? normalizeToPosix(path.relative(basePath, pattern))
159
+ : pattern;
160
+
161
+ const matcher = new Minimatch(patternToUse);
162
+
163
+ const fsWalkSettings = {
164
+
165
+ deepFilter(entry) {
166
+ const relativePath = normalizeToPosix(path.relative(basePath, entry.path));
167
+
168
+ return !found && matcher.match(relativePath, true);
169
+ },
170
+
171
+ entryFilter(entry) {
172
+ if (found || entry.dirent.isDirectory()) {
173
+ return false;
174
+ }
175
+
176
+ const relativePath = normalizeToPosix(path.relative(basePath, entry.path));
177
+
178
+ if (matcher.match(relativePath)) {
179
+ found = true;
180
+ return true;
181
+ }
182
+
183
+ return false;
184
+ }
185
+ };
186
+
187
+ return new Promise(resolve => {
188
+
189
+ // using a stream so we can exit early because we just need one match
190
+ const globStream = fswalk.walkStream(basePath, fsWalkSettings);
191
+
192
+ globStream.on("data", () => {
193
+ globStream.destroy();
194
+ resolve(true);
195
+ });
196
+
197
+ // swallow errors as they're not important here
198
+ globStream.on("error", () => { });
199
+
200
+ globStream.on("end", () => {
201
+ resolve(false);
202
+ });
203
+ globStream.read();
204
+ });
205
+
206
+ }
207
+
208
+ /**
209
+ * Searches a directory looking for matching glob patterns. This uses
210
+ * the config array's logic to determine if a directory or file should
211
+ * be ignored, so it is consistent with how ignoring works throughout
212
+ * ESLint.
213
+ * @param {Object} options The options for this function.
214
+ * @param {string} options.basePath The directory to search.
215
+ * @param {Array<string>} options.patterns An array of glob patterns
216
+ * to match.
217
+ * @param {Array<string>} options.rawPatterns An array of glob patterns
218
+ * as the user inputted them. Used for errors.
219
+ * @param {FlatConfigArray} options.configs The config array to use for
220
+ * determining what to ignore.
221
+ * @param {boolean} options.errorOnUnmatchedPattern Determines if an error
222
+ * should be thrown when a pattern is unmatched.
223
+ * @returns {Promise<Array<string>>} An array of matching file paths
224
+ * or an empty array if there are no matches.
225
+ * @throws {UnmatchedSearchPatternsErrror} If there is a pattern that doesn't
226
+ * match any files.
227
+ */
228
+ async function globSearch({
229
+ basePath,
230
+ patterns,
231
+ rawPatterns,
232
+ configs,
233
+ errorOnUnmatchedPattern
234
+ }) {
235
+
236
+ if (patterns.length === 0) {
237
+ return [];
238
+ }
239
+
240
+ /*
241
+ * In this section we are converting the patterns into Minimatch
242
+ * instances for performance reasons. Because we are doing the same
243
+ * matches repeatedly, it's best to compile those patterns once and
244
+ * reuse them multiple times.
245
+ *
246
+ * To do that, we convert any patterns with an absolute path into a
247
+ * relative path and normalize it to Posix-style slashes. We also keep
248
+ * track of the relative patterns to map them back to the original
249
+ * patterns, which we need in order to throw an error if there are any
250
+ * unmatched patterns.
251
+ */
252
+ const relativeToPatterns = new Map();
253
+ const matchers = patterns.map((pattern, i) => {
254
+ const patternToUse = path.isAbsolute(pattern)
255
+ ? normalizeToPosix(path.relative(basePath, pattern))
256
+ : pattern;
257
+
258
+ relativeToPatterns.set(patternToUse, patterns[i]);
259
+
260
+ return new minimatch.Minimatch(patternToUse);
261
+ });
262
+
263
+ /*
264
+ * We track unmatched patterns because we may want to throw an error when
265
+ * they occur. To start, this set is initialized with all of the patterns.
266
+ * Every time a match occurs, the pattern is removed from the set, making
267
+ * it easy to tell if we have any unmatched patterns left at the end of
268
+ * search.
269
+ */
270
+ const unmatchedPatterns = new Set([...relativeToPatterns.keys()]);
271
+
272
+ const filePaths = (await doFsWalk(basePath, {
273
+
274
+ deepFilter(entry) {
275
+ const relativePath = normalizeToPosix(path.relative(basePath, entry.path));
276
+ const matchesPattern = matchers.some(matcher => matcher.match(relativePath, true));
277
+
278
+ return matchesPattern && !configs.isDirectoryIgnored(entry.path);
279
+ },
280
+ entryFilter(entry) {
281
+ const relativePath = normalizeToPosix(path.relative(basePath, entry.path));
282
+
283
+ // entries may be directories or files so filter out directories
284
+ if (entry.dirent.isDirectory()) {
285
+ return false;
286
+ }
287
+
288
+ /*
289
+ * Optimization: We need to track when patterns are left unmatched
290
+ * and so we use `unmatchedPatterns` to do that. There is a bit of
291
+ * complexity here because the same file can be matched by more than
292
+ * one pattern. So, when we start, we actually need to test every
293
+ * pattern against every file. Once we know there are no remaining
294
+ * unmatched patterns, then we can switch to just looking for the
295
+ * first matching pattern for improved speed.
296
+ */
297
+ const matchesPattern = unmatchedPatterns.size > 0
298
+ ? matchers.reduce((previousValue, matcher) => {
299
+ const pathMatches = matcher.match(relativePath);
300
+
301
+ /*
302
+ * We updated the unmatched patterns set only if the path
303
+ * matches and the file isn't ignored. If the file is
304
+ * ignored, that means there wasn't a match for the
305
+ * pattern so it should not be removed.
306
+ *
307
+ * Performance note: isFileIgnored() aggressively caches
308
+ * results so there is no performance penalty for calling
309
+ * it twice with the same argument.
310
+ */
311
+ if (pathMatches && !configs.isFileIgnored(entry.path)) {
312
+ unmatchedPatterns.delete(matcher.pattern);
313
+ }
314
+
315
+ return pathMatches || previousValue;
316
+ }, false)
317
+ : matchers.some(matcher => matcher.match(relativePath));
318
+
319
+ return matchesPattern && !configs.isFileIgnored(entry.path);
320
+ }
321
+
322
+ })).map(entry => entry.path);
323
+
324
+ // now check to see if we have any unmatched patterns
325
+ if (errorOnUnmatchedPattern && unmatchedPatterns.size > 0) {
326
+ throw new UnmatchedSearchPatternsError({
327
+ basePath,
328
+ unmatchedPatterns: [...unmatchedPatterns].map(
329
+ pattern => relativeToPatterns.get(pattern)
330
+ ),
331
+ patterns,
332
+ rawPatterns
333
+ });
334
+ }
335
+
336
+ return filePaths;
337
+ }
338
+
339
+ /**
340
+ * Checks to see if there are any ignored results for a given search. This
341
+ * happens either when there are unmatched patterns during a search or if
342
+ * a search returns no results.
343
+ * @param {Object} options The options for this function.
344
+ * @param {string} options.basePath The directory to search.
345
+ * @param {Array<string>} options.patterns An array of glob patterns
346
+ * that were used in the original search.
347
+ * @param {Array<string>} options.rawPatterns An array of glob patterns
348
+ * as the user inputted them. Used for errors.
349
+ * @param {Array<string>} options.patternsToCheck An array of glob patterns
350
+ * to use for this check.
351
+ * @returns {void}
352
+ * @throws {NoFilesFoundError} If there is a pattern that doesn't match
353
+ * any files and `errorOnUnmatchedPattern` is true.
354
+ * @throws {AllFilesIgnoredError} If there is a pattern that matches files
355
+ * when there are no ignores.
356
+ */
357
+ async function checkForIgnoredResults({
358
+ basePath,
359
+ patterns,
360
+ rawPatterns,
361
+ patternsToCheck = patterns
362
+ }) {
363
+
364
+ for (const pattern of patternsToCheck) {
365
+
366
+ const patternHasMatch = await globMatch({
367
+ basePath,
368
+ pattern
369
+ });
370
+
371
+ if (patternHasMatch) {
372
+ throw new AllFilesIgnoredError(
373
+ rawPatterns[patterns.indexOf(pattern)]
374
+ );
375
+ }
376
+ }
377
+
378
+ // if we get here there are truly no matches
379
+ throw new NoFilesFoundError(
380
+ rawPatterns[patterns.indexOf(patternsToCheck[0])],
381
+ true
382
+ );
383
+ }
384
+
385
+ /**
386
+ * Performs multiple glob searches in parallel.
387
+ * @param {Object} options The options for this function.
388
+ * @param {Map<string,GlobSearch>} options.searches
389
+ * An array of glob patterns to match.
390
+ * @param {FlatConfigArray} options.configs The config array to use for
391
+ * determining what to ignore.
392
+ * @param {boolean} options.errorOnUnmatchedPattern Determines if an
393
+ * unmatched glob pattern should throw an error.
394
+ * @returns {Promise<Array<string>>} An array of matching file paths
395
+ * or an empty array if there are no matches.
396
+ */
397
+ async function globMultiSearch({ searches, configs, errorOnUnmatchedPattern }) {
398
+
399
+ /*
400
+ * For convenience, we normalized the search map into an array of objects.
401
+ * Next, we filter out all searches that have no patterns. This happens
402
+ * primarily for the cwd, which is prepopulated in the searches map as an
403
+ * optimization. However, if it has no patterns, it means all patterns
404
+ * occur outside of the cwd and we can safely filter out that search.
405
+ */
406
+ const normalizedSearches = [...searches].map(
407
+ ([basePath, { patterns, rawPatterns }]) => ({ basePath, patterns, rawPatterns })
408
+ ).filter(({ patterns }) => patterns.length > 0);
409
+
410
+ const results = await Promise.allSettled(
411
+ normalizedSearches.map(
412
+ ({ basePath, patterns, rawPatterns }) => globSearch({
413
+ basePath,
414
+ patterns,
415
+ rawPatterns,
416
+ configs,
417
+ errorOnUnmatchedPattern
418
+ })
419
+ )
420
+ );
421
+
422
+ const filePaths = [];
423
+
424
+ for (let i = 0; i < results.length; i++) {
425
+
426
+ const result = results[i];
427
+ const currentSearch = normalizedSearches[i];
428
+
429
+ if (result.status === "fulfilled") {
430
+
431
+ // if the search was successful just add the results
432
+ if (result.value.length > 0) {
433
+ filePaths.push(...result.value);
434
+ }
435
+
436
+ continue;
437
+ }
438
+
439
+ // if we make it here then there was an error
440
+ const error = result.reason;
441
+
442
+ // unexpected errors should be re-thrown
443
+ if (!error.basePath) {
444
+ throw error;
445
+ }
446
+
447
+ if (errorOnUnmatchedPattern) {
448
+
449
+ await checkForIgnoredResults({
450
+ ...currentSearch,
451
+ patternsToCheck: error.patternsToCheck
452
+ });
453
+
454
+ }
455
+
456
+ }
457
+
458
+ return [...new Set(filePaths)];
459
+
460
+ }
461
+
100
462
  /**
101
463
  * Finds all files matching the options specified.
102
464
  * @param {Object} args The arguments objects.
@@ -120,8 +482,10 @@ async function findFiles({
120
482
  }) {
121
483
 
122
484
  const results = [];
123
- const globbyPatterns = [];
124
485
  const missingPatterns = [];
486
+ let globbyPatterns = [];
487
+ let rawPatterns = [];
488
+ const searches = new Map([[cwd, { patterns: globbyPatterns, rawPatterns: [] }]]);
125
489
 
126
490
  // check to see if we have explicit files and directories
127
491
  const filePaths = patterns.map(filePath => path.resolve(cwd, filePath));
@@ -142,76 +506,25 @@ async function findFiles({
142
506
  if (stat.isFile()) {
143
507
  results.push({
144
508
  filePath,
145
- ignored: configs.isIgnored(filePath)
509
+ ignored: configs.isFileIgnored(filePath)
146
510
  });
147
511
  }
148
512
 
149
513
  // directories need extensions attached
150
514
  if (stat.isDirectory()) {
151
515
 
152
- // filePatterns are all relative to cwd
153
- const filePatterns = configs.files
154
- .filter(filePattern => {
155
-
156
- // can only do this for strings, not functions
157
- if (typeof filePattern !== "string") {
158
- return false;
159
- }
160
-
161
- // patterns starting with ** always apply
162
- if (filePattern.startsWith("**")) {
163
- return true;
164
- }
165
-
166
- // patterns ending with * are not used for file search
167
- if (filePattern.endsWith("*")) {
168
- return false;
169
- }
170
-
171
- // not sure how to handle negated patterns yet
172
- if (filePattern.startsWith("!")) {
173
- return false;
174
- }
175
-
176
- // check if the pattern would be inside the config base path or not
177
- const fullFilePattern = path.join(cwd, filePattern);
178
- const patternRelativeToConfigBasePath = path.relative(configs.basePath, fullFilePattern);
179
-
180
- if (patternRelativeToConfigBasePath.startsWith("..")) {
181
- return false;
182
- }
183
-
184
- // check if the pattern matches
185
- if (minimatch(filePath, path.dirname(fullFilePattern), { partial: true })) {
186
- return true;
187
- }
188
-
189
- // check if the pattern is inside the directory or not
190
- const patternRelativeToFilePath = path.relative(filePath, fullFilePattern);
191
-
192
- if (patternRelativeToFilePath.startsWith("..")) {
193
- return false;
194
- }
195
-
196
- return true;
197
- })
198
- .map(filePattern => {
199
- if (filePattern.startsWith("**")) {
200
- return path.join(pattern, filePattern);
201
- }
202
-
203
- // adjust the path to be relative to the cwd
204
- return path.relative(
205
- cwd,
206
- path.join(configs.basePath, filePattern)
207
- );
208
- })
209
- .map(normalizeToPosix);
210
-
211
- if (filePatterns.length) {
212
- globbyPatterns.push(...filePatterns);
516
+ // group everything in cwd together and split out others
517
+ if (isPathInside(filePath, cwd)) {
518
+ ({ patterns: globbyPatterns, rawPatterns } = searches.get(cwd));
519
+ } else {
520
+ if (!searches.has(filePath)) {
521
+ searches.set(filePath, { patterns: [], rawPatterns: [] });
522
+ }
523
+ ({ patterns: globbyPatterns, rawPatterns } = searches.get(filePath));
213
524
  }
214
525
 
526
+ globbyPatterns.push(`${normalizeToPosix(filePath)}/**`);
527
+ rawPatterns.push(pattern);
215
528
  }
216
529
 
217
530
  return;
@@ -219,47 +532,37 @@ async function findFiles({
219
532
 
220
533
  // save patterns for later use based on whether globs are enabled
221
534
  if (globInputPaths && isGlobPattern(filePath)) {
222
- globbyPatterns.push(pattern);
223
- } else {
224
- missingPatterns.push(pattern);
225
- }
226
- });
227
-
228
- // note: globbyPatterns can be an empty array
229
- const globbyResults = (await globby(globbyPatterns, {
230
- cwd,
231
- absolute: true,
232
- ignore: configs.ignores.filter(matcher => typeof matcher === "string")
233
- }));
234
-
235
- // if there are no results, tell the user why
236
- if (!results.length && !globbyResults.length) {
237
-
238
- // try globby without ignoring anything
239
- /* eslint-disable no-unreachable-loop -- We want to exit early. */
240
- for (const globbyPattern of globbyPatterns) {
241
535
 
242
- /* eslint-disable-next-line no-unused-vars -- Want to exit early. */
243
- for await (const filePath of globby.stream(globbyPattern, { cwd, absolute: true })) {
536
+ const basePath = globParent(filePath);
244
537
 
245
- // files were found but ignored
246
- throw new AllFilesIgnoredError(globbyPattern);
538
+ // group in cwd if possible and split out others
539
+ if (isPathInside(basePath, cwd)) {
540
+ ({ patterns: globbyPatterns, rawPatterns } = searches.get(cwd));
541
+ } else {
542
+ if (!searches.has(basePath)) {
543
+ searches.set(basePath, { patterns: [], rawPatterns: [] });
544
+ }
545
+ ({ patterns: globbyPatterns, rawPatterns } = searches.get(basePath));
247
546
  }
248
547
 
249
- // no files were found
250
- if (errorOnUnmatchedPattern) {
251
- throw new NoFilesFoundError(globbyPattern, globInputPaths);
252
- }
548
+ globbyPatterns.push(filePath);
549
+ rawPatterns.push(pattern);
550
+ } else {
551
+ missingPatterns.push(pattern);
253
552
  }
254
- /* eslint-enable no-unreachable-loop -- Go back to normal. */
255
-
256
- }
553
+ });
257
554
 
258
555
  // there were patterns that didn't match anything, tell the user
259
556
  if (errorOnUnmatchedPattern && missingPatterns.length) {
260
557
  throw new NoFilesFoundError(missingPatterns[0], globInputPaths);
261
558
  }
262
559
 
560
+ // now we are safe to do the search
561
+ const globbyResults = await globMultiSearch({
562
+ searches,
563
+ configs,
564
+ errorOnUnmatchedPattern
565
+ });
263
566
 
264
567
  return [
265
568
  ...results,