eslint 9.33.0 → 9.35.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.
Files changed (42) hide show
  1. package/README.md +1 -1
  2. package/lib/cli-engine/file-enumerator.js +1 -1
  3. package/lib/cli.js +62 -266
  4. package/lib/config/flat-config-schema.js +1 -1
  5. package/lib/eslint/eslint-helpers.js +426 -6
  6. package/lib/eslint/eslint.js +381 -313
  7. package/lib/eslint/worker.js +164 -0
  8. package/lib/languages/js/source-code/source-code.js +3 -4
  9. package/lib/linter/esquery.js +3 -0
  10. package/lib/linter/interpolate.js +1 -1
  11. package/lib/linter/linter.js +3 -4
  12. package/lib/options.js +23 -9
  13. package/lib/rule-tester/rule-tester.js +3 -0
  14. package/lib/rules/array-callback-return.js +0 -1
  15. package/lib/rules/dot-notation.js +1 -1
  16. package/lib/rules/grouped-accessor-pairs.js +8 -7
  17. package/lib/rules/indent-legacy.js +1 -1
  18. package/lib/rules/index.js +1 -0
  19. package/lib/rules/no-alert.js +1 -1
  20. package/lib/rules/no-empty-function.js +20 -1
  21. package/lib/rules/no-empty-static-block.js +25 -1
  22. package/lib/rules/no-empty.js +37 -0
  23. package/lib/rules/no-eval.js +3 -1
  24. package/lib/rules/no-irregular-whitespace.js +2 -2
  25. package/lib/rules/no-loss-of-precision.js +26 -3
  26. package/lib/rules/no-mixed-spaces-and-tabs.js +1 -0
  27. package/lib/rules/no-octal.js +1 -4
  28. package/lib/rules/no-trailing-spaces.js +2 -1
  29. package/lib/rules/no-useless-escape.js +1 -1
  30. package/lib/rules/prefer-regex-literals.js +1 -1
  31. package/lib/rules/preserve-caught-error.js +509 -0
  32. package/lib/rules/strict.js +2 -1
  33. package/lib/rules/utils/ast-utils.js +1 -1
  34. package/lib/rules/utils/char-source.js +1 -1
  35. package/lib/rules/yoda.js +2 -2
  36. package/lib/services/suppressions-service.js +3 -0
  37. package/lib/services/warning-service.js +13 -0
  38. package/lib/shared/naming.js +1 -1
  39. package/lib/shared/translate-cli-options.js +281 -0
  40. package/lib/types/index.d.ts +7 -0
  41. package/lib/types/rules.d.ts +22 -14
  42. package/package.json +3 -3
@@ -9,11 +9,13 @@
9
9
  // Requirements
10
10
  //------------------------------------------------------------------------------
11
11
 
12
- const fs = require("node:fs/promises");
13
12
  const { existsSync } = require("node:fs");
13
+ const fs = require("node:fs/promises");
14
+ const os = require("node:os");
14
15
  const path = require("node:path");
16
+ const { pathToFileURL } = require("node:url");
17
+ const { SHARE_ENV, Worker } = require("node:worker_threads");
15
18
  const { version } = require("../../package.json");
16
- const { Linter } = require("../linter");
17
19
  const { defaultConfig } = require("../config/default-config");
18
20
 
19
21
  const {
@@ -25,14 +27,21 @@ const {
25
27
 
26
28
  createIgnoreResult,
27
29
  isErrorMessage,
28
- calculateStatsPerFile,
30
+ getPlaceholderPath,
29
31
 
30
32
  processOptions,
33
+ loadOptionsFromModule,
34
+
35
+ getFixerForFixTypes,
36
+ verifyText,
37
+ lintFile,
38
+ createLinter,
39
+ createLintResultCache,
40
+ createDefaultConfigs,
41
+ createConfigLoader,
31
42
  } = require("./eslint-helpers");
32
- const { pathToFileURL } = require("node:url");
33
- const LintResultCache = require("../cli-engine/lint-result-cache");
34
43
  const { Retrier } = require("@humanwhocodes/retry");
35
- const { ConfigLoader, LegacyConfigLoader } = require("../config/config-loader");
44
+ const { ConfigLoader } = require("../config/config-loader");
36
45
  const { WarningService } = require("../services/warning-service");
37
46
  const { Config } = require("../config/config.js");
38
47
  const {
@@ -53,16 +62,14 @@ const { resolve } = require("../shared/relative-module-resolver.js");
53
62
 
54
63
  // For VSCode IntelliSense
55
64
  /**
56
- * @import { ConfigArray } from "../cli-engine/cli-engine.js";
57
- * @import { CLIEngineLintReport } from "./legacy-eslint.js";
65
+ * @import { Config as CalculatedConfig } from "../config/config.js";
58
66
  * @import { FlatConfigArray } from "../config/flat-config-array.js";
59
- * @import { RuleDefinition } from "@eslint/core";
67
+ * @import { RuleDefinition, RulesMeta } from "@eslint/core";
68
+ * @import { WorkerLintResults } from "./worker.js";
60
69
  */
61
70
 
62
- /** @typedef {ReturnType<ConfigArray.extractConfig>} ExtractedConfig */
63
71
  /** @typedef {import("../types").Linter.Config} Config */
64
72
  /** @typedef {import("../types").ESLint.DeprecatedRuleUse} DeprecatedRuleInfo */
65
- /** @typedef {import("../types").Linter.LintMessage} LintMessage */
66
73
  /** @typedef {import("../types").ESLint.LintResult} LintResult */
67
74
  /** @typedef {import("../types").ESLint.Plugin} Plugin */
68
75
  /** @typedef {import("../types").ESLint.ResultsMeta} ResultsMeta */
@@ -75,6 +82,7 @@ const { resolve } = require("../shared/relative-module-resolver.js");
75
82
  * @property {boolean} [cache] Enable result caching.
76
83
  * @property {string} [cacheLocation] The cache file to use instead of .eslintcache.
77
84
  * @property {"metadata" | "content"} [cacheStrategy] The strategy used to detect changed files.
85
+ * @property {number | "auto" | "off"} [concurrency] Maximum number of linting threads, "auto" to choose automatically, "off" for no multithreading.
78
86
  * @property {string} [cwd] The value to use for the current working directory.
79
87
  * @property {boolean} [errorOnUnmatchedPattern] If `false` then `ESLint#lintFiles()` doesn't throw even if no target files found. Defaults to `true`.
80
88
  * @property {boolean|Function} [fix] Execute in autofix mode. If a function, should return a boolean.
@@ -87,11 +95,11 @@ const { resolve } = require("../shared/relative-module-resolver.js");
87
95
  * @property {boolean|string} [overrideConfigFile] Searches for default config file when falsy;
88
96
  * doesn't do any config file lookup when `true`; considered to be a config filename
89
97
  * when a string.
98
+ * @property {boolean} [passOnNoPatterns=false] When set to true, missing patterns cause
99
+ * the linting operation to short circuit and not report any failures.
90
100
  * @property {Record<string,Plugin>} [plugins] An array of plugin implementations.
91
101
  * @property {boolean} [stats] True enables added statistics on lint results.
92
102
  * @property {boolean} [warnIgnored] Show warnings when the file list includes ignored files
93
- * @property {boolean} [passOnNoPatterns=false] When set to true, missing patterns cause
94
- * the linting operation to short circuit and not report any failures.
95
103
  */
96
104
 
97
105
  //------------------------------------------------------------------------------
@@ -111,11 +119,12 @@ const removedFormatters = new Set([
111
119
  "unix",
112
120
  "visualstudio",
113
121
  ]);
122
+ const fileRetryCodes = new Set(["ENFILE", "EMFILE"]);
114
123
 
115
124
  /**
116
125
  * Create rulesMeta object.
117
- * @param {Map<string,RuleDefinition>} rules a map of rules from which to generate the object.
118
- * @returns {Object} metadata for all enabled rules.
126
+ * @param {Map<string, RuleDefinition>} rules a map of rules from which to generate the object.
127
+ * @returns {Record<string, RulesMeta>} metadata for all enabled rules.
119
128
  */
120
129
  function createRulesMeta(rules) {
121
130
  return Array.from(rules).reduce((retVal, [id, rule]) => {
@@ -124,17 +133,7 @@ function createRulesMeta(rules) {
124
133
  }, {});
125
134
  }
126
135
 
127
- /**
128
- * Return the absolute path of a file named `"__placeholder__.js"` in a given directory.
129
- * This is used as a replacement for a missing file path.
130
- * @param {string} cwd An absolute directory path.
131
- * @returns {string} The absolute path of a file named `"__placeholder__.js"` in the given directory.
132
- */
133
- function getPlaceholderPath(cwd) {
134
- return path.join(cwd, "__placeholder__.js");
135
- }
136
-
137
- /** @type {WeakMap<ExtractedConfig, DeprecatedRuleInfo[]>} */
136
+ /** @type {WeakMap<CalculatedConfig, DeprecatedRuleInfo[]>} */
138
137
  const usedDeprecatedRulesCache = new WeakMap();
139
138
 
140
139
  /**
@@ -193,10 +192,10 @@ function getOrFindUsedDeprecatedRules(eslint, maybeFilePath) {
193
192
  * Processes the linting results generated by a CLIEngine linting report to
194
193
  * match the ESLint class's API.
195
194
  * @param {ESLint} eslint The ESLint instance.
196
- * @param {CLIEngineLintReport} report The CLIEngine linting report to process.
195
+ * @param {LintResult[]} results The linting results to process.
197
196
  * @returns {LintResult[]} The processed linting results.
198
197
  */
199
- function processLintReport(eslint, { results }) {
198
+ function processLintReport(eslint, results) {
200
199
  const descriptor = {
201
200
  configurable: true,
202
201
  enumerable: true,
@@ -263,146 +262,295 @@ async function locateConfigFileToUse({ configFile, cwd }) {
263
262
  }
264
263
 
265
264
  /**
266
- * Processes an source code using ESLint.
267
- * @param {Object} config The config object.
268
- * @param {string} config.text The source code to verify.
269
- * @param {string} config.cwd The path to the current working directory.
270
- * @param {string|undefined} config.filePath The path to the file of `text`. If this is undefined, it uses `<text>`.
271
- * @param {FlatConfigArray} config.configs The config.
272
- * @param {boolean} config.fix If `true` then it does fix.
273
- * @param {boolean} config.allowInlineConfig If `true` then it uses directive comments.
274
- * @param {Function} config.ruleFilter A predicate function to filter which rules should be run.
275
- * @param {boolean} config.stats If `true`, then if reports extra statistics with the lint results.
276
- * @param {Linter} config.linter The linter instance to verify.
277
- * @returns {LintResult} The result of linting.
278
- * @private
265
+ * Creates an error to be thrown when an array of results passed to `getRulesMetaForResults` was not created by the current engine.
266
+ * @param {Error|undefined} cause The original error that led to this symptom error being thrown. Might not always be available.
267
+ * @returns {TypeError} An error object.
279
268
  */
280
- function verifyText({
281
- text,
282
- cwd,
283
- filePath: providedFilePath,
284
- configs,
285
- fix,
286
- allowInlineConfig,
287
- ruleFilter,
288
- stats,
289
- linter,
290
- }) {
291
- const filePath = providedFilePath || "<text>";
292
-
293
- debug(`Lint ${filePath}`);
294
-
295
- /*
296
- * Verify.
297
- * `config.extractConfig(filePath)` requires an absolute path, but `linter`
298
- * doesn't know CWD, so it gives `linter` an absolute path always.
299
- */
300
- const filePathToVerify =
301
- filePath === "<text>" ? getPlaceholderPath(cwd) : filePath;
302
- const { fixed, messages, output } = linter.verifyAndFix(text, configs, {
303
- allowInlineConfig,
304
- filename: filePathToVerify,
305
- fix,
306
- ruleFilter,
307
- stats,
308
-
309
- /**
310
- * Check if the linter should adopt a given code block or not.
311
- * @param {string} blockFilename The virtual filename of a code block.
312
- * @returns {boolean} `true` if the linter should adopt the code block.
313
- */
314
- filterCodeBlock(blockFilename) {
315
- return configs.getConfig(blockFilename) !== void 0;
269
+ function createExtraneousResultsError(cause) {
270
+ return new TypeError(
271
+ "Results object was not created from this ESLint instance.",
272
+ {
273
+ cause,
316
274
  },
317
- });
318
-
319
- // Tweak and return.
320
- const result = {
321
- filePath: filePath === "<text>" ? filePath : path.resolve(filePath),
322
- messages,
323
- suppressedMessages: linter.getSuppressedMessages(),
324
- ...calculateStatsPerFile(messages),
325
- };
275
+ );
276
+ }
326
277
 
327
- if (fixed) {
328
- result.output = output;
329
- }
278
+ /**
279
+ * Maximum number of files assumed to be best handled by one worker thread.
280
+ * This value is a heuristic estimation that can be adjusted if required.
281
+ */
282
+ const AUTO_FILES_PER_WORKER = 35;
330
283
 
331
- if (
332
- result.errorCount + result.warningCount > 0 &&
333
- typeof result.output === "undefined"
334
- ) {
335
- result.source = text;
284
+ /**
285
+ * Calculates the number of workers to run based on the concurrency setting and the number of files to lint.
286
+ * @param {number | "auto" | "off"} concurrency The normalized concurrency setting.
287
+ * @param {number} fileCount The number of files to be linted.
288
+ * @param {{ availableParallelism: () => number }} [os] Node.js `os` module, or a mock for testing.
289
+ * @returns {number} The effective number of worker threads to be started. A value of zero disables multithread linting.
290
+ */
291
+ function calculateWorkerCount(
292
+ concurrency,
293
+ fileCount,
294
+ { availableParallelism } = os,
295
+ ) {
296
+ let workerCount;
297
+ switch (concurrency) {
298
+ case "off":
299
+ return 0;
300
+ case "auto": {
301
+ workerCount = Math.min(
302
+ availableParallelism() >> 1,
303
+ Math.ceil(fileCount / AUTO_FILES_PER_WORKER),
304
+ );
305
+ break;
306
+ }
307
+ default:
308
+ workerCount = Math.min(concurrency, fileCount);
309
+ break;
336
310
  }
311
+ return workerCount > 1 ? workerCount : 0;
312
+ }
337
313
 
338
- if (stats) {
339
- result.stats = {
340
- times: linter.getTimes(),
341
- fixPasses: linter.getFixPassCount(),
342
- };
343
- }
314
+ // Used internally. Do not expose.
315
+ const disableCloneabilityCheck = Symbol(
316
+ "Do not check for uncloneable options.",
317
+ );
344
318
 
345
- return result;
346
- }
319
+ /**
320
+ * The smallest net linting ratio that doesn't trigger a poor concurrency warning.
321
+ * The net linting ratio is defined as the net linting duration divided by the thread's total runtime,
322
+ * where the net linting duration is the total linting time minus the time spent on I/O-intensive operations:
323
+ * **Net Linting Ratio** = (**Linting Time** – **I/O Time**) / **Thread Runtime**.
324
+ * - **Linting Time**: Total time spent linting files
325
+ * - **I/O Time**: Portion of linting time spent loading configs and reading files
326
+ * - **Thread Runtime**: End-to-end execution time of the thread
327
+ *
328
+ * This value is a heuristic estimation that can be adjusted if required.
329
+ */
330
+ const LOW_NET_LINTING_RATIO = 0.7;
347
331
 
348
332
  /**
349
- * Checks whether a message's rule type should be fixed.
350
- * @param {LintMessage} message The message to check.
351
- * @param {FlatConfigArray} config The config for the file that generated the message.
352
- * @param {string[]} fixTypes An array of fix types to check.
353
- * @returns {boolean} Whether the message should be fixed.
333
+ * Runs worker threads to lint files.
334
+ * @param {string[]} filePaths File paths to lint.
335
+ * @param {number} workerCount The number of worker threads to run.
336
+ * @param {ESLintOptions | string} eslintOptionsOrURL The unprocessed ESLint options or the URL of the options module.
337
+ * @param {() => void} warnOnLowNetLintingRatio A function to call if the net linting ratio is low.
338
+ * @returns {Promise<LintResult[]>} Lint results.
354
339
  */
355
- function shouldMessageBeFixed(message, config, fixTypes) {
356
- if (!message.ruleId) {
357
- return fixTypes.has("directive");
340
+ async function runWorkers(
341
+ filePaths,
342
+ workerCount,
343
+ eslintOptionsOrURL,
344
+ warnOnLowNetLintingRatio,
345
+ ) {
346
+ const fileCount = filePaths.length;
347
+ const results = Array(fileCount);
348
+ const workerURL = pathToFileURL(path.join(__dirname, "./worker.js"));
349
+ const filePathIndexArray = new Uint32Array(
350
+ new SharedArrayBuffer(Uint32Array.BYTES_PER_ELEMENT),
351
+ );
352
+ const abortController = new AbortController();
353
+ const abortSignal = abortController.signal;
354
+ const workerOptions = {
355
+ env: SHARE_ENV,
356
+ workerData: {
357
+ eslintOptionsOrURL,
358
+ filePathIndexArray,
359
+ filePaths,
360
+ },
361
+ };
362
+
363
+ const hrtimeBigint = process.hrtime.bigint;
364
+ let worstNetLintingRatio = 1;
365
+
366
+ /**
367
+ * A promise executor function that starts a worker thread on each invocation.
368
+ * @param {() => void} resolve_ Called when the worker thread terminates successfully.
369
+ * @param {(error: Error) => void} reject Called when the worker thread terminates with an error.
370
+ * @returns {void}
371
+ */
372
+ function workerExecutor(resolve_, reject) {
373
+ const workerStartTime = hrtimeBigint();
374
+ const worker = new Worker(workerURL, workerOptions);
375
+ worker.once(
376
+ "message",
377
+ (/** @type {WorkerLintResults} */ indexedResults) => {
378
+ const workerDuration = hrtimeBigint() - workerStartTime;
379
+
380
+ // The net linting ratio provides an approximate measure of worker thread efficiency, defined as the net linting duration divided by the thread's total runtime.
381
+ const netLintingRatio =
382
+ Number(indexedResults.netLintingDuration) /
383
+ Number(workerDuration);
384
+
385
+ worstNetLintingRatio = Math.min(
386
+ worstNetLintingRatio,
387
+ netLintingRatio,
388
+ );
389
+ for (const result of indexedResults) {
390
+ const { index } = result;
391
+ delete result.index;
392
+ results[index] = result;
393
+ }
394
+ resolve_();
395
+ },
396
+ );
397
+ worker.once("error", error => {
398
+ abortController.abort(error);
399
+ reject(error);
400
+ });
401
+ abortSignal.addEventListener("abort", () => worker.terminate());
358
402
  }
359
403
 
360
- const rule = message.ruleId && config.getRuleDefinition(message.ruleId);
404
+ const promises = Array(workerCount);
405
+ for (let index = 0; index < workerCount; ++index) {
406
+ promises[index] = new Promise(workerExecutor);
407
+ }
408
+ await Promise.all(promises);
409
+
410
+ if (worstNetLintingRatio < LOW_NET_LINTING_RATIO) {
411
+ warnOnLowNetLintingRatio();
412
+ }
361
413
 
362
- return Boolean(rule && rule.meta && fixTypes.has(rule.meta.type));
414
+ return results;
363
415
  }
364
416
 
365
417
  /**
366
- * Creates an error to be thrown when an array of results passed to `getRulesMetaForResults` was not created by the current engine.
367
- * @returns {TypeError} An error object.
418
+ * Lint files in multithread mode.
419
+ * @param {ESLint} eslint ESLint instance.
420
+ * @param {string[]} filePaths File paths to lint.
421
+ * @param {number} workerCount The number of worker threads to run.
422
+ * @param {ESLintOptions | string} eslintOptionsOrURL The unprocessed ESLint options or the URL of the options module.
423
+ * @param {() => void} warnOnLowNetLintingRatio A function to call if the net linting ratio is low.
424
+ * @returns {Promise<LintResult[]>} Lint results.
368
425
  */
369
- function createExtraneousResultsError() {
370
- return new TypeError(
371
- "Results object was not created from this ESLint instance.",
426
+ async function lintFilesWithMultithreading(
427
+ eslint,
428
+ filePaths,
429
+ workerCount,
430
+ eslintOptionsOrURL,
431
+ warnOnLowNetLintingRatio,
432
+ ) {
433
+ const { configLoader, lintResultCache } = privateMembers.get(eslint);
434
+
435
+ const results = await runWorkers(
436
+ filePaths,
437
+ workerCount,
438
+ eslintOptionsOrURL,
439
+ warnOnLowNetLintingRatio,
372
440
  );
441
+ // Persist the cache to disk.
442
+ if (lintResultCache) {
443
+ results.forEach((result, index) => {
444
+ if (result) {
445
+ const filePath = filePaths[index];
446
+ const configs =
447
+ configLoader.getCachedConfigArrayForFile(filePath);
448
+ const config = configs.getConfig(filePath);
449
+
450
+ if (config) {
451
+ /*
452
+ * Store the lint result in the LintResultCache.
453
+ * NOTE: The LintResultCache will remove the file source and any
454
+ * other properties that are difficult to serialize, and will
455
+ * hydrate those properties back in on future lint runs.
456
+ */
457
+ lintResultCache.setCachedLintResults(
458
+ filePath,
459
+ config,
460
+ result,
461
+ );
462
+ }
463
+ }
464
+ });
465
+ }
466
+ return results;
373
467
  }
374
468
 
375
469
  /**
376
- * Creates a fixer function based on the provided fix, fixTypesSet, and config.
377
- * @param {Function|boolean} fix The original fix option.
378
- * @param {Set<string>} fixTypesSet A set of fix types to filter messages for fixing.
379
- * @param {FlatConfigArray} config The config for the file that generated the message.
380
- * @returns {Function|boolean} The fixer function or the original fix value.
470
+ * Lint files in single-thread mode.
471
+ * @param {ESLint} eslint ESLint instance.
472
+ * @param {string[]} filePaths File paths to lint.
473
+ * @returns {Promise<LintResult[]>} Lint results.
381
474
  */
382
- function getFixerForFixTypes(fix, fixTypesSet, config) {
383
- if (!fix || !fixTypesSet) {
384
- return fix;
385
- }
475
+ async function lintFilesWithoutMultithreading(eslint, filePaths) {
476
+ const {
477
+ configLoader,
478
+ linter,
479
+ lintResultCache,
480
+ options: eslintOptions,
481
+ } = privateMembers.get(eslint);
482
+
483
+ const controller = new AbortController();
484
+ const retrier = new Retrier(error => fileRetryCodes.has(error.code), {
485
+ concurrency: 100,
486
+ });
386
487
 
387
- const originalFix = typeof fix === "function" ? fix : () => true;
488
+ /*
489
+ * Because we need to process multiple files, including reading from disk,
490
+ * it is most efficient to start by reading each file via promises so that
491
+ * they can be done in parallel. Then, we can lint the returned text. This
492
+ * ensures we are waiting the minimum amount of time in between lints.
493
+ */
494
+ const results = await Promise.all(
495
+ filePaths.map(async filePath => {
496
+ const configs = configLoader.getCachedConfigArrayForFile(filePath);
497
+ const config = configs.getConfig(filePath);
498
+
499
+ const result = await lintFile(
500
+ filePath,
501
+ configs,
502
+ eslintOptions,
503
+ linter,
504
+ lintResultCache,
505
+ null,
506
+ retrier,
507
+ controller,
508
+ );
509
+
510
+ if (config) {
511
+ /*
512
+ * Store the lint result in the LintResultCache.
513
+ * NOTE: The LintResultCache will remove the file source and any
514
+ * other properties that are difficult to serialize, and will
515
+ * hydrate those properties back in on future lint runs.
516
+ */
517
+ lintResultCache?.setCachedLintResults(filePath, config, result);
518
+ }
388
519
 
389
- return message =>
390
- shouldMessageBeFixed(message, config, fixTypesSet) &&
391
- originalFix(message);
520
+ return result;
521
+ }),
522
+ );
523
+ return results;
392
524
  }
393
525
 
394
526
  /**
395
- * Retrieves flags from the environment variable ESLINT_FLAGS.
396
- * @param {string[]} flags The flags defined via the API.
397
- * @returns {string[]} The merged flags to use.
527
+ * Throws an error if the given options are not cloneable.
528
+ * @param {ESLintOptions} options The options to check.
529
+ * @returns {void}
530
+ * @throws {TypeError} If the options are not cloneable.
398
531
  */
399
- function mergeEnvironmentFlags(flags) {
400
- if (!process.env.ESLINT_FLAGS) {
401
- return flags;
532
+ function validateOptionCloneability(options) {
533
+ try {
534
+ structuredClone(options);
535
+ return;
536
+ } catch {
537
+ // continue
402
538
  }
403
-
404
- const envFlags = process.env.ESLINT_FLAGS.trim().split(/\s*,\s*/gu);
405
- return Array.from(new Set([...envFlags, ...flags]));
539
+ const uncloneableOptionKeys = Object.keys(options)
540
+ .filter(key => {
541
+ try {
542
+ structuredClone(options[key]);
543
+ } catch {
544
+ return true;
545
+ }
546
+ return false;
547
+ })
548
+ .sort();
549
+ const error = new TypeError(
550
+ `The ${uncloneableOptionKeys.length === 1 ? "option" : "options"} ${new Intl.ListFormat("en-US").format(uncloneableOptionKeys.map(key => `"${key}"`))} cannot be cloned. When concurrency is enabled, all options must be cloneable values (JSON values). Remove uncloneable options or use an options module.`,
551
+ );
552
+ error.code = "ESLINT_UNCLONEABLE_OPTIONS";
553
+ throw error;
406
554
  }
407
555
 
408
556
  //-----------------------------------------------------------------------------
@@ -421,51 +569,51 @@ class ESLint {
421
569
 
422
570
  /**
423
571
  * The loader to use for finding config files.
424
- * @type {ConfigLoader|LegacyConfigLoader}
572
+ * @type {ConfigLoader}
425
573
  */
426
574
  #configLoader;
427
575
 
576
+ /**
577
+ * The unprocessed options or the URL of the options module. Only set when concurrency is enabled.
578
+ * @type {ESLintOptions | string | undefined}
579
+ */
580
+ #optionsOrURL;
581
+
428
582
  /**
429
583
  * Creates a new instance of the main ESLint API.
430
584
  * @param {ESLintOptions} options The options for this instance.
431
585
  */
432
586
  constructor(options = {}) {
433
- const defaultConfigs = [];
434
587
  const processedOptions = processOptions(options);
588
+ if (
589
+ !options[disableCloneabilityCheck] &&
590
+ processedOptions.concurrency !== "off"
591
+ ) {
592
+ validateOptionCloneability(options);
593
+
594
+ // Save the unprocessed options in an instance field to pass to worker threads in `lintFiles()`.
595
+ this.#optionsOrURL = options;
596
+ }
435
597
  const warningService = new WarningService();
436
- const linter = new Linter({
437
- cwd: processedOptions.cwd,
438
- configType: "flat",
439
- flags: mergeEnvironmentFlags(processedOptions.flags),
440
- warningService,
441
- });
598
+ const linter = createLinter(processedOptions, warningService);
442
599
 
443
600
  const cacheFilePath = getCacheFile(
444
601
  processedOptions.cacheLocation,
445
602
  processedOptions.cwd,
446
603
  );
447
604
 
448
- const lintResultCache = processedOptions.cache
449
- ? new LintResultCache(cacheFilePath, processedOptions.cacheStrategy)
450
- : null;
451
-
452
- const configLoaderOptions = {
453
- cwd: processedOptions.cwd,
454
- baseConfig: processedOptions.baseConfig,
455
- overrideConfig: processedOptions.overrideConfig,
456
- configFile: processedOptions.configFile,
457
- ignoreEnabled: processedOptions.ignore,
458
- ignorePatterns: processedOptions.ignorePatterns,
605
+ const lintResultCache = createLintResultCache(
606
+ processedOptions,
607
+ cacheFilePath,
608
+ );
609
+ const defaultConfigs = createDefaultConfigs(options.plugins);
610
+
611
+ this.#configLoader = createConfigLoader(
612
+ processedOptions,
459
613
  defaultConfigs,
460
- hasUnstableNativeNodeJsTSConfigFlag: linter.hasFlag(
461
- "unstable_native_nodejs_ts_config",
462
- ),
614
+ linter,
463
615
  warningService,
464
- };
465
-
466
- this.#configLoader = linter.hasFlag("v10_config_lookup_from_file")
467
- ? new ConfigLoader(configLoaderOptions)
468
- : new LegacyConfigLoader(configLoaderOptions);
616
+ );
469
617
 
470
618
  debug(`Using config loader ${this.#configLoader.constructor.name}`);
471
619
 
@@ -477,26 +625,9 @@ class ESLint {
477
625
  defaultConfigs,
478
626
  configs: null,
479
627
  configLoader: this.#configLoader,
628
+ warningService,
480
629
  });
481
630
 
482
- /**
483
- * If additional plugins are passed in, add that to the default
484
- * configs for this instance.
485
- */
486
- if (options.plugins) {
487
- const plugins = {};
488
-
489
- for (const [pluginName, plugin] of Object.entries(
490
- options.plugins,
491
- )) {
492
- plugins[getShorthandName(pluginName, "eslint-plugin")] = plugin;
493
- }
494
-
495
- defaultConfigs.push({
496
- plugins,
497
- });
498
- }
499
-
500
631
  // Check for the .eslintignore file, and warn if it's present.
501
632
  if (existsSync(path.resolve(processedOptions.cwd, ".eslintignore"))) {
502
633
  warningService.emitESLintIgnoreWarning();
@@ -514,7 +645,7 @@ class ESLint {
514
645
  /**
515
646
  * The default configuration that ESLint uses internally. This is provided for tooling that wants to calculate configurations using the same defaults as ESLint.
516
647
  * Keep in mind that the default configuration may change from version to version, so you shouldn't rely on any particular keys or values to be present.
517
- * @type {ConfigArray}
648
+ * @type {FlatConfigArray}
518
649
  */
519
650
  static get defaultConfig() {
520
651
  return defaultConfig;
@@ -530,8 +661,7 @@ class ESLint {
530
661
  throw new Error("'results' must be an array");
531
662
  }
532
663
 
533
- const retryCodes = new Set(["ENFILE", "EMFILE"]);
534
- const retrier = new Retrier(error => retryCodes.has(error.code), {
664
+ const retrier = new Retrier(error => fileRetryCodes.has(error.code), {
535
665
  concurrency: 100,
536
666
  });
537
667
 
@@ -581,10 +711,31 @@ class ESLint {
581
711
  return filtered;
582
712
  }
583
713
 
714
+ /**
715
+ * Creates a new ESLint instance using options loaded from a module.
716
+ * @param {URL} optionsURL The URL of the options module.
717
+ * @returns {ESLint} The new ESLint instance.
718
+ */
719
+ static async fromOptionsModule(optionsURL) {
720
+ if (!(optionsURL instanceof URL)) {
721
+ throw new TypeError("Argument must be a URL object");
722
+ }
723
+ const optionsURLString = optionsURL.href;
724
+ const loadedOptions = await loadOptionsFromModule(optionsURLString);
725
+ const options = { ...loadedOptions, [disableCloneabilityCheck]: true };
726
+ const eslint = new ESLint(options);
727
+
728
+ if (options.concurrency !== "off") {
729
+ // Save the options module URL in an instance field to pass to worker threads in `lintFiles()`.
730
+ eslint.#optionsOrURL = optionsURLString;
731
+ }
732
+ return eslint;
733
+ }
734
+
584
735
  /**
585
736
  * Returns meta objects for each rule represented in the lint results.
586
737
  * @param {LintResult[]} results The results to fetch rules meta for.
587
- * @returns {Object} A mapping of ruleIds to rule meta objects.
738
+ * @returns {Record<string, RulesMeta>} A mapping of ruleIds to rule meta objects.
588
739
  * @throws {TypeError} When the results object wasn't created from this ESLint instance.
589
740
  * @throws {TypeError} When a plugin or rule is missing.
590
741
  */
@@ -626,8 +777,8 @@ class ESLint {
626
777
  try {
627
778
  configs =
628
779
  configLoader.getCachedConfigArrayForFile(filePath);
629
- } catch {
630
- throw createExtraneousResultsError();
780
+ } catch (err) {
781
+ throw createExtraneousResultsError(err);
631
782
  }
632
783
 
633
784
  const config = configs.getConfig(filePath);
@@ -667,8 +818,8 @@ class ESLint {
667
818
  const {
668
819
  cacheFilePath,
669
820
  lintResultCache,
670
- linter,
671
821
  options: eslintOptions,
822
+ warningService,
672
823
  } = privateMembers.get(this);
673
824
 
674
825
  /*
@@ -709,19 +860,12 @@ class ESLint {
709
860
  debug(`Using file patterns: ${normalizedPatterns}`);
710
861
 
711
862
  const {
712
- allowInlineConfig,
713
863
  cache,
864
+ concurrency,
714
865
  cwd,
715
- fix,
716
- fixTypes,
717
- ruleFilter,
718
- stats,
719
866
  globInputPaths,
720
867
  errorOnUnmatchedPattern,
721
- warnIgnored,
722
868
  } = eslintOptions;
723
- const startTime = Date.now();
724
- const fixTypesSet = fixTypes ? new Set(fixTypes) : null;
725
869
 
726
870
  // Delete cache file; should this be done here?
727
871
  if (!cache && cacheFilePath) {
@@ -738,6 +882,7 @@ class ESLint {
738
882
  }
739
883
  }
740
884
 
885
+ const startTime = Date.now();
741
886
  const filePaths = await findFiles({
742
887
  patterns: normalizedPatterns,
743
888
  cwd,
@@ -745,119 +890,45 @@ class ESLint {
745
890
  configLoader: this.#configLoader,
746
891
  errorOnUnmatchedPattern,
747
892
  });
748
- const controller = new AbortController();
749
- const retryCodes = new Set(["ENFILE", "EMFILE"]);
750
- const retrier = new Retrier(error => retryCodes.has(error.code), {
751
- concurrency: 100,
752
- });
753
-
754
893
  debug(
755
894
  `${filePaths.length} files found in: ${Date.now() - startTime}ms`,
756
895
  );
757
896
 
758
- /*
759
- * Because we need to process multiple files, including reading from disk,
760
- * it is most efficient to start by reading each file via promises so that
761
- * they can be done in parallel. Then, we can lint the returned text. This
762
- * ensures we are waiting the minimum amount of time in between lints.
763
- */
764
- const results = await Promise.all(
765
- filePaths.map(async filePath => {
766
- const configs =
767
- await this.#configLoader.loadConfigArrayForFile(filePath);
768
- const config = configs.getConfig(filePath);
769
-
770
- /*
771
- * If a filename was entered that cannot be matched
772
- * to a config, then notify the user.
773
- */
774
- if (!config) {
775
- if (warnIgnored) {
776
- const configStatus = configs.getConfigStatus(filePath);
777
-
778
- return createIgnoreResult(filePath, cwd, configStatus);
779
- }
780
-
781
- return void 0;
782
- }
783
-
784
- // Skip if there is cached result.
785
- if (lintResultCache) {
786
- const cachedResult = lintResultCache.getCachedLintResults(
787
- filePath,
788
- config,
789
- );
790
-
791
- if (cachedResult) {
792
- const hadMessages =
793
- cachedResult.messages &&
794
- cachedResult.messages.length > 0;
795
-
796
- if (hadMessages && fix) {
797
- debug(
798
- `Reprocessing cached file to allow autofix: ${filePath}`,
799
- );
800
- } else {
801
- debug(
802
- `Skipping file since it hasn't changed: ${filePath}`,
803
- );
804
- return cachedResult;
805
- }
806
- }
807
- }
897
+ /** @type {LintResult[]} */
898
+ let results;
808
899
 
809
- // set up fixer for fixTypes if necessary
810
- const fixer = getFixerForFixTypes(fix, fixTypesSet, config);
811
-
812
- return retrier
813
- .retry(
814
- () =>
815
- fs
816
- .readFile(filePath, {
817
- encoding: "utf8",
818
- signal: controller.signal,
819
- })
820
- .then(text => {
821
- // fail immediately if an error occurred in another file
822
- controller.signal.throwIfAborted();
823
-
824
- // do the linting
825
- const result = verifyText({
826
- text,
827
- filePath,
828
- configs,
829
- cwd,
830
- fix: fixer,
831
- allowInlineConfig,
832
- ruleFilter,
833
- stats,
834
- linter,
835
- });
836
-
837
- /*
838
- * Store the lint result in the LintResultCache.
839
- * NOTE: The LintResultCache will remove the file source and any
840
- * other properties that are difficult to serialize, and will
841
- * hydrate those properties back in on future lint runs.
842
- */
843
- if (lintResultCache) {
844
- lintResultCache.setCachedLintResults(
845
- filePath,
846
- config,
847
- result,
848
- );
849
- }
850
-
851
- return result;
852
- }),
853
- { signal: controller.signal },
854
- )
855
- .catch(error => {
856
- controller.abort(error);
857
- throw error;
858
- });
859
- }),
900
+ // The value of `module.exports.calculateWorkerCount` can be overridden in tests.
901
+ const workerCount = module.exports.calculateWorkerCount(
902
+ concurrency,
903
+ filePaths.length,
860
904
  );
905
+ if (workerCount) {
906
+ debug(`Linting using ${workerCount} worker thread(s).`);
907
+ let poorConcurrencyNotice;
908
+ if (workerCount <= 2) {
909
+ poorConcurrencyNotice = "disable concurrency";
910
+ } else {
911
+ if (concurrency === "auto") {
912
+ poorConcurrencyNotice =
913
+ "disable concurrency or use a numeric concurrency setting";
914
+ } else {
915
+ poorConcurrencyNotice = "reduce or disable concurrency";
916
+ }
917
+ }
918
+ results = await lintFilesWithMultithreading(
919
+ this,
920
+ filePaths,
921
+ workerCount,
922
+ this.#optionsOrURL,
923
+ () =>
924
+ warningService.emitPoorConcurrencyWarning(
925
+ poorConcurrencyNotice,
926
+ ),
927
+ );
928
+ } else {
929
+ debug(`Linting in single-thread mode.`);
930
+ results = await lintFilesWithoutMultithreading(this, filePaths);
931
+ }
861
932
 
862
933
  // Persist the cache to disk.
863
934
  if (lintResultCache) {
@@ -866,9 +937,7 @@ class ESLint {
866
937
 
867
938
  const finalResults = results.filter(result => !!result);
868
939
 
869
- return processLintReport(this, {
870
- results: finalResults,
871
- });
940
+ return processLintReport(this, finalResults);
872
941
  }
873
942
 
874
943
  /**
@@ -977,9 +1046,7 @@ class ESLint {
977
1046
 
978
1047
  debug(`Linting complete in: ${Date.now() - startTime}ms`);
979
1048
 
980
- return processLintReport(this, {
981
- results,
982
- });
1049
+ return processLintReport(this, results);
983
1050
  }
984
1051
 
985
1052
  /**
@@ -1089,7 +1156,7 @@ class ESLint {
1089
1156
  * This is the same logic used by the ESLint CLI executable to determine
1090
1157
  * configuration for each file it processes.
1091
1158
  * @param {string} filePath The path of the file to retrieve a config object for.
1092
- * @returns {Promise<Config|undefined>} A configuration object for the file
1159
+ * @returns {Promise<CalculatedConfig|undefined>} A configuration object for the file
1093
1160
  * or `undefined` if there is no configuration data for the object.
1094
1161
  */
1095
1162
  async calculateConfigForFile(filePath) {
@@ -1161,4 +1228,5 @@ module.exports = {
1161
1228
  ESLint,
1162
1229
  shouldUseFlatConfig,
1163
1230
  locateConfigFileToUse,
1231
+ calculateWorkerCount,
1164
1232
  };