eslint 9.0.0 → 9.1.1

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.
package/README.md CHANGED
@@ -48,7 +48,7 @@ Prerequisites: [Node.js](https://nodejs.org/) (`^18.18.0`, `^20.9.0`, or `>=21.1
48
48
  You can install and configure ESLint using this command:
49
49
 
50
50
  ```shell
51
- npm init @eslint/config
51
+ npm init @eslint/config@latest
52
52
  ```
53
53
 
54
54
  After that, you can run ESLint on any file or directory like this:
@@ -59,7 +59,7 @@ After that, you can run ESLint on any file or directory like this:
59
59
 
60
60
  ## Configuration
61
61
 
62
- After running `npm init @eslint/config`, you'll have an `eslint.config.js` or `eslint.config.mjs` file in your directory. In it, you'll see some rules configured like this:
62
+ After running `npm init @eslint/config`, you'll have an `eslint.config.js` (or `eslint.config.mjs`) file in your directory. In it, you'll see some rules configured like this:
63
63
 
64
64
  ```js
65
65
  import pluginJs from "@eslint/js";
@@ -300,7 +300,7 @@ The following companies, organizations, and individuals support ESLint's ongoing
300
300
  <!--sponsorsstart-->
301
301
  <h3>Platinum Sponsors</h3>
302
302
  <p><a href="https://automattic.com"><img src="https://images.opencollective.com/automattic/d0ef3e1/logo.png" alt="Automattic" height="undefined"></a></p><h3>Gold Sponsors</h3>
303
- <p><a href="https://bitwarden.com"><img src="https://avatars.githubusercontent.com/u/15990069?v=4" alt="Bitwarden" height="96"></a> <a href="https://engineering.salesforce.com"><img src="https://images.opencollective.com/salesforce/ca8f997/logo.png" alt="Salesforce" height="96"></a> <a href="https://www.airbnb.com/"><img src="https://images.opencollective.com/airbnb/d327d66/logo.png" alt="Airbnb" height="96"></a></p><h3>Silver Sponsors</h3>
303
+ <p><a href="https://engineering.salesforce.com"><img src="https://images.opencollective.com/salesforce/ca8f997/logo.png" alt="Salesforce" height="96"></a> <a href="https://www.airbnb.com/"><img src="https://images.opencollective.com/airbnb/d327d66/logo.png" alt="Airbnb" height="96"></a></p><h3>Silver Sponsors</h3>
304
304
  <p><a href="https://www.jetbrains.com/"><img src="https://images.opencollective.com/jetbrains/eb04ddc/logo.png" alt="JetBrains" height="64"></a> <a href="https://liftoff.io/"><img src="https://images.opencollective.com/liftoff/5c4fa84/logo.png" alt="Liftoff" height="64"></a> <a href="https://americanexpress.io"><img src="https://avatars.githubusercontent.com/u/3853301?v=4" alt="American Express" height="64"></a> <a href="https://www.workleap.com"><img src="https://avatars.githubusercontent.com/u/53535748?u=d1e55d7661d724bf2281c1bfd33cb8f99fe2465f&v=4" alt="Workleap" height="64"></a></p><h3>Bronze Sponsors</h3>
305
305
  <p><a href="https://www.notion.so"><img src="https://images.opencollective.com/notion/bf3b117/logo.png" alt="notion" height="32"></a> <a href="https://www.crosswordsolver.org/anagram-solver/"><img src="https://images.opencollective.com/anagram-solver/2666271/logo.png" alt="Anagram Solver" height="32"></a> <a href="https://icons8.com/"><img src="https://images.opencollective.com/icons8/7fa1641/logo.png" alt="Icons8" height="32"></a> <a href="https://discord.com"><img src="https://images.opencollective.com/discordapp/f9645d9/logo.png" alt="Discord" height="32"></a> <a href="https://transloadit.com/"><img src="https://avatars.githubusercontent.com/u/125754?v=4" alt="Transloadit" height="32"></a> <a href="https://www.ignitionapp.com"><img src="https://avatars.githubusercontent.com/u/5753491?v=4" alt="Ignition" height="32"></a> <a href="https://nx.dev"><img src="https://avatars.githubusercontent.com/u/23692104?v=4" alt="Nx" height="32"></a> <a href="https://herocoders.com"><img src="https://avatars.githubusercontent.com/u/37549774?v=4" alt="HeroCoders" height="32"></a> <a href="https://usenextbase.com"><img src="https://avatars.githubusercontent.com/u/145838380?v=4" alt="Nextbase Starter Kit" height="32"></a></p>
306
306
  <!--sponsorsend-->
package/bin/eslint.js CHANGED
@@ -140,23 +140,11 @@ ${getErrorMessage(error)}`;
140
140
  if (process.argv.includes("--init")) {
141
141
 
142
142
  // `eslint --init` has been moved to `@eslint/create-config`
143
- console.warn("You can also run this command directly using 'npm init @eslint/config'.");
143
+ console.warn("You can also run this command directly using 'npm init @eslint/config@latest'.");
144
144
 
145
145
  const spawn = require("cross-spawn");
146
146
 
147
- spawn.sync("npm", ["init", "@eslint/config"], { encoding: "utf8", stdio: "inherit" });
148
- return;
149
- }
150
-
151
- // Call the config inspector if `--inspect-config` is present.
152
- if (process.argv.includes("--inspect-config")) {
153
-
154
- console.warn("You can also run this command directly using 'npx @eslint/config-inspector' in the same directory as your configuration file.");
155
-
156
- const spawn = require("cross-spawn");
157
-
158
- spawn.sync("npx", ["@eslint/config-inspector"], { encoding: "utf8", stdio: "inherit" });
159
-
147
+ spawn.sync("npm", ["init", "@eslint/config@latest"], { encoding: "utf8", stdio: "inherit" });
160
148
  return;
161
149
  }
162
150
 
package/conf/globals.js CHANGED
@@ -70,6 +70,7 @@ const es2015 = {
70
70
  Int16Array: false,
71
71
  Int32Array: false,
72
72
  Int8Array: false,
73
+ Intl: false,
73
74
  Map: false,
74
75
  Promise: false,
75
76
  Proxy: false,
package/lib/cli.js CHANGED
@@ -19,7 +19,7 @@ const fs = require("fs"),
19
19
  path = require("path"),
20
20
  { promisify } = require("util"),
21
21
  { LegacyESLint } = require("./eslint"),
22
- { ESLint, shouldUseFlatConfig } = require("./eslint/eslint"),
22
+ { ESLint, shouldUseFlatConfig, locateConfigFileToUse } = require("./eslint/eslint"),
23
23
  createCLIOptions = require("./options"),
24
24
  log = require("./shared/logging"),
25
25
  RuntimeInfo = require("./shared/runtime-info"),
@@ -336,6 +336,27 @@ async function printResults(engine, results, format, outputFile, resultsMeta) {
336
336
  */
337
337
  const cli = {
338
338
 
339
+ /**
340
+ * Calculates the command string for the --inspect-config operation.
341
+ * @param {string} configFile The path to the config file to inspect.
342
+ * @returns {Promise<string>} The command string to execute.
343
+ */
344
+ async calculateInspectConfigFlags(configFile) {
345
+
346
+ // find the config file
347
+ const {
348
+ configFilePath,
349
+ basePath,
350
+ error
351
+ } = await locateConfigFileToUse({ cwd: process.cwd(), configFile });
352
+
353
+ if (error) {
354
+ throw error;
355
+ }
356
+
357
+ return ["--config", configFilePath, "--basePath", basePath];
358
+ },
359
+
339
360
  /**
340
361
  * Executes the CLI based on an array of arguments that is passed in.
341
362
  * @param {string|Array|Object} args The arguments to process.
@@ -425,6 +446,24 @@ const cli = {
425
446
  return 0;
426
447
  }
427
448
 
449
+ if (options.inspectConfig) {
450
+
451
+ log.info("You can also run this command directly using 'npx @eslint/config-inspector' in the same directory as your configuration file.");
452
+
453
+ try {
454
+ const flatOptions = await translateOptions(options, "flat");
455
+ const spawn = require("cross-spawn");
456
+ const flags = await cli.calculateInspectConfigFlags(flatOptions.overrideConfigFile);
457
+
458
+ spawn.sync("npx", ["@eslint/config-inspector", ...flags], { encoding: "utf8", stdio: "inherit" });
459
+ } catch (error) {
460
+ log.error(error);
461
+ return 2;
462
+ }
463
+
464
+ return 0;
465
+ }
466
+
428
467
  debug(`Running on ${useStdin ? "text" : "files"}`);
429
468
 
430
469
  if (options.fix && options.fixDryRun) {
@@ -18,6 +18,11 @@ const { defaultConfig } = require("./default-config");
18
18
  // Helpers
19
19
  //-----------------------------------------------------------------------------
20
20
 
21
+ /**
22
+ * Fields that are considered metadata and not part of the config object.
23
+ */
24
+ const META_FIELDS = new Set(["name"]);
25
+
21
26
  const ruleValidator = new RuleValidator();
22
27
 
23
28
  /**
@@ -74,7 +79,53 @@ function getObjectId(object) {
74
79
  return name;
75
80
  }
76
81
 
82
+ /**
83
+ * Wraps a config error with details about where the error occurred.
84
+ * @param {Error} error The original error.
85
+ * @param {number} originalLength The original length of the config array.
86
+ * @param {number} baseLength The length of the base config.
87
+ * @returns {TypeError} The new error with details.
88
+ */
89
+ function wrapConfigErrorWithDetails(error, originalLength, baseLength) {
90
+
91
+ let location = "user-defined";
92
+ let configIndex = error.index;
93
+
94
+ /*
95
+ * A config array is set up in this order:
96
+ * 1. Base config
97
+ * 2. Original configs
98
+ * 3. User-defined configs
99
+ * 4. CLI-defined configs
100
+ *
101
+ * So we need to adjust the index to account for the base config.
102
+ *
103
+ * - If the index is less than the base length, it's in the base config
104
+ * (as specified by `baseConfig` argument to `FlatConfigArray` constructor).
105
+ * - If the index is greater than the base length but less than the original
106
+ * length + base length, it's in the original config. The original config
107
+ * is passed to the `FlatConfigArray` constructor as the first argument.
108
+ * - Otherwise, it's in the user-defined config, which is loaded from the
109
+ * config file and merged with any command-line options.
110
+ */
111
+ if (error.index < baseLength) {
112
+ location = "base";
113
+ } else if (error.index < originalLength + baseLength) {
114
+ location = "original";
115
+ configIndex = error.index - baseLength;
116
+ } else {
117
+ configIndex = error.index - originalLength - baseLength;
118
+ }
119
+
120
+ return new TypeError(
121
+ `${error.message.slice(0, -1)} at ${location} index ${configIndex}.`,
122
+ { cause: error }
123
+ );
124
+ }
125
+
77
126
  const originalBaseConfig = Symbol("originalBaseConfig");
127
+ const originalLength = Symbol("originalLength");
128
+ const baseLength = Symbol("baseLength");
78
129
 
79
130
  //-----------------------------------------------------------------------------
80
131
  // Exports
@@ -101,12 +152,24 @@ class FlatConfigArray extends ConfigArray {
101
152
  schema: flatConfigSchema
102
153
  });
103
154
 
155
+ /**
156
+ * The original length of the array before any modifications.
157
+ * @type {number}
158
+ */
159
+ this[originalLength] = this.length;
160
+
104
161
  if (baseConfig[Symbol.iterator]) {
105
162
  this.unshift(...baseConfig);
106
163
  } else {
107
164
  this.unshift(baseConfig);
108
165
  }
109
166
 
167
+ /**
168
+ * The length of the array after applying the base config.
169
+ * @type {number}
170
+ */
171
+ this[baseLength] = this.length - this[originalLength];
172
+
110
173
  /**
111
174
  * The base config used to build the config array.
112
175
  * @type {Array<FlatConfig>}
@@ -124,6 +187,49 @@ class FlatConfigArray extends ConfigArray {
124
187
  Object.defineProperty(this, "shouldIgnore", { writable: false });
125
188
  }
126
189
 
190
+ /**
191
+ * Normalizes the array by calling the superclass method and catching/rethrowing
192
+ * any ConfigError exceptions with additional details.
193
+ * @param {any} [context] The context to use to normalize the array.
194
+ * @returns {Promise<FlatConfigArray>} A promise that resolves when the array is normalized.
195
+ */
196
+ normalize(context) {
197
+ return super.normalize(context)
198
+ .catch(error => {
199
+ if (error.name === "ConfigError") {
200
+ throw wrapConfigErrorWithDetails(error, this[originalLength], this[baseLength]);
201
+ }
202
+
203
+ throw error;
204
+
205
+ });
206
+ }
207
+
208
+ /**
209
+ * Normalizes the array by calling the superclass method and catching/rethrowing
210
+ * any ConfigError exceptions with additional details.
211
+ * @param {any} [context] The context to use to normalize the array.
212
+ * @returns {FlatConfigArray} The current instance.
213
+ * @throws {TypeError} If the config is invalid.
214
+ */
215
+ normalizeSync(context) {
216
+
217
+ try {
218
+
219
+ return super.normalizeSync(context);
220
+
221
+ } catch (error) {
222
+
223
+ if (error.name === "ConfigError") {
224
+ throw wrapConfigErrorWithDetails(error, this[originalLength], this[baseLength]);
225
+ }
226
+
227
+ throw error;
228
+
229
+ }
230
+
231
+ }
232
+
127
233
  /* eslint-disable class-methods-use-this -- Desired as instance method */
128
234
  /**
129
235
  * Replaces a config with another config to allow us to put strings
@@ -135,15 +241,15 @@ class FlatConfigArray extends ConfigArray {
135
241
  [ConfigArraySymbol.preprocessConfig](config) {
136
242
 
137
243
  /*
138
- * If `shouldIgnore` is false, we remove any ignore patterns specified
139
- * in the config so long as it's not a default config and it doesn't
140
- * have a `files` entry.
244
+ * If a config object has `ignores` and no other non-meta fields, then it's an object
245
+ * for global ignores. If `shouldIgnore` is false, that object shouldn't apply,
246
+ * so we'll remove its `ignores`.
141
247
  */
142
248
  if (
143
249
  !this.shouldIgnore &&
144
250
  !this[originalBaseConfig].includes(config) &&
145
251
  config.ignores &&
146
- !config.files
252
+ Object.keys(config).filter(key => !META_FIELDS.has(key)).length === 1
147
253
  ) {
148
254
  /* eslint-disable-next-line no-unused-vars -- need to strip off other keys */
149
255
  const { ignores, ...otherKeys } = config;
@@ -15,7 +15,6 @@ const fsp = fs.promises;
15
15
  const isGlob = require("is-glob");
16
16
  const hash = require("../cli-engine/hash");
17
17
  const minimatch = require("minimatch");
18
- const util = require("util");
19
18
  const fswalk = require("@nodelib/fs.walk");
20
19
  const globParent = require("glob-parent");
21
20
  const isPathInside = require("is-path-inside");
@@ -24,7 +23,6 @@ const isPathInside = require("is-path-inside");
24
23
  // Fixup references
25
24
  //-----------------------------------------------------------------------------
26
25
 
27
- const doFsWalk = util.promisify(fswalk.walk);
28
26
  const Minimatch = minimatch.Minimatch;
29
27
  const MINIMATCH_OPTIONS = { dot: true };
30
28
 
@@ -280,56 +278,92 @@ async function globSearch({
280
278
  */
281
279
  const unmatchedPatterns = new Set([...relativeToPatterns.keys()]);
282
280
 
283
- const filePaths = (await doFsWalk(basePath, {
281
+ const filePaths = (await new Promise((resolve, reject) => {
284
282
 
285
- deepFilter(entry) {
286
- const relativePath = normalizeToPosix(path.relative(basePath, entry.path));
287
- const matchesPattern = matchers.some(matcher => matcher.match(relativePath, true));
288
-
289
- return matchesPattern && !configs.isDirectoryIgnored(entry.path);
290
- },
291
- entryFilter(entry) {
292
- const relativePath = normalizeToPosix(path.relative(basePath, entry.path));
283
+ let promiseRejected = false;
293
284
 
294
- // entries may be directories or files so filter out directories
295
- if (entry.dirent.isDirectory()) {
285
+ /**
286
+ * Wraps a boolean-returning filter function. The wrapped function will reject the promise if an error occurs.
287
+ * @param {Function} filter A filter function to wrap.
288
+ * @returns {Function} A function similar to the wrapped filter that rejects the promise if an error occurs.
289
+ */
290
+ function wrapFilter(filter) {
291
+ return (...args) => {
292
+
293
+ // No need to run the filter if an error has been thrown.
294
+ if (!promiseRejected) {
295
+ try {
296
+ return filter(...args);
297
+ } catch (error) {
298
+ promiseRejected = true;
299
+ reject(error);
300
+ }
301
+ }
296
302
  return false;
297
- }
303
+ };
304
+ }
298
305
 
299
- /*
300
- * Optimization: We need to track when patterns are left unmatched
301
- * and so we use `unmatchedPatterns` to do that. There is a bit of
302
- * complexity here because the same file can be matched by more than
303
- * one pattern. So, when we start, we actually need to test every
304
- * pattern against every file. Once we know there are no remaining
305
- * unmatched patterns, then we can switch to just looking for the
306
- * first matching pattern for improved speed.
307
- */
308
- const matchesPattern = unmatchedPatterns.size > 0
309
- ? matchers.reduce((previousValue, matcher) => {
310
- const pathMatches = matcher.match(relativePath);
306
+ fswalk.walk(
307
+ basePath,
308
+ {
309
+ deepFilter: wrapFilter(entry => {
310
+ const relativePath = normalizeToPosix(path.relative(basePath, entry.path));
311
+ const matchesPattern = matchers.some(matcher => matcher.match(relativePath, true));
312
+
313
+ return matchesPattern && !configs.isDirectoryIgnored(entry.path);
314
+ }),
315
+ entryFilter: wrapFilter(entry => {
316
+ const relativePath = normalizeToPosix(path.relative(basePath, entry.path));
317
+
318
+ // entries may be directories or files so filter out directories
319
+ if (entry.dirent.isDirectory()) {
320
+ return false;
321
+ }
311
322
 
312
323
  /*
313
- * We updated the unmatched patterns set only if the path
314
- * matches and the file isn't ignored. If the file is
315
- * ignored, that means there wasn't a match for the
316
- * pattern so it should not be removed.
317
- *
318
- * Performance note: isFileIgnored() aggressively caches
319
- * results so there is no performance penalty for calling
320
- * it twice with the same argument.
324
+ * Optimization: We need to track when patterns are left unmatched
325
+ * and so we use `unmatchedPatterns` to do that. There is a bit of
326
+ * complexity here because the same file can be matched by more than
327
+ * one pattern. So, when we start, we actually need to test every
328
+ * pattern against every file. Once we know there are no remaining
329
+ * unmatched patterns, then we can switch to just looking for the
330
+ * first matching pattern for improved speed.
321
331
  */
322
- if (pathMatches && !configs.isFileIgnored(entry.path)) {
323
- unmatchedPatterns.delete(matcher.pattern);
324
- }
325
-
326
- return pathMatches || previousValue;
327
- }, false)
328
- : matchers.some(matcher => matcher.match(relativePath));
329
-
330
- return matchesPattern && !configs.isFileIgnored(entry.path);
331
- }
332
-
332
+ const matchesPattern = unmatchedPatterns.size > 0
333
+ ? matchers.reduce((previousValue, matcher) => {
334
+ const pathMatches = matcher.match(relativePath);
335
+
336
+ /*
337
+ * We updated the unmatched patterns set only if the path
338
+ * matches and the file isn't ignored. If the file is
339
+ * ignored, that means there wasn't a match for the
340
+ * pattern so it should not be removed.
341
+ *
342
+ * Performance note: isFileIgnored() aggressively caches
343
+ * results so there is no performance penalty for calling
344
+ * it twice with the same argument.
345
+ */
346
+ if (pathMatches && !configs.isFileIgnored(entry.path)) {
347
+ unmatchedPatterns.delete(matcher.pattern);
348
+ }
349
+
350
+ return pathMatches || previousValue;
351
+ }, false)
352
+ : matchers.some(matcher => matcher.match(relativePath));
353
+
354
+ return matchesPattern && !configs.isFileIgnored(entry.path);
355
+ })
356
+ },
357
+ (error, entries) => {
358
+
359
+ // If the promise is already rejected, calling `resolve` or `reject` will do nothing.
360
+ if (error) {
361
+ reject(error);
362
+ } else {
363
+ resolve(entries);
364
+ }
365
+ }
366
+ );
333
367
  })).map(entry => entry.path);
334
368
 
335
369
  // now check to see if we have any unmatched patterns
@@ -42,6 +42,7 @@ const {
42
42
  const { pathToFileURL } = require("url");
43
43
  const { FlatConfigArray } = require("../config/flat-config-array");
44
44
  const LintResultCache = require("../cli-engine/lint-result-cache");
45
+ const { Retrier } = require("@humanwhocodes/retry");
45
46
 
46
47
  /*
47
48
  * This is necessary to allow overwriting writeFile for testing purposes.
@@ -851,6 +852,8 @@ class ESLint {
851
852
  errorOnUnmatchedPattern
852
853
  });
853
854
  const controller = new AbortController();
855
+ const retryCodes = new Set(["ENFILE", "EMFILE"]);
856
+ const retrier = new Retrier(error => retryCodes.has(error.code));
854
857
 
855
858
  debug(`${filePaths.length} files found in: ${Date.now() - startTime}ms`);
856
859
 
@@ -919,7 +922,7 @@ class ESLint {
919
922
  fixer = message => shouldMessageBeFixed(message, config, fixTypesSet) && originalFix(message);
920
923
  }
921
924
 
922
- return fs.readFile(filePath, { encoding: "utf8", signal: controller.signal })
925
+ return retrier.retry(() => fs.readFile(filePath, { encoding: "utf8", signal: controller.signal })
923
926
  .then(text => {
924
927
 
925
928
  // fail immediately if an error occurred in another file
@@ -949,11 +952,11 @@ class ESLint {
949
952
  }
950
953
 
951
954
  return result;
952
- }).catch(error => {
955
+ }))
956
+ .catch(error => {
953
957
  controller.abort(error);
954
958
  throw error;
955
959
  });
956
-
957
960
  })
958
961
  );
959
962
 
@@ -1214,5 +1217,6 @@ async function shouldUseFlatConfig() {
1214
1217
 
1215
1218
  module.exports = {
1216
1219
  ESLint,
1217
- shouldUseFlatConfig
1220
+ shouldUseFlatConfig,
1221
+ locateConfigFileToUse
1218
1222
  };
@@ -38,16 +38,16 @@ function compareLocations(itemA, itemB) {
38
38
  * @param {Iterable<Directive>} directives Unused directives to be removed.
39
39
  * @returns {Directive[][]} Directives grouped by their parent comment.
40
40
  */
41
- function groupByParentComment(directives) {
41
+ function groupByParentDirective(directives) {
42
42
  const groups = new Map();
43
43
 
44
44
  for (const directive of directives) {
45
- const { unprocessedDirective: { parentComment } } = directive;
45
+ const { unprocessedDirective: { parentDirective } } = directive;
46
46
 
47
- if (groups.has(parentComment)) {
48
- groups.get(parentComment).push(directive);
47
+ if (groups.has(parentDirective)) {
48
+ groups.get(parentDirective).push(directive);
49
49
  } else {
50
- groups.set(parentComment, [directive]);
50
+ groups.set(parentDirective, [directive]);
51
51
  }
52
52
  }
53
53
 
@@ -57,19 +57,19 @@ function groupByParentComment(directives) {
57
57
  /**
58
58
  * Creates removal details for a set of directives within the same comment.
59
59
  * @param {Directive[]} directives Unused directives to be removed.
60
- * @param {Token} commentToken The backing Comment token.
60
+ * @param {Token} node The backing Comment token.
61
61
  * @returns {{ description, fix, unprocessedDirective }[]} Details for later creation of output Problems.
62
62
  */
63
- function createIndividualDirectivesRemoval(directives, commentToken) {
63
+ function createIndividualDirectivesRemoval(directives, node) {
64
64
 
65
65
  /*
66
- * `commentToken.value` starts right after `//` or `/*`.
66
+ * `node.value` starts right after `//` or `/*`.
67
67
  * All calculated offsets will be relative to this index.
68
68
  */
69
- const commentValueStart = commentToken.range[0] + "//".length;
69
+ const commentValueStart = node.range[0] + "//".length;
70
70
 
71
71
  // Find where the list of rules starts. `\S+` matches with the directive name (e.g. `eslint-disable-line`)
72
- const listStartOffset = /^\s*\S+\s+/u.exec(commentToken.value)[0].length;
72
+ const listStartOffset = /^\s*\S+\s+/u.exec(node.value)[0].length;
73
73
 
74
74
  /*
75
75
  * Get the list text without any surrounding whitespace. In order to preserve the original
@@ -78,7 +78,7 @@ function createIndividualDirectivesRemoval(directives, commentToken) {
78
78
  * // eslint-disable-line rule-one , rule-two , rule-three -- comment
79
79
  * ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
80
80
  */
81
- const listText = commentToken.value
81
+ const listText = node.value
82
82
  .slice(listStartOffset) // remove directive name and all whitespace before the list
83
83
  .split(/\s-{2,}\s/u)[0] // remove `-- comment`, if it exists
84
84
  .trimEnd(); // remove all whitespace after the list
@@ -159,13 +159,13 @@ function createIndividualDirectivesRemoval(directives, commentToken) {
159
159
  }
160
160
 
161
161
  /**
162
- * Creates a description of deleting an entire unused disable comment.
162
+ * Creates a description of deleting an entire unused disable directive.
163
163
  * @param {Directive[]} directives Unused directives to be removed.
164
- * @param {Token} commentToken The backing Comment token.
165
- * @returns {{ description, fix, unprocessedDirective }} Details for later creation of an output Problem.
164
+ * @param {Token} node The backing Comment token.
165
+ * @returns {{ description, fix, unprocessedDirective }} Details for later creation of an output problem.
166
166
  */
167
- function createCommentRemoval(directives, commentToken) {
168
- const { range } = commentToken;
167
+ function createDirectiveRemoval(directives, node) {
168
+ const { range } = node;
169
169
  const ruleIds = directives.filter(directive => directive.ruleId).map(directive => `'${directive.ruleId}'`);
170
170
 
171
171
  return {
@@ -186,20 +186,20 @@ function createCommentRemoval(directives, commentToken) {
186
186
  * @returns {{ description, fix, unprocessedDirective }[]} Details for later creation of output Problems.
187
187
  */
188
188
  function processUnusedDirectives(allDirectives) {
189
- const directiveGroups = groupByParentComment(allDirectives);
189
+ const directiveGroups = groupByParentDirective(allDirectives);
190
190
 
191
191
  return directiveGroups.flatMap(
192
192
  directives => {
193
- const { parentComment } = directives[0].unprocessedDirective;
194
- const remainingRuleIds = new Set(parentComment.ruleIds);
193
+ const { parentDirective } = directives[0].unprocessedDirective;
194
+ const remainingRuleIds = new Set(parentDirective.ruleIds);
195
195
 
196
196
  for (const directive of directives) {
197
197
  remainingRuleIds.delete(directive.ruleId);
198
198
  }
199
199
 
200
200
  return remainingRuleIds.size
201
- ? createIndividualDirectivesRemoval(directives, parentComment.commentToken)
202
- : [createCommentRemoval(directives, parentComment.commentToken)];
201
+ ? createIndividualDirectivesRemoval(directives, parentDirective.node)
202
+ : [createDirectiveRemoval(directives, parentDirective.node)];
203
203
  }
204
204
  );
205
205
  }
@@ -372,7 +372,7 @@ function applyDirectives(options) {
372
372
 
373
373
  const unusedDirectives = processed
374
374
  .map(({ description, fix, unprocessedDirective }) => {
375
- const { parentComment, type, line, column } = unprocessedDirective;
375
+ const { parentDirective, type, line, column } = unprocessedDirective;
376
376
 
377
377
  let message;
378
378
 
@@ -388,8 +388,8 @@ function applyDirectives(options) {
388
388
  return {
389
389
  ruleId: null,
390
390
  message,
391
- line: type === "disable-next-line" ? parentComment.commentToken.loc.start.line : line,
392
- column: type === "disable-next-line" ? parentComment.commentToken.loc.start.column + 1 : column,
391
+ line: type === "disable-next-line" ? parentDirective.node.loc.start.line : line,
392
+ column: type === "disable-next-line" ? parentDirective.node.loc.start.column + 1 : column,
393
393
  severity: options.reportUnusedDisableDirectives === "warn" ? 1 : 2,
394
394
  nodeType: null,
395
395
  ...options.disableFixes ? {} : { fix }
@@ -273,23 +273,21 @@ function createLintingProblem(options) {
273
273
  * Creates a collection of disable directives from a comment
274
274
  * @param {Object} options to create disable directives
275
275
  * @param {("disable"|"enable"|"disable-line"|"disable-next-line")} options.type The type of directive comment
276
- * @param {token} options.commentToken The Comment token
277
276
  * @param {string} options.value The value after the directive in the comment
278
277
  * comment specified no specific rules, so it applies to all rules (e.g. `eslint-disable`)
279
278
  * @param {string} options.justification The justification of the directive
280
- * @param {function(string): {create: Function}} options.ruleMapper A map from rule IDs to defined rules
279
+ * @param {ASTNode|token} options.node The Comment node/token.
280
+ * @param {function(string): {create: Function}} ruleMapper A map from rule IDs to defined rules
281
281
  * @returns {Object} Directives and problems from the comment
282
282
  */
283
- function createDisableDirectives(options) {
284
- const { commentToken, type, value, justification, ruleMapper } = options;
283
+ function createDisableDirectives({ type, value, justification, node }, ruleMapper) {
285
284
  const ruleIds = Object.keys(commentParser.parseListConfig(value));
286
285
  const directiveRules = ruleIds.length ? ruleIds : [null];
287
286
  const result = {
288
287
  directives: [], // valid disable directives
289
288
  directiveProblems: [] // problems in directives
290
289
  };
291
-
292
- const parentComment = { commentToken, ruleIds };
290
+ const parentDirective = { node, ruleIds };
293
291
 
294
292
  for (const ruleId of directiveRules) {
295
293
 
@@ -297,25 +295,25 @@ function createDisableDirectives(options) {
297
295
  if (ruleId === null || !!ruleMapper(ruleId)) {
298
296
  if (type === "disable-next-line") {
299
297
  result.directives.push({
300
- parentComment,
298
+ parentDirective,
301
299
  type,
302
- line: commentToken.loc.end.line,
303
- column: commentToken.loc.end.column + 1,
300
+ line: node.loc.end.line,
301
+ column: node.loc.end.column + 1,
304
302
  ruleId,
305
303
  justification
306
304
  });
307
305
  } else {
308
306
  result.directives.push({
309
- parentComment,
307
+ parentDirective,
310
308
  type,
311
- line: commentToken.loc.start.line,
312
- column: commentToken.loc.start.column + 1,
309
+ line: node.loc.start.line,
310
+ column: node.loc.start.column + 1,
313
311
  ruleId,
314
312
  justification
315
313
  });
316
314
  }
317
315
  } else {
318
- result.directiveProblems.push(createLintingProblem({ ruleId, loc: commentToken.loc }));
316
+ result.directiveProblems.push(createLintingProblem({ ruleId, loc: node.loc }));
319
317
  }
320
318
  }
321
319
  return result;
@@ -388,8 +386,12 @@ function getDirectiveComments(sourceCode, ruleMapper, warnInlineConfig, config)
388
386
  case "eslint-disable-next-line":
389
387
  case "eslint-disable-line": {
390
388
  const directiveType = directiveText.slice("eslint-".length);
391
- const options = { commentToken: comment, type: directiveType, value: directiveValue, justification: justificationPart, ruleMapper };
392
- const { directives, directiveProblems } = createDisableDirectives(options);
389
+ const { directives, directiveProblems } = createDisableDirectives({
390
+ type: directiveType,
391
+ value: directiveValue,
392
+ justification: justificationPart,
393
+ node: comment
394
+ }, ruleMapper);
393
395
 
394
396
  disableDirectives.push(...directives);
395
397
  problems.push(...directiveProblems);
@@ -543,53 +545,21 @@ function getDirectiveComments(sourceCode, ruleMapper, warnInlineConfig, config)
543
545
  * A collection of the directive comments that were found, along with any problems that occurred when parsing
544
546
  */
545
547
  function getDirectiveCommentsForFlatConfig(sourceCode, ruleMapper) {
546
- const problems = [];
547
548
  const disableDirectives = [];
549
+ const problems = [];
548
550
 
549
- sourceCode.getInlineConfigNodes().filter(token => token.type !== "Shebang").forEach(comment => {
550
- const { directivePart, justificationPart } = commentParser.extractDirectiveComment(comment.value);
551
-
552
- const match = directivesPattern.exec(directivePart);
553
-
554
- if (!match) {
555
- return;
556
- }
557
- const directiveText = match[1];
558
- const lineCommentSupported = /^eslint-disable-(next-)?line$/u.test(directiveText);
559
-
560
- if (comment.type === "Line" && !lineCommentSupported) {
561
- return;
562
- }
563
-
564
- if (directiveText === "eslint-disable-line" && comment.loc.start.line !== comment.loc.end.line) {
565
- const message = `${directiveText} comment should not span multiple lines.`;
566
-
567
- problems.push(createLintingProblem({
568
- ruleId: null,
569
- message,
570
- loc: comment.loc
571
- }));
572
- return;
573
- }
574
-
575
- const directiveValue = directivePart.slice(match.index + directiveText.length);
551
+ const {
552
+ directives: directivesSources,
553
+ problems: directivesProblems
554
+ } = sourceCode.getDisableDirectives();
576
555
 
577
- switch (directiveText) {
578
- case "eslint-disable":
579
- case "eslint-enable":
580
- case "eslint-disable-next-line":
581
- case "eslint-disable-line": {
582
- const directiveType = directiveText.slice("eslint-".length);
583
- const options = { commentToken: comment, type: directiveType, value: directiveValue, justification: justificationPart, ruleMapper };
584
- const { directives, directiveProblems } = createDisableDirectives(options);
556
+ problems.push(...directivesProblems.map(createLintingProblem));
585
557
 
586
- disableDirectives.push(...directives);
587
- problems.push(...directiveProblems);
588
- break;
589
- }
558
+ directivesSources.forEach(directive => {
559
+ const { directives, directiveProblems } = createDisableDirectives(directive, ruleMapper);
590
560
 
591
- // no default
592
- }
561
+ disableDirectives.push(...directives);
562
+ problems.push(...directiveProblems);
593
563
  });
594
564
 
595
565
  return {
package/lib/options.js CHANGED
@@ -38,7 +38,7 @@ const optionator = require("optionator");
38
38
  * @property {boolean} [help] Show help
39
39
  * @property {boolean} ignore Disable use of ignore files and patterns
40
40
  * @property {string} [ignorePath] Specify path of ignore file
41
- * @property {string[]} [ignorePattern] Pattern of files to ignore (in addition to those in .eslintignore)
41
+ * @property {string[]} [ignorePattern] Patterns of files to ignore. In eslintrc mode, these are in addition to `.eslintignore`
42
42
  * @property {boolean} init Run config initialization wizard
43
43
  * @property {boolean} inlineConfig Prevent comments from changing config or rules
44
44
  * @property {number} maxWarnings Number of warnings to trigger nonzero exit code
@@ -261,7 +261,7 @@ module.exports = function(usingFlatConfig) {
261
261
  {
262
262
  option: "ignore-pattern",
263
263
  type: "[String]",
264
- description: "Pattern of files to ignore (in addition to those in .eslintignore)",
264
+ description: `Patterns of files to ignore${usingFlatConfig ? "" : " (in addition to those in .eslintignore)"}`,
265
265
  concatRepeatedArrays: [true, {
266
266
  oneValuePerFlag: true
267
267
  }]
@@ -31,8 +31,7 @@ module.exports = {
31
31
  type: "object",
32
32
  properties: {
33
33
  checkLoops: {
34
- type: "boolean",
35
- default: true
34
+ enum: ["all", "allExceptWhileTrue", "none", true, false]
36
35
  }
37
36
  },
38
37
  additionalProperties: false
@@ -45,11 +44,17 @@ module.exports = {
45
44
  },
46
45
 
47
46
  create(context) {
48
- const options = context.options[0] || {},
49
- checkLoops = options.checkLoops !== false,
50
- loopSetStack = [];
47
+ const options = context.options[0] || {};
48
+ let checkLoops = options.checkLoops ?? "allExceptWhileTrue";
49
+ const loopSetStack = [];
51
50
  const sourceCode = context.sourceCode;
52
51
 
52
+ if (options.checkLoops === true) {
53
+ checkLoops = "all";
54
+ } else if (options.checkLoops === false) {
55
+ checkLoops = "none";
56
+ }
57
+
53
58
  let loopsInCurrentScope = new Set();
54
59
 
55
60
  //--------------------------------------------------------------------------
@@ -120,7 +125,7 @@ module.exports = {
120
125
  * @private
121
126
  */
122
127
  function checkLoop(node) {
123
- if (checkLoops) {
128
+ if (checkLoops === "all" || checkLoops === "allExceptWhileTrue") {
124
129
  trackConstantConditionLoop(node);
125
130
  }
126
131
  }
@@ -132,7 +137,13 @@ module.exports = {
132
137
  return {
133
138
  ConditionalExpression: reportIfConstant,
134
139
  IfStatement: reportIfConstant,
135
- WhileStatement: checkLoop,
140
+ WhileStatement(node) {
141
+ if (node.test.type === "Literal" && node.test.value === true && checkLoops === "allExceptWhileTrue") {
142
+ return;
143
+ }
144
+
145
+ checkLoop(node);
146
+ },
136
147
  "WhileStatement:exit": checkConstantConditionLoopInSet,
137
148
  DoWhileStatement: checkLoop,
138
149
  "DoWhileStatement:exit": checkConstantConditionLoopInSet,
@@ -5,12 +5,6 @@
5
5
 
6
6
  "use strict";
7
7
 
8
- //------------------------------------------------------------------------------
9
- // Requirements
10
- //------------------------------------------------------------------------------
11
-
12
- const Graphemer = require("graphemer").default;
13
-
14
8
  //------------------------------------------------------------------------------
15
9
  // Helpers
16
10
  //------------------------------------------------------------------------------
@@ -18,8 +12,8 @@ const Graphemer = require("graphemer").default;
18
12
  // eslint-disable-next-line no-control-regex -- intentionally including control characters
19
13
  const ASCII_REGEX = /^[\u0000-\u007f]*$/u;
20
14
 
21
- /** @type {Graphemer | undefined} */
22
- let splitter;
15
+ /** @type {Intl.Segmenter | undefined} */
16
+ let segmenter;
23
17
 
24
18
  //------------------------------------------------------------------------------
25
19
  // Public Interface
@@ -47,11 +41,15 @@ function getGraphemeCount(value) {
47
41
  return value.length;
48
42
  }
49
43
 
50
- if (!splitter) {
51
- splitter = new Graphemer();
44
+ segmenter ??= new Intl.Segmenter("en-US"); // en-US locale should be supported everywhere
45
+ let graphemeCount = 0;
46
+
47
+ // eslint-disable-next-line no-unused-vars -- for-of needs a variable
48
+ for (const unused of segmenter.segment(value)) {
49
+ graphemeCount++;
52
50
  }
53
51
 
54
- return splitter.countGraphemes(value);
52
+ return graphemeCount;
55
53
  }
56
54
 
57
55
  module.exports = {
@@ -373,6 +373,56 @@ class TraversalStep {
373
373
  }
374
374
  }
375
375
 
376
+ /**
377
+ * A class to represent a directive comment.
378
+ */
379
+ class Directive {
380
+
381
+ /**
382
+ * The type of directive.
383
+ * @type {"disable"|"enable"|"disable-next-line"|"disable-line"}
384
+ * @readonly
385
+ */
386
+ type;
387
+
388
+ /**
389
+ * The node representing the directive.
390
+ * @type {ASTNode|Comment}
391
+ * @readonly
392
+ */
393
+ node;
394
+
395
+ /**
396
+ * Everything after the "eslint-disable" portion of the directive,
397
+ * but before the "--" that indicates the justification.
398
+ * @type {string}
399
+ * @readonly
400
+ */
401
+ value;
402
+
403
+ /**
404
+ * The justification for the directive.
405
+ * @type {string}
406
+ * @readonly
407
+ */
408
+ justification;
409
+
410
+ /**
411
+ * Creates a new instance.
412
+ * @param {Object} options The options for the directive.
413
+ * @param {"disable"|"enable"|"disable-next-line"|"disable-line"} options.type The type of directive.
414
+ * @param {ASTNode|Comment} options.node The node representing the directive.
415
+ * @param {string} options.value The value of the directive.
416
+ * @param {string} options.justification The justification for the directive.
417
+ */
418
+ constructor({ type, node, value, justification }) {
419
+ this.type = type;
420
+ this.node = node;
421
+ this.value = value;
422
+ this.justification = justification;
423
+ }
424
+ }
425
+
376
426
  //------------------------------------------------------------------------------
377
427
  // Public Interface
378
428
  //------------------------------------------------------------------------------
@@ -921,6 +971,84 @@ class SourceCode extends TokenStore {
921
971
  return configNodes;
922
972
  }
923
973
 
974
+ /**
975
+ * Returns an all directive nodes that enable or disable rules along with any problems
976
+ * encountered while parsing the directives.
977
+ * @returns {{problems:Array<Problem>,directives:Array<Directive>}} Information
978
+ * that ESLint needs to further process the directives.
979
+ */
980
+ getDisableDirectives() {
981
+
982
+ // check the cache first
983
+ const cachedDirectives = this[caches].get("disableDirectives");
984
+
985
+ if (cachedDirectives) {
986
+ return cachedDirectives;
987
+ }
988
+
989
+ const problems = [];
990
+ const directives = [];
991
+
992
+ this.getInlineConfigNodes().forEach(comment => {
993
+ const { directivePart, justificationPart } = commentParser.extractDirectiveComment(comment.value);
994
+
995
+ // Step 1: Extract the directive text
996
+ const match = directivesPattern.exec(directivePart);
997
+
998
+ if (!match) {
999
+ return;
1000
+ }
1001
+
1002
+ const directiveText = match[1];
1003
+
1004
+ // Step 2: Extract the directive value
1005
+ const lineCommentSupported = /^eslint-disable-(next-)?line$/u.test(directiveText);
1006
+
1007
+ if (comment.type === "Line" && !lineCommentSupported) {
1008
+ return;
1009
+ }
1010
+
1011
+ // Step 3: Validate the directive does not span multiple lines
1012
+ if (directiveText === "eslint-disable-line" && comment.loc.start.line !== comment.loc.end.line) {
1013
+ const message = `${directiveText} comment should not span multiple lines.`;
1014
+
1015
+ problems.push({
1016
+ ruleId: null,
1017
+ message,
1018
+ loc: comment.loc
1019
+ });
1020
+ return;
1021
+ }
1022
+
1023
+ // Step 4: Extract the directive value and create the Directive object
1024
+ const directiveValue = directivePart.slice(match.index + directiveText.length);
1025
+
1026
+ switch (directiveText) {
1027
+ case "eslint-disable":
1028
+ case "eslint-enable":
1029
+ case "eslint-disable-next-line":
1030
+ case "eslint-disable-line": {
1031
+ const directiveType = directiveText.slice("eslint-".length);
1032
+
1033
+ directives.push(new Directive({
1034
+ type: directiveType,
1035
+ node: comment,
1036
+ value: directiveValue,
1037
+ justification: justificationPart
1038
+ }));
1039
+ }
1040
+
1041
+ // no default
1042
+ }
1043
+ });
1044
+
1045
+ const result = { problems, directives };
1046
+
1047
+ this[caches].set("disableDirectives", result);
1048
+
1049
+ return result;
1050
+ }
1051
+
924
1052
  /**
925
1053
  * Applies language options sent in from the core.
926
1054
  * @param {Object} languageOptions The language options for this run.
@@ -6,7 +6,7 @@ module.exports = function(it) {
6
6
  return `
7
7
  ESLint couldn't find a configuration file. To set up a configuration file for this project, please run:
8
8
 
9
- npm init @eslint/config
9
+ npm init @eslint/config@latest
10
10
 
11
11
  ESLint looked for configuration files in ${directoryPath} and its ancestors. If it found none, it then looked in your home directory.
12
12
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint",
3
- "version": "9.0.0",
3
+ "version": "9.1.1",
4
4
  "author": "Nicholas C. Zakas <nicholas+npm@nczconsulting.com>",
5
5
  "description": "An AST-based pattern checker for JavaScript.",
6
6
  "bin": {
@@ -33,7 +33,8 @@
33
33
  "test:browser": "node Makefile.js wdio",
34
34
  "test:cli": "mocha",
35
35
  "test:fuzz": "node Makefile.js fuzz",
36
- "test:performance": "node Makefile.js perf"
36
+ "test:performance": "node Makefile.js perf",
37
+ "test:emfile": "node tools/check-emfile-handling.js"
37
38
  },
38
39
  "gitHooks": {
39
40
  "pre-commit": "lint-staged"
@@ -68,9 +69,10 @@
68
69
  "@eslint-community/eslint-utils": "^4.2.0",
69
70
  "@eslint-community/regexpp": "^4.6.1",
70
71
  "@eslint/eslintrc": "^3.0.2",
71
- "@eslint/js": "9.0.0",
72
- "@humanwhocodes/config-array": "^0.12.3",
72
+ "@eslint/js": "9.1.1",
73
+ "@humanwhocodes/config-array": "^0.13.0",
73
74
  "@humanwhocodes/module-importer": "^1.0.1",
75
+ "@humanwhocodes/retry": "^0.2.3",
74
76
  "@nodelib/fs.walk": "^1.2.8",
75
77
  "ajv": "^6.12.4",
76
78
  "chalk": "^4.0.0",
@@ -86,7 +88,6 @@
86
88
  "file-entry-cache": "^8.0.0",
87
89
  "find-up": "^5.0.0",
88
90
  "glob-parent": "^6.0.2",
89
- "graphemer": "^1.4.0",
90
91
  "ignore": "^5.2.0",
91
92
  "imurmurhash": "^0.1.4",
92
93
  "is-glob": "^4.0.0",
@@ -120,7 +121,7 @@
120
121
  "eslint": "file:.",
121
122
  "eslint-config-eslint": "file:packages/eslint-config-eslint",
122
123
  "eslint-plugin-eslint-comments": "^3.2.0",
123
- "eslint-plugin-eslint-plugin": "^5.2.1",
124
+ "eslint-plugin-eslint-plugin": "^6.0.0",
124
125
  "eslint-plugin-internal-rules": "file:tools/internal-rules",
125
126
  "eslint-plugin-jsdoc": "^46.9.0",
126
127
  "eslint-plugin-n": "^16.6.0",
@@ -131,7 +132,7 @@
131
132
  "fast-glob": "^3.2.11",
132
133
  "fs-teardown": "^0.1.3",
133
134
  "glob": "^10.0.0",
134
- "globals": "^14.0.0",
135
+ "globals": "^15.0.0",
135
136
  "got": "^11.8.3",
136
137
  "gray-matter": "^4.0.3",
137
138
  "js-yaml": "^4.1.0",