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.
- 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 +376 -312
- package/lib/eslint/worker.js +164 -0
- package/lib/languages/js/source-code/source-code.js +3 -4
- package/lib/linter/interpolate.js +1 -1
- package/lib/linter/linter.js +3 -4
- package/lib/options.js +23 -9
- 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/no-alert.js +1 -1
- package/lib/rules/no-empty-function.js +1 -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/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/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 +5 -0
- package/lib/types/rules.d.ts +1 -1
- package/package.json +2 -2
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,291 @@ async function locateConfigFileToUse({ configFile, cwd }) {
|
|
263
262
|
}
|
264
263
|
|
265
264
|
/**
|
266
|
-
*
|
267
|
-
* @
|
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
|
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;
|
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
|
-
|
328
|
-
|
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
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
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
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
};
|
343
|
-
}
|
310
|
+
// Used internally. Do not expose.
|
311
|
+
const disableCloneabilityCheck = Symbol(
|
312
|
+
"Do not check for uncloneable options.",
|
313
|
+
);
|
344
314
|
|
345
|
-
|
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
|
-
*
|
350
|
-
* @param {
|
351
|
-
* @param {
|
352
|
-
* @param {string
|
353
|
-
* @
|
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
|
356
|
-
|
357
|
-
|
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
|
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
|
-
|
406
|
+
if (worstNetLintingRatio < LOW_NET_LINTING_RATIO) {
|
407
|
+
warnOnLowNetLintingRatio();
|
408
|
+
}
|
409
|
+
|
410
|
+
return results;
|
363
411
|
}
|
364
412
|
|
365
413
|
/**
|
366
|
-
*
|
367
|
-
* @
|
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
|
370
|
-
|
371
|
-
|
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
|
-
*
|
377
|
-
* @param {
|
378
|
-
* @param {
|
379
|
-
* @
|
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
|
383
|
-
|
384
|
-
|
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
|
-
|
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
|
-
|
390
|
-
|
391
|
-
|
516
|
+
return result;
|
517
|
+
}),
|
518
|
+
);
|
519
|
+
return results;
|
392
520
|
}
|
393
521
|
|
394
522
|
/**
|
395
|
-
*
|
396
|
-
* @param {
|
397
|
-
* @returns {
|
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
|
400
|
-
|
401
|
-
|
528
|
+
function validateOptionCloneability(options) {
|
529
|
+
try {
|
530
|
+
structuredClone(options);
|
531
|
+
return;
|
532
|
+
} catch {
|
533
|
+
// continue
|
402
534
|
}
|
403
|
-
|
404
|
-
|
405
|
-
|
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
|
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 =
|
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 =
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
const
|
453
|
-
|
454
|
-
|
455
|
-
|
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
|
-
|
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 {
|
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
|
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 {
|
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
|
-
|
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
|
-
|
779
|
-
|
780
|
-
|
781
|
-
|
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<
|
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
|
};
|