eslint 9.33.0 → 9.34.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.
@@ -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,291 @@ 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
+ * @returns {TypeError} An error object.
279
267
  */
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;
316
- },
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
- };
268
+ function createExtraneousResultsError() {
269
+ return new TypeError(
270
+ "Results object was not created from this ESLint instance.",
271
+ );
272
+ }
326
273
 
327
- if (fixed) {
328
- result.output = output;
329
- }
274
+ /**
275
+ * Maximum number of files assumed to be best handled by one worker thread.
276
+ * This value is a heuristic estimation that can be adjusted if required.
277
+ */
278
+ const AUTO_FILES_PER_WORKER = 35;
330
279
 
331
- if (
332
- result.errorCount + result.warningCount > 0 &&
333
- typeof result.output === "undefined"
334
- ) {
335
- result.source = text;
280
+ /**
281
+ * Calculates the number of workers to run based on the concurrency setting and the number of files to lint.
282
+ * @param {number | "auto" | "off"} concurrency The normalized concurrency setting.
283
+ * @param {number} fileCount The number of files to be linted.
284
+ * @param {{ availableParallelism: () => number }} [os] Node.js `os` module, or a mock for testing.
285
+ * @returns {number} The effective number of worker threads to be started. A value of zero disables multithread linting.
286
+ */
287
+ function calculateWorkerCount(
288
+ concurrency,
289
+ fileCount,
290
+ { availableParallelism } = os,
291
+ ) {
292
+ let workerCount;
293
+ switch (concurrency) {
294
+ case "off":
295
+ return 0;
296
+ case "auto": {
297
+ workerCount = Math.min(
298
+ availableParallelism() >> 1,
299
+ Math.ceil(fileCount / AUTO_FILES_PER_WORKER),
300
+ );
301
+ break;
302
+ }
303
+ default:
304
+ workerCount = Math.min(concurrency, fileCount);
305
+ break;
336
306
  }
307
+ return workerCount > 1 ? workerCount : 0;
308
+ }
337
309
 
338
- if (stats) {
339
- result.stats = {
340
- times: linter.getTimes(),
341
- fixPasses: linter.getFixPassCount(),
342
- };
343
- }
310
+ // Used internally. Do not expose.
311
+ const disableCloneabilityCheck = Symbol(
312
+ "Do not check for uncloneable options.",
313
+ );
344
314
 
345
- return result;
346
- }
315
+ /**
316
+ * The smallest net linting ratio that doesn't trigger a poor concurrency warning.
317
+ * The net linting ratio is defined as the net linting duration divided by the thread's total runtime,
318
+ * where the net linting duration is the total linting time minus the time spent on I/O-intensive operations:
319
+ * **Net Linting Ratio** = (**Linting Time** – **I/O Time**) / **Thread Runtime**.
320
+ * - **Linting Time**: Total time spent linting files
321
+ * - **I/O Time**: Portion of linting time spent loading configs and reading files
322
+ * - **Thread Runtime**: End-to-end execution time of the thread
323
+ *
324
+ * This value is a heuristic estimation that can be adjusted if required.
325
+ */
326
+ const LOW_NET_LINTING_RATIO = 0.7;
347
327
 
348
328
  /**
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.
329
+ * Runs worker threads to lint files.
330
+ * @param {string[]} filePaths File paths to lint.
331
+ * @param {number} workerCount The number of worker threads to run.
332
+ * @param {ESLintOptions | string} eslintOptionsOrURL The unprocessed ESLint options or the URL of the options module.
333
+ * @param {() => void} warnOnLowNetLintingRatio A function to call if the net linting ratio is low.
334
+ * @returns {Promise<LintResult[]>} Lint results.
354
335
  */
355
- function shouldMessageBeFixed(message, config, fixTypes) {
356
- if (!message.ruleId) {
357
- return fixTypes.has("directive");
336
+ async function runWorkers(
337
+ filePaths,
338
+ workerCount,
339
+ eslintOptionsOrURL,
340
+ warnOnLowNetLintingRatio,
341
+ ) {
342
+ const fileCount = filePaths.length;
343
+ const results = Array(fileCount);
344
+ const workerURL = pathToFileURL(path.join(__dirname, "./worker.js"));
345
+ const filePathIndexArray = new Uint32Array(
346
+ new SharedArrayBuffer(Uint32Array.BYTES_PER_ELEMENT),
347
+ );
348
+ const abortController = new AbortController();
349
+ const abortSignal = abortController.signal;
350
+ const workerOptions = {
351
+ env: SHARE_ENV,
352
+ workerData: {
353
+ eslintOptionsOrURL,
354
+ filePathIndexArray,
355
+ filePaths,
356
+ },
357
+ };
358
+
359
+ const hrtimeBigint = process.hrtime.bigint;
360
+ let worstNetLintingRatio = 1;
361
+
362
+ /**
363
+ * A promise executor function that starts a worker thread on each invocation.
364
+ * @param {() => void} resolve_ Called when the worker thread terminates successfully.
365
+ * @param {(error: Error) => void} reject Called when the worker thread terminates with an error.
366
+ * @returns {void}
367
+ */
368
+ function workerExecutor(resolve_, reject) {
369
+ const workerStartTime = hrtimeBigint();
370
+ const worker = new Worker(workerURL, workerOptions);
371
+ worker.once(
372
+ "message",
373
+ (/** @type {WorkerLintResults} */ indexedResults) => {
374
+ const workerDuration = hrtimeBigint() - workerStartTime;
375
+
376
+ // 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.
377
+ const netLintingRatio =
378
+ Number(indexedResults.netLintingDuration) /
379
+ Number(workerDuration);
380
+
381
+ worstNetLintingRatio = Math.min(
382
+ worstNetLintingRatio,
383
+ netLintingRatio,
384
+ );
385
+ for (const result of indexedResults) {
386
+ const { index } = result;
387
+ delete result.index;
388
+ results[index] = result;
389
+ }
390
+ resolve_();
391
+ },
392
+ );
393
+ worker.once("error", error => {
394
+ abortController.abort(error);
395
+ reject(error);
396
+ });
397
+ abortSignal.addEventListener("abort", () => worker.terminate());
358
398
  }
359
399
 
360
- const rule = message.ruleId && config.getRuleDefinition(message.ruleId);
400
+ const promises = Array(workerCount);
401
+ for (let index = 0; index < workerCount; ++index) {
402
+ promises[index] = new Promise(workerExecutor);
403
+ }
404
+ await Promise.all(promises);
361
405
 
362
- return Boolean(rule && rule.meta && fixTypes.has(rule.meta.type));
406
+ if (worstNetLintingRatio < LOW_NET_LINTING_RATIO) {
407
+ warnOnLowNetLintingRatio();
408
+ }
409
+
410
+ return results;
363
411
  }
364
412
 
365
413
  /**
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.
414
+ * Lint files in multithread mode.
415
+ * @param {ESLint} eslint ESLint instance.
416
+ * @param {string[]} filePaths File paths to lint.
417
+ * @param {number} workerCount The number of worker threads to run.
418
+ * @param {ESLintOptions | string} eslintOptionsOrURL The unprocessed ESLint options or the URL of the options module.
419
+ * @param {() => void} warnOnLowNetLintingRatio A function to call if the net linting ratio is low.
420
+ * @returns {Promise<LintResult[]>} Lint results.
368
421
  */
369
- function createExtraneousResultsError() {
370
- return new TypeError(
371
- "Results object was not created from this ESLint instance.",
422
+ async function lintFilesWithMultithreading(
423
+ eslint,
424
+ filePaths,
425
+ workerCount,
426
+ eslintOptionsOrURL,
427
+ warnOnLowNetLintingRatio,
428
+ ) {
429
+ const { configLoader, lintResultCache } = privateMembers.get(eslint);
430
+
431
+ const results = await runWorkers(
432
+ filePaths,
433
+ workerCount,
434
+ eslintOptionsOrURL,
435
+ warnOnLowNetLintingRatio,
372
436
  );
437
+ // Persist the cache to disk.
438
+ if (lintResultCache) {
439
+ results.forEach((result, index) => {
440
+ if (result) {
441
+ const filePath = filePaths[index];
442
+ const configs =
443
+ configLoader.getCachedConfigArrayForFile(filePath);
444
+ const config = configs.getConfig(filePath);
445
+
446
+ if (config) {
447
+ /*
448
+ * Store the lint result in the LintResultCache.
449
+ * NOTE: The LintResultCache will remove the file source and any
450
+ * other properties that are difficult to serialize, and will
451
+ * hydrate those properties back in on future lint runs.
452
+ */
453
+ lintResultCache.setCachedLintResults(
454
+ filePath,
455
+ config,
456
+ result,
457
+ );
458
+ }
459
+ }
460
+ });
461
+ }
462
+ return results;
373
463
  }
374
464
 
375
465
  /**
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.
466
+ * Lint files in single-thread mode.
467
+ * @param {ESLint} eslint ESLint instance.
468
+ * @param {string[]} filePaths File paths to lint.
469
+ * @returns {Promise<LintResult[]>} Lint results.
381
470
  */
382
- function getFixerForFixTypes(fix, fixTypesSet, config) {
383
- if (!fix || !fixTypesSet) {
384
- return fix;
385
- }
471
+ async function lintFilesWithoutMultithreading(eslint, filePaths) {
472
+ const {
473
+ configLoader,
474
+ linter,
475
+ lintResultCache,
476
+ options: eslintOptions,
477
+ } = privateMembers.get(eslint);
478
+
479
+ const controller = new AbortController();
480
+ const retrier = new Retrier(error => fileRetryCodes.has(error.code), {
481
+ concurrency: 100,
482
+ });
483
+
484
+ /*
485
+ * Because we need to process multiple files, including reading from disk,
486
+ * it is most efficient to start by reading each file via promises so that
487
+ * they can be done in parallel. Then, we can lint the returned text. This
488
+ * ensures we are waiting the minimum amount of time in between lints.
489
+ */
490
+ const results = await Promise.all(
491
+ filePaths.map(async filePath => {
492
+ const configs = configLoader.getCachedConfigArrayForFile(filePath);
493
+ const config = configs.getConfig(filePath);
494
+
495
+ const result = await lintFile(
496
+ filePath,
497
+ configs,
498
+ eslintOptions,
499
+ linter,
500
+ lintResultCache,
501
+ null,
502
+ retrier,
503
+ controller,
504
+ );
386
505
 
387
- const originalFix = typeof fix === "function" ? fix : () => true;
506
+ if (config) {
507
+ /*
508
+ * Store the lint result in the LintResultCache.
509
+ * NOTE: The LintResultCache will remove the file source and any
510
+ * other properties that are difficult to serialize, and will
511
+ * hydrate those properties back in on future lint runs.
512
+ */
513
+ lintResultCache?.setCachedLintResults(filePath, config, result);
514
+ }
388
515
 
389
- return message =>
390
- shouldMessageBeFixed(message, config, fixTypesSet) &&
391
- originalFix(message);
516
+ return result;
517
+ }),
518
+ );
519
+ return results;
392
520
  }
393
521
 
394
522
  /**
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.
523
+ * Throws an error if the given options are not cloneable.
524
+ * @param {ESLintOptions} options The options to check.
525
+ * @returns {void}
526
+ * @throws {TypeError} If the options are not cloneable.
398
527
  */
399
- function mergeEnvironmentFlags(flags) {
400
- if (!process.env.ESLINT_FLAGS) {
401
- return flags;
528
+ function validateOptionCloneability(options) {
529
+ try {
530
+ structuredClone(options);
531
+ return;
532
+ } catch {
533
+ // continue
402
534
  }
403
-
404
- const envFlags = process.env.ESLINT_FLAGS.trim().split(/\s*,\s*/gu);
405
- return Array.from(new Set([...envFlags, ...flags]));
535
+ const uncloneableOptionKeys = Object.keys(options)
536
+ .filter(key => {
537
+ try {
538
+ structuredClone(options[key]);
539
+ } catch {
540
+ return true;
541
+ }
542
+ return false;
543
+ })
544
+ .sort();
545
+ const error = new TypeError(
546
+ `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. Remove uncloneable options or use an options module.`,
547
+ );
548
+ error.code = "ESLINT_UNCLONEABLE_OPTIONS";
549
+ throw error;
406
550
  }
407
551
 
408
552
  //-----------------------------------------------------------------------------
@@ -421,51 +565,51 @@ class ESLint {
421
565
 
422
566
  /**
423
567
  * The loader to use for finding config files.
424
- * @type {ConfigLoader|LegacyConfigLoader}
568
+ * @type {ConfigLoader}
425
569
  */
426
570
  #configLoader;
427
571
 
572
+ /**
573
+ * The unprocessed options or the URL of the options module. Only set when concurrency is enabled.
574
+ * @type {ESLintOptions | string | undefined}
575
+ */
576
+ #optionsOrURL;
577
+
428
578
  /**
429
579
  * Creates a new instance of the main ESLint API.
430
580
  * @param {ESLintOptions} options The options for this instance.
431
581
  */
432
582
  constructor(options = {}) {
433
- const defaultConfigs = [];
434
583
  const processedOptions = processOptions(options);
584
+ if (
585
+ !options[disableCloneabilityCheck] &&
586
+ processedOptions.concurrency !== "off"
587
+ ) {
588
+ validateOptionCloneability(options);
589
+
590
+ // Save the unprocessed options in an instance field to pass to worker threads in `lintFiles()`.
591
+ this.#optionsOrURL = options;
592
+ }
435
593
  const warningService = new WarningService();
436
- const linter = new Linter({
437
- cwd: processedOptions.cwd,
438
- configType: "flat",
439
- flags: mergeEnvironmentFlags(processedOptions.flags),
440
- warningService,
441
- });
594
+ const linter = createLinter(processedOptions, warningService);
442
595
 
443
596
  const cacheFilePath = getCacheFile(
444
597
  processedOptions.cacheLocation,
445
598
  processedOptions.cwd,
446
599
  );
447
600
 
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,
601
+ const lintResultCache = createLintResultCache(
602
+ processedOptions,
603
+ cacheFilePath,
604
+ );
605
+ const defaultConfigs = createDefaultConfigs(options.plugins);
606
+
607
+ this.#configLoader = createConfigLoader(
608
+ processedOptions,
459
609
  defaultConfigs,
460
- hasUnstableNativeNodeJsTSConfigFlag: linter.hasFlag(
461
- "unstable_native_nodejs_ts_config",
462
- ),
610
+ linter,
463
611
  warningService,
464
- };
465
-
466
- this.#configLoader = linter.hasFlag("v10_config_lookup_from_file")
467
- ? new ConfigLoader(configLoaderOptions)
468
- : new LegacyConfigLoader(configLoaderOptions);
612
+ );
469
613
 
470
614
  debug(`Using config loader ${this.#configLoader.constructor.name}`);
471
615
 
@@ -477,26 +621,9 @@ class ESLint {
477
621
  defaultConfigs,
478
622
  configs: null,
479
623
  configLoader: this.#configLoader,
624
+ warningService,
480
625
  });
481
626
 
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
627
  // Check for the .eslintignore file, and warn if it's present.
501
628
  if (existsSync(path.resolve(processedOptions.cwd, ".eslintignore"))) {
502
629
  warningService.emitESLintIgnoreWarning();
@@ -514,7 +641,7 @@ class ESLint {
514
641
  /**
515
642
  * The default configuration that ESLint uses internally. This is provided for tooling that wants to calculate configurations using the same defaults as ESLint.
516
643
  * 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}
644
+ * @type {FlatConfigArray}
518
645
  */
519
646
  static get defaultConfig() {
520
647
  return defaultConfig;
@@ -530,8 +657,7 @@ class ESLint {
530
657
  throw new Error("'results' must be an array");
531
658
  }
532
659
 
533
- const retryCodes = new Set(["ENFILE", "EMFILE"]);
534
- const retrier = new Retrier(error => retryCodes.has(error.code), {
660
+ const retrier = new Retrier(error => fileRetryCodes.has(error.code), {
535
661
  concurrency: 100,
536
662
  });
537
663
 
@@ -581,10 +707,31 @@ class ESLint {
581
707
  return filtered;
582
708
  }
583
709
 
710
+ /**
711
+ * Creates a new ESLint instance using options loaded from a module.
712
+ * @param {URL} optionsURL The URL of the options module.
713
+ * @returns {ESLint} The new ESLint instance.
714
+ */
715
+ static async fromOptionsModule(optionsURL) {
716
+ if (!(optionsURL instanceof URL)) {
717
+ throw new TypeError("Argument must be a URL object");
718
+ }
719
+ const optionsURLString = optionsURL.href;
720
+ const loadedOptions = await loadOptionsFromModule(optionsURLString);
721
+ const options = { ...loadedOptions, [disableCloneabilityCheck]: true };
722
+ const eslint = new ESLint(options);
723
+
724
+ if (options.concurrency !== "off") {
725
+ // Save the options module URL in an instance field to pass to worker threads in `lintFiles()`.
726
+ eslint.#optionsOrURL = optionsURLString;
727
+ }
728
+ return eslint;
729
+ }
730
+
584
731
  /**
585
732
  * Returns meta objects for each rule represented in the lint results.
586
733
  * @param {LintResult[]} results The results to fetch rules meta for.
587
- * @returns {Object} A mapping of ruleIds to rule meta objects.
734
+ * @returns {Record<string, RulesMeta>} A mapping of ruleIds to rule meta objects.
588
735
  * @throws {TypeError} When the results object wasn't created from this ESLint instance.
589
736
  * @throws {TypeError} When a plugin or rule is missing.
590
737
  */
@@ -667,8 +814,8 @@ class ESLint {
667
814
  const {
668
815
  cacheFilePath,
669
816
  lintResultCache,
670
- linter,
671
817
  options: eslintOptions,
818
+ warningService,
672
819
  } = privateMembers.get(this);
673
820
 
674
821
  /*
@@ -709,19 +856,12 @@ class ESLint {
709
856
  debug(`Using file patterns: ${normalizedPatterns}`);
710
857
 
711
858
  const {
712
- allowInlineConfig,
713
859
  cache,
860
+ concurrency,
714
861
  cwd,
715
- fix,
716
- fixTypes,
717
- ruleFilter,
718
- stats,
719
862
  globInputPaths,
720
863
  errorOnUnmatchedPattern,
721
- warnIgnored,
722
864
  } = eslintOptions;
723
- const startTime = Date.now();
724
- const fixTypesSet = fixTypes ? new Set(fixTypes) : null;
725
865
 
726
866
  // Delete cache file; should this be done here?
727
867
  if (!cache && cacheFilePath) {
@@ -738,6 +878,7 @@ class ESLint {
738
878
  }
739
879
  }
740
880
 
881
+ const startTime = Date.now();
741
882
  const filePaths = await findFiles({
742
883
  patterns: normalizedPatterns,
743
884
  cwd,
@@ -745,119 +886,45 @@ class ESLint {
745
886
  configLoader: this.#configLoader,
746
887
  errorOnUnmatchedPattern,
747
888
  });
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
889
  debug(
755
890
  `${filePaths.length} files found in: ${Date.now() - startTime}ms`,
756
891
  );
757
892
 
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);
893
+ /** @type {LintResult[]} */
894
+ let results;
777
895
 
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
- }
808
-
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
- }),
896
+ // The value of `module.exports.calculateWorkerCount` can be overridden in tests.
897
+ const workerCount = module.exports.calculateWorkerCount(
898
+ concurrency,
899
+ filePaths.length,
860
900
  );
901
+ if (workerCount) {
902
+ debug(`Linting using ${workerCount} worker thread(s).`);
903
+ let poorConcurrencyNotice;
904
+ if (workerCount <= 2) {
905
+ poorConcurrencyNotice = "disable concurrency";
906
+ } else {
907
+ if (concurrency === "auto") {
908
+ poorConcurrencyNotice =
909
+ "disable concurrency or use a numeric concurrency setting";
910
+ } else {
911
+ poorConcurrencyNotice = "reduce or disable concurrency";
912
+ }
913
+ }
914
+ results = await lintFilesWithMultithreading(
915
+ this,
916
+ filePaths,
917
+ workerCount,
918
+ this.#optionsOrURL,
919
+ () =>
920
+ warningService.emitPoorConcurrencyWarning(
921
+ poorConcurrencyNotice,
922
+ ),
923
+ );
924
+ } else {
925
+ debug(`Linting in single-thread mode.`);
926
+ results = await lintFilesWithoutMultithreading(this, filePaths);
927
+ }
861
928
 
862
929
  // Persist the cache to disk.
863
930
  if (lintResultCache) {
@@ -866,9 +933,7 @@ class ESLint {
866
933
 
867
934
  const finalResults = results.filter(result => !!result);
868
935
 
869
- return processLintReport(this, {
870
- results: finalResults,
871
- });
936
+ return processLintReport(this, finalResults);
872
937
  }
873
938
 
874
939
  /**
@@ -977,9 +1042,7 @@ class ESLint {
977
1042
 
978
1043
  debug(`Linting complete in: ${Date.now() - startTime}ms`);
979
1044
 
980
- return processLintReport(this, {
981
- results,
982
- });
1045
+ return processLintReport(this, results);
983
1046
  }
984
1047
 
985
1048
  /**
@@ -1089,7 +1152,7 @@ class ESLint {
1089
1152
  * This is the same logic used by the ESLint CLI executable to determine
1090
1153
  * configuration for each file it processes.
1091
1154
  * @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
1155
+ * @returns {Promise<CalculatedConfig|undefined>} A configuration object for the file
1093
1156
  * or `undefined` if there is no configuration data for the object.
1094
1157
  */
1095
1158
  async calculateConfigForFile(filePath) {
@@ -1161,4 +1224,5 @@ module.exports = {
1161
1224
  ESLint,
1162
1225
  shouldUseFlatConfig,
1163
1226
  locateConfigFileToUse,
1227
+ calculateWorkerCount,
1164
1228
  };