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.
- package/README.md +1 -1
- package/lib/cli-engine/file-enumerator.js +1 -1
- package/lib/cli.js +62 -266
- package/lib/config/flat-config-schema.js +1 -1
- package/lib/eslint/eslint-helpers.js +426 -6
- package/lib/eslint/eslint.js +381 -313
- package/lib/eslint/worker.js +164 -0
- package/lib/languages/js/source-code/source-code.js +3 -4
- package/lib/linter/esquery.js +3 -0
- package/lib/linter/interpolate.js +1 -1
- package/lib/linter/linter.js +3 -4
- package/lib/options.js +23 -9
- package/lib/rule-tester/rule-tester.js +3 -0
- package/lib/rules/array-callback-return.js +0 -1
- package/lib/rules/dot-notation.js +1 -1
- package/lib/rules/grouped-accessor-pairs.js +8 -7
- package/lib/rules/indent-legacy.js +1 -1
- package/lib/rules/index.js +1 -0
- package/lib/rules/no-alert.js +1 -1
- package/lib/rules/no-empty-function.js +20 -1
- package/lib/rules/no-empty-static-block.js +25 -1
- package/lib/rules/no-empty.js +37 -0
- package/lib/rules/no-eval.js +3 -1
- package/lib/rules/no-irregular-whitespace.js +2 -2
- package/lib/rules/no-loss-of-precision.js +26 -3
- package/lib/rules/no-mixed-spaces-and-tabs.js +1 -0
- package/lib/rules/no-octal.js +1 -4
- package/lib/rules/no-trailing-spaces.js +2 -1
- package/lib/rules/no-useless-escape.js +1 -1
- package/lib/rules/prefer-regex-literals.js +1 -1
- package/lib/rules/preserve-caught-error.js +509 -0
- package/lib/rules/strict.js +2 -1
- package/lib/rules/utils/ast-utils.js +1 -1
- package/lib/rules/utils/char-source.js +1 -1
- package/lib/rules/yoda.js +2 -2
- package/lib/services/suppressions-service.js +3 -0
- package/lib/services/warning-service.js +13 -0
- package/lib/shared/naming.js +1 -1
- package/lib/shared/translate-cli-options.js +281 -0
- package/lib/types/index.d.ts +7 -0
- package/lib/types/rules.d.ts +22 -14
- package/package.json +3 -3
package/lib/eslint/eslint.js
CHANGED
@@ -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
|
-
|
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
|
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 {
|
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 {
|
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 {
|
195
|
+
* @param {LintResult[]} results The linting results to process.
|
197
196
|
* @returns {LintResult[]} The processed linting results.
|
198
197
|
*/
|
199
|
-
function processLintReport(eslint,
|
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
|
-
*
|
267
|
-
* @param {
|
268
|
-
* @
|
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
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
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
|
-
|
328
|
-
|
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
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
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
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
};
|
343
|
-
}
|
314
|
+
// Used internally. Do not expose.
|
315
|
+
const disableCloneabilityCheck = Symbol(
|
316
|
+
"Do not check for uncloneable options.",
|
317
|
+
);
|
344
318
|
|
345
|
-
|
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
|
-
*
|
350
|
-
* @param {
|
351
|
-
* @param {
|
352
|
-
* @param {string
|
353
|
-
* @
|
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
|
356
|
-
|
357
|
-
|
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
|
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
|
414
|
+
return results;
|
363
415
|
}
|
364
416
|
|
365
417
|
/**
|
366
|
-
*
|
367
|
-
* @
|
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
|
370
|
-
|
371
|
-
|
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
|
-
*
|
377
|
-
* @param {
|
378
|
-
* @param {
|
379
|
-
* @
|
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
|
383
|
-
|
384
|
-
|
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
|
-
|
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
|
-
|
390
|
-
|
391
|
-
|
520
|
+
return result;
|
521
|
+
}),
|
522
|
+
);
|
523
|
+
return results;
|
392
524
|
}
|
393
525
|
|
394
526
|
/**
|
395
|
-
*
|
396
|
-
* @param {
|
397
|
-
* @returns {
|
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
|
400
|
-
|
401
|
-
|
532
|
+
function validateOptionCloneability(options) {
|
533
|
+
try {
|
534
|
+
structuredClone(options);
|
535
|
+
return;
|
536
|
+
} catch {
|
537
|
+
// continue
|
402
538
|
}
|
403
|
-
|
404
|
-
|
405
|
-
|
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
|
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 =
|
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 =
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
const
|
453
|
-
|
454
|
-
|
455
|
-
|
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
|
-
|
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 {
|
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
|
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 {
|
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
|
-
|
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
|
-
|
810
|
-
|
811
|
-
|
812
|
-
|
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<
|
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
|
};
|