eslint 9.28.0 → 9.29.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/conf/ecma-version.js +1 -1
- package/conf/globals.js +10 -0
- package/lib/cli.js +6 -11
- package/lib/eslint/eslint.js +13 -17
- package/lib/languages/js/source-code/source-code.js +74 -27
- package/lib/linter/apply-disable-directives.js +2 -4
- package/lib/linter/code-path-analysis/code-path-analyzer.js +8 -9
- package/lib/linter/linter.js +4 -4
- package/lib/linter/source-code-traverser.js +64 -49
- package/lib/linter/source-code-visitor.js +81 -0
- package/lib/rules/class-methods-use-this.js +7 -0
- package/lib/rules/no-promise-executor-return.js +4 -35
- package/lib/rules/no-restricted-globals.js +35 -2
- package/lib/rules/no-restricted-properties.js +24 -10
- package/lib/rules/no-setter-return.js +13 -48
- package/lib/rules/no-use-before-define.js +2 -0
- package/lib/rules/no-var.js +14 -2
- package/lib/rules/prefer-regex-literals.js +1 -18
- package/lib/services/suppressions-service.js +8 -0
- package/lib/shared/naming.js +109 -0
- package/lib/shared/relative-module-resolver.js +28 -0
- package/lib/types/index.d.ts +8 -2
- package/lib/types/rules.d.ts +5 -0
- package/package.json +7 -7
- package/lib/linter/safe-emitter.js +0 -52
package/README.md
CHANGED
@@ -329,7 +329,7 @@ to get your logo on our READMEs and [website](https://eslint.org/sponsors).
|
|
329
329
|
<p><a href="https://automattic.com"><img src="https://images.opencollective.com/automattic/d0ef3e1/logo.png" alt="Automattic" height="128"></a> <a href="https://www.airbnb.com/"><img src="https://images.opencollective.com/airbnb/d327d66/logo.png" alt="Airbnb" height="128"></a></p><h3>Gold Sponsors</h3>
|
330
330
|
<p><a href="https://qlty.sh/"><img src="https://images.opencollective.com/qltysh/33d157d/logo.png" alt="Qlty Software" height="96"></a> <a href="https://trunk.io/"><img src="https://images.opencollective.com/trunkio/fb92d60/avatar.png" alt="trunk.io" height="96"></a> <a href="https://shopify.engineering/"><img src="https://avatars.githubusercontent.com/u/8085" alt="Shopify" height="96"></a></p><h3>Silver Sponsors</h3>
|
331
331
|
<p><a href="https://vite.dev/"><img src="https://images.opencollective.com/vite/e6d15e1/logo.png" alt="Vite" height="64"></a> <a href="https://liftoff.io/"><img src="https://images.opencollective.com/liftoff/5c4fa84/logo.png" alt="Liftoff" height="64"></a> <a href="https://americanexpress.io"><img src="https://avatars.githubusercontent.com/u/3853301" alt="American Express" height="64"></a> <a href="https://stackblitz.com"><img src="https://avatars.githubusercontent.com/u/28635252" alt="StackBlitz" height="64"></a></p><h3>Bronze Sponsors</h3>
|
332
|
-
<p><a href="https://cybozu.co.jp/"><img src="https://images.opencollective.com/cybozu/933e46d/logo.png" alt="Cybozu" height="32"></a> <a href="https://www.crosswordsolver.org/anagram-solver/"><img src="https://images.opencollective.com/anagram-solver/2666271/logo.png" alt="Anagram Solver" height="32"></a> <a href="https://icons8.com/"><img src="https://images.opencollective.com/icons8/7fa1641/logo.png" alt="Icons8" height="32"></a> <a href="https://discord.com"><img src="https://images.opencollective.com/discordapp/f9645d9/logo.png" alt="Discord" height="32"></a> <a href="https://www.gitbook.com"><img src="https://avatars.githubusercontent.com/u/7111340" alt="GitBook" height="32"></a> <a href="https://nolebase.ayaka.io"><img src="https://avatars.githubusercontent.com/u/11081491" alt="Neko" height="32"></a> <a href="https://nx.dev"><img src="https://avatars.githubusercontent.com/u/23692104" alt="Nx" height="32"></a> <a href="https://opensource.mercedes-benz.com/"><img src="https://avatars.githubusercontent.com/u/34240465" alt="Mercedes-Benz Group" height="32"></a> <a href="https://herocoders.com"><img src="https://avatars.githubusercontent.com/u/37549774" alt="HeroCoders" height="32"></a> <a href="https://www.lambdatest.com"><img src="https://avatars.githubusercontent.com/u/171592363" alt="LambdaTest" height="32"></a></p>
|
332
|
+
<p><a href="https://sentry.io"><img src="https://github.com/getsentry.png" alt="Sentry" height="32"></a> <a href="https://syntax.fm"><img src="https://github.com/syntaxfm.png" alt="Syntax" height="32"></a> <a href="https://cybozu.co.jp/"><img src="https://images.opencollective.com/cybozu/933e46d/logo.png" alt="Cybozu" height="32"></a> <a href="https://www.crosswordsolver.org/anagram-solver/"><img src="https://images.opencollective.com/anagram-solver/2666271/logo.png" alt="Anagram Solver" height="32"></a> <a href="https://icons8.com/"><img src="https://images.opencollective.com/icons8/7fa1641/logo.png" alt="Icons8" height="32"></a> <a href="https://discord.com"><img src="https://images.opencollective.com/discordapp/f9645d9/logo.png" alt="Discord" height="32"></a> <a href="https://www.gitbook.com"><img src="https://avatars.githubusercontent.com/u/7111340" alt="GitBook" height="32"></a> <a href="https://nolebase.ayaka.io"><img src="https://avatars.githubusercontent.com/u/11081491" alt="Neko" height="32"></a> <a href="https://nx.dev"><img src="https://avatars.githubusercontent.com/u/23692104" alt="Nx" height="32"></a> <a href="https://opensource.mercedes-benz.com/"><img src="https://avatars.githubusercontent.com/u/34240465" alt="Mercedes-Benz Group" height="32"></a> <a href="https://herocoders.com"><img src="https://avatars.githubusercontent.com/u/37549774" alt="HeroCoders" height="32"></a> <a href="https://www.lambdatest.com"><img src="https://avatars.githubusercontent.com/u/171592363" alt="LambdaTest" height="32"></a></p>
|
333
333
|
<h3>Technology Sponsors</h3>
|
334
334
|
Technology sponsors allow us to use their products and services for free as part of a contribution to the open source ecosystem and our work.
|
335
335
|
<p><a href="https://netlify.com"><img src="https://raw.githubusercontent.com/eslint/eslint.org/main/src/assets/images/techsponsors/netlify-icon.svg" alt="Netlify" height="32"></a> <a href="https://algolia.com"><img src="https://raw.githubusercontent.com/eslint/eslint.org/main/src/assets/images/techsponsors/algolia-icon.svg" alt="Algolia" height="32"></a> <a href="https://1password.com"><img src="https://raw.githubusercontent.com/eslint/eslint.org/main/src/assets/images/techsponsors/1password-icon.svg" alt="1Password" height="32"></a></p>
|
package/conf/ecma-version.js
CHANGED
package/conf/globals.js
CHANGED
@@ -135,6 +135,15 @@ const es2024 = {
|
|
135
135
|
|
136
136
|
const es2025 = {
|
137
137
|
...es2024,
|
138
|
+
Float16Array: false,
|
139
|
+
Iterator: false,
|
140
|
+
};
|
141
|
+
|
142
|
+
const es2026 = {
|
143
|
+
...es2025,
|
144
|
+
AsyncDisposableStack: false,
|
145
|
+
DisposableStack: false,
|
146
|
+
SuppressedError: false,
|
138
147
|
};
|
139
148
|
|
140
149
|
//-----------------------------------------------------------------------------
|
@@ -156,4 +165,5 @@ module.exports = {
|
|
156
165
|
es2023,
|
157
166
|
es2024,
|
158
167
|
es2025,
|
168
|
+
es2026,
|
159
169
|
};
|
package/lib/cli.js
CHANGED
@@ -28,13 +28,14 @@ const fs = require("node:fs"),
|
|
28
28
|
log = require("./shared/logging"),
|
29
29
|
RuntimeInfo = require("./shared/runtime-info"),
|
30
30
|
{ normalizeSeverityToString } = require("./shared/severity");
|
31
|
-
const {
|
32
|
-
Legacy: { naming },
|
33
|
-
} = require("@eslint/eslintrc");
|
34
31
|
const { ModuleImporter } = require("@humanwhocodes/module-importer");
|
35
32
|
const { getCacheFile } = require("./eslint/eslint-helpers");
|
36
33
|
const { SuppressionsService } = require("./services/suppressions-service");
|
37
34
|
const debug = require("debug")("eslint:cli");
|
35
|
+
const {
|
36
|
+
normalizePackageName,
|
37
|
+
getShorthandName,
|
38
|
+
} = require("./shared/naming.js");
|
38
39
|
|
39
40
|
//------------------------------------------------------------------------------
|
40
41
|
// Types
|
@@ -67,10 +68,7 @@ async function loadPlugins(importer, pluginNames) {
|
|
67
68
|
|
68
69
|
await Promise.all(
|
69
70
|
pluginNames.map(async pluginName => {
|
70
|
-
const longName =
|
71
|
-
pluginName,
|
72
|
-
"eslint-plugin",
|
73
|
-
);
|
71
|
+
const longName = normalizePackageName(pluginName, "eslint-plugin");
|
74
72
|
const module = await importer.import(longName);
|
75
73
|
|
76
74
|
if (!("default" in module)) {
|
@@ -79,10 +77,7 @@ async function loadPlugins(importer, pluginNames) {
|
|
79
77
|
);
|
80
78
|
}
|
81
79
|
|
82
|
-
const shortName =
|
83
|
-
pluginName,
|
84
|
-
"eslint-plugin",
|
85
|
-
);
|
80
|
+
const shortName = getShorthandName(pluginName, "eslint-plugin");
|
86
81
|
|
87
82
|
plugins[shortName] = module.default;
|
88
83
|
}),
|
package/lib/eslint/eslint.js
CHANGED
@@ -15,13 +15,6 @@ const path = require("node:path");
|
|
15
15
|
const { version } = require("../../package.json");
|
16
16
|
const { Linter } = require("../linter");
|
17
17
|
const { defaultConfig } = require("../config/default-config");
|
18
|
-
const {
|
19
|
-
Legacy: {
|
20
|
-
ConfigOps: { getRuleSeverity },
|
21
|
-
ModuleResolver,
|
22
|
-
naming,
|
23
|
-
},
|
24
|
-
} = require("@eslint/eslintrc");
|
25
18
|
|
26
19
|
const {
|
27
20
|
findFiles,
|
@@ -41,6 +34,13 @@ const LintResultCache = require("../cli-engine/lint-result-cache");
|
|
41
34
|
const { Retrier } = require("@humanwhocodes/retry");
|
42
35
|
const { ConfigLoader, LegacyConfigLoader } = require("../config/config-loader");
|
43
36
|
const { WarningService } = require("../services/warning-service");
|
37
|
+
const { Config } = require("../config/config.js");
|
38
|
+
const {
|
39
|
+
getShorthandName,
|
40
|
+
getNamespaceFromTerm,
|
41
|
+
normalizePackageName,
|
42
|
+
} = require("../shared/naming.js");
|
43
|
+
const { resolve } = require("../shared/relative-module-resolver.js");
|
44
44
|
|
45
45
|
/*
|
46
46
|
* This is necessary to allow overwriting writeFile for testing purposes.
|
@@ -160,7 +160,7 @@ function getOrFindUsedDeprecatedRules(eslint, maybeFilePath) {
|
|
160
160
|
|
161
161
|
if (config.rules) {
|
162
162
|
for (const [ruleId, ruleConf] of Object.entries(config.rules)) {
|
163
|
-
if (
|
163
|
+
if (Config.getRuleNumericSeverity(ruleConf) === 0) {
|
164
164
|
continue;
|
165
165
|
}
|
166
166
|
const rule = config.getRuleDefinition(ruleId);
|
@@ -174,7 +174,7 @@ function getOrFindUsedDeprecatedRules(eslint, maybeFilePath) {
|
|
174
174
|
replacedBy: usesNewFormat
|
175
175
|
? (meta.deprecated.replacedBy?.map(
|
176
176
|
replacement =>
|
177
|
-
`${replacement.plugin?.name !== void 0 ? `${
|
177
|
+
`${replacement.plugin?.name !== void 0 ? `${getShorthandName(replacement.plugin.name, "eslint-plugin")}/` : ""}${replacement.rule?.name ?? ""}`,
|
178
178
|
) ?? [])
|
179
179
|
: meta.replacedBy || [],
|
180
180
|
info: usesNewFormat ? meta.deprecated : void 0,
|
@@ -489,8 +489,7 @@ class ESLint {
|
|
489
489
|
for (const [pluginName, plugin] of Object.entries(
|
490
490
|
options.plugins,
|
491
491
|
)) {
|
492
|
-
plugins[
|
493
|
-
plugin;
|
492
|
+
plugins[getShorthandName(pluginName, "eslint-plugin")] = plugin;
|
494
493
|
}
|
495
494
|
|
496
495
|
defaultConfigs.push({
|
@@ -998,7 +997,7 @@ class ESLint {
|
|
998
997
|
|
999
998
|
// replace \ with / for Windows compatibility
|
1000
999
|
const normalizedFormatName = name.replace(/\\/gu, "/");
|
1001
|
-
const namespace =
|
1000
|
+
const namespace = getNamespaceFromTerm(normalizedFormatName);
|
1002
1001
|
|
1003
1002
|
// grab our options
|
1004
1003
|
const { cwd } = privateMembers.get(this).options;
|
@@ -1010,16 +1009,13 @@ class ESLint {
|
|
1010
1009
|
formatterPath = path.resolve(cwd, normalizedFormatName);
|
1011
1010
|
} else {
|
1012
1011
|
try {
|
1013
|
-
const npmFormat =
|
1012
|
+
const npmFormat = normalizePackageName(
|
1014
1013
|
normalizedFormatName,
|
1015
1014
|
"eslint-formatter",
|
1016
1015
|
);
|
1017
1016
|
|
1018
1017
|
// TODO: This is pretty dirty...would be nice to clean up at some point.
|
1019
|
-
formatterPath =
|
1020
|
-
npmFormat,
|
1021
|
-
getPlaceholderPath(cwd),
|
1022
|
-
);
|
1018
|
+
formatterPath = resolve(npmFormat, getPlaceholderPath(cwd));
|
1023
1019
|
} catch {
|
1024
1020
|
formatterPath = path.resolve(
|
1025
1021
|
__dirname,
|
@@ -15,7 +15,6 @@ const { isCommentToken } = require("@eslint-community/eslint-utils"),
|
|
15
15
|
globals = require("../../../../conf/globals"),
|
16
16
|
{ directivesPattern } = require("../../../shared/directives"),
|
17
17
|
CodePathAnalyzer = require("../../../linter/code-path-analysis/code-path-analyzer"),
|
18
|
-
createEmitter = require("../../../linter/safe-emitter"),
|
19
18
|
{
|
20
19
|
ConfigCommentParser,
|
21
20
|
VisitNodeStep,
|
@@ -40,16 +39,6 @@ const { isCommentToken } = require("@eslint-community/eslint-utils"),
|
|
40
39
|
|
41
40
|
const commentParser = new ConfigCommentParser();
|
42
41
|
|
43
|
-
const CODE_PATH_EVENTS = [
|
44
|
-
"onCodePathStart",
|
45
|
-
"onCodePathEnd",
|
46
|
-
"onCodePathSegmentStart",
|
47
|
-
"onCodePathSegmentEnd",
|
48
|
-
"onCodePathSegmentLoop",
|
49
|
-
"onUnreachableCodePathSegmentStart",
|
50
|
-
"onUnreachableCodePathSegmentEnd",
|
51
|
-
];
|
52
|
-
|
53
42
|
/**
|
54
43
|
* Validates that the given AST has the required information.
|
55
44
|
* @param {ASTNode} ast The Program node of the AST to check.
|
@@ -240,6 +229,33 @@ function isSpaceBetween(sourceCode, first, second, checkInsideOfJSXText) {
|
|
240
229
|
return false;
|
241
230
|
}
|
242
231
|
|
232
|
+
/**
|
233
|
+
* Performs binary search to find the line number containing a given character index.
|
234
|
+
* Returns the lower bound - the index of the first element greater than the target.
|
235
|
+
* **Please note that the `lineStartIndices` should be sorted in ascending order**.
|
236
|
+
* - Time Complexity: O(log n) - Significantly faster than linear search for large files.
|
237
|
+
* @param {number[]} lineStartIndices Sorted array of line start indices.
|
238
|
+
* @param {number} target The character index to find the line number for.
|
239
|
+
* @returns {number} The 1-based line number for the target index.
|
240
|
+
* @private
|
241
|
+
*/
|
242
|
+
function findLineNumberBinarySearch(lineStartIndices, target) {
|
243
|
+
let low = 0;
|
244
|
+
let high = lineStartIndices.length;
|
245
|
+
|
246
|
+
while (low < high) {
|
247
|
+
const mid = ((low + high) / 2) | 0; // Use bitwise OR to floor the division
|
248
|
+
|
249
|
+
if (target < lineStartIndices[mid]) {
|
250
|
+
high = mid;
|
251
|
+
} else {
|
252
|
+
low = mid + 1;
|
253
|
+
}
|
254
|
+
}
|
255
|
+
|
256
|
+
return low;
|
257
|
+
}
|
258
|
+
|
243
259
|
//-----------------------------------------------------------------------------
|
244
260
|
// Directive Comments
|
245
261
|
//-----------------------------------------------------------------------------
|
@@ -422,6 +438,7 @@ class SourceCode extends TokenStore {
|
|
422
438
|
["scopes", new WeakMap()],
|
423
439
|
["vars", new Map()],
|
424
440
|
["configNodes", void 0],
|
441
|
+
["isGlobalReference", new WeakMap()],
|
425
442
|
]);
|
426
443
|
|
427
444
|
/**
|
@@ -720,9 +737,9 @@ class SourceCode extends TokenStore {
|
|
720
737
|
|
721
738
|
/**
|
722
739
|
* Converts a source text index into a (line, column) pair.
|
723
|
-
* @param {number} index The index of a character in a file
|
740
|
+
* @param {number} index The index of a character in a file.
|
724
741
|
* @throws {TypeError|RangeError} If non-numeric index or index out of range.
|
725
|
-
* @returns {{line: number, column: number}} A {line, column} location object with
|
742
|
+
* @returns {{line: number, column: number}} A {line, column} location object with 1-indexed line and 0-indexed column.
|
726
743
|
* @public
|
727
744
|
*/
|
728
745
|
getLocFromIndex(index) {
|
@@ -757,7 +774,7 @@ class SourceCode extends TokenStore {
|
|
757
774
|
const lineNumber =
|
758
775
|
index >= this.lineStartIndices.at(-1)
|
759
776
|
? this.lineStartIndices.length
|
760
|
-
: this.lineStartIndices
|
777
|
+
: findLineNumberBinarySearch(this.lineStartIndices, index);
|
761
778
|
|
762
779
|
return {
|
763
780
|
line: lineNumber,
|
@@ -902,6 +919,41 @@ class SourceCode extends TokenStore {
|
|
902
919
|
return ancestorsStartingAtParent.reverse();
|
903
920
|
}
|
904
921
|
|
922
|
+
/**
|
923
|
+
* Determines whether the given identifier node is a reference to a global variable.
|
924
|
+
* @param {ASTNode} node `Identifier` node to check.
|
925
|
+
* @returns {boolean} True if the identifier is a reference to a global variable.
|
926
|
+
*/
|
927
|
+
isGlobalReference(node) {
|
928
|
+
if (!node) {
|
929
|
+
throw new TypeError("Missing required argument: node.");
|
930
|
+
}
|
931
|
+
|
932
|
+
const cache = this[caches].get("isGlobalReference");
|
933
|
+
|
934
|
+
if (cache.has(node)) {
|
935
|
+
return cache.get(node);
|
936
|
+
}
|
937
|
+
|
938
|
+
if (node.type !== "Identifier") {
|
939
|
+
cache.set(node, false);
|
940
|
+
return false;
|
941
|
+
}
|
942
|
+
|
943
|
+
const variable = this.scopeManager.scopes[0].set.get(node.name);
|
944
|
+
|
945
|
+
if (!variable || variable.defs.length > 0) {
|
946
|
+
cache.set(node, false);
|
947
|
+
return false;
|
948
|
+
}
|
949
|
+
|
950
|
+
const result = variable.references.some(
|
951
|
+
({ identifier }) => identifier === node,
|
952
|
+
);
|
953
|
+
cache.set(node, result);
|
954
|
+
return result;
|
955
|
+
}
|
956
|
+
|
905
957
|
/**
|
906
958
|
* Returns the location of the given node or token.
|
907
959
|
* @param {ASTNode|Token} nodeOrToken The node or token to get the location of.
|
@@ -1239,7 +1291,6 @@ class SourceCode extends TokenStore {
|
|
1239
1291
|
* custom parsers to return any AST, we need to ensure that the traversal
|
1240
1292
|
* logic works for any AST.
|
1241
1293
|
*/
|
1242
|
-
const emitter = createEmitter();
|
1243
1294
|
let analyzer = {
|
1244
1295
|
enterNode(node) {
|
1245
1296
|
steps.push(
|
@@ -1259,7 +1310,14 @@ class SourceCode extends TokenStore {
|
|
1259
1310
|
}),
|
1260
1311
|
);
|
1261
1312
|
},
|
1262
|
-
|
1313
|
+
emit(eventName, args) {
|
1314
|
+
steps.push(
|
1315
|
+
new CallMethodStep({
|
1316
|
+
target: eventName,
|
1317
|
+
args,
|
1318
|
+
}),
|
1319
|
+
);
|
1320
|
+
},
|
1263
1321
|
};
|
1264
1322
|
|
1265
1323
|
/*
|
@@ -1273,17 +1331,6 @@ class SourceCode extends TokenStore {
|
|
1273
1331
|
*/
|
1274
1332
|
if (this.isESTree) {
|
1275
1333
|
analyzer = new CodePathAnalyzer(analyzer);
|
1276
|
-
|
1277
|
-
CODE_PATH_EVENTS.forEach(eventName => {
|
1278
|
-
emitter.on(eventName, (...args) => {
|
1279
|
-
steps.push(
|
1280
|
-
new CallMethodStep({
|
1281
|
-
target: eventName,
|
1282
|
-
args,
|
1283
|
-
}),
|
1284
|
-
);
|
1285
|
-
});
|
1286
|
-
});
|
1287
1334
|
}
|
1288
1335
|
|
1289
1336
|
/*
|
@@ -19,9 +19,7 @@
|
|
19
19
|
//------------------------------------------------------------------------------
|
20
20
|
|
21
21
|
const escapeRegExp = require("escape-string-regexp");
|
22
|
-
const {
|
23
|
-
Legacy: { ConfigOps },
|
24
|
-
} = require("@eslint/eslintrc/universal");
|
22
|
+
const { Config } = require("../config/config.js");
|
25
23
|
|
26
24
|
/**
|
27
25
|
* Compares the locations of two objects in a source file
|
@@ -539,7 +537,7 @@ module.exports = ({
|
|
539
537
|
configuredRules && ruleFilter
|
540
538
|
? new Set(
|
541
539
|
Object.keys(configuredRules).filter(ruleId => {
|
542
|
-
const severity =
|
540
|
+
const severity = Config.getRuleNumericSeverity(
|
543
541
|
configuredRules[ruleId],
|
544
542
|
);
|
545
543
|
|
@@ -196,7 +196,7 @@ function forwardCurrentToHead(analyzer, node) {
|
|
196
196
|
|
197
197
|
debug.dump(`${eventName} ${currentSegment.id}`);
|
198
198
|
|
199
|
-
analyzer.
|
199
|
+
analyzer.emit(eventName, [currentSegment, node]);
|
200
200
|
}
|
201
201
|
}
|
202
202
|
|
@@ -215,7 +215,7 @@ function forwardCurrentToHead(analyzer, node) {
|
|
215
215
|
|
216
216
|
debug.dump(`${eventName} ${headSegment.id}`);
|
217
217
|
CodePathSegment.markUsed(headSegment);
|
218
|
-
analyzer.
|
218
|
+
analyzer.emit(eventName, [headSegment, node]);
|
219
219
|
}
|
220
220
|
}
|
221
221
|
}
|
@@ -239,7 +239,7 @@ function leaveFromCurrentSegment(analyzer, node) {
|
|
239
239
|
|
240
240
|
debug.dump(`${eventName} ${currentSegment.id}`);
|
241
241
|
|
242
|
-
analyzer.
|
242
|
+
analyzer.emit(eventName, [currentSegment, node]);
|
243
243
|
}
|
244
244
|
|
245
245
|
state.currentSegments = [];
|
@@ -416,7 +416,7 @@ function processCodePathToEnter(analyzer, node) {
|
|
416
416
|
|
417
417
|
// Emits onCodePathStart events.
|
418
418
|
debug.dump(`onCodePathStart ${codePath.id}`);
|
419
|
-
analyzer.
|
419
|
+
analyzer.emit("onCodePathStart", [codePath, node]);
|
420
420
|
}
|
421
421
|
|
422
422
|
/*
|
@@ -681,7 +681,7 @@ function postprocess(analyzer, node) {
|
|
681
681
|
|
682
682
|
// Emits onCodePathEnd event of this code path.
|
683
683
|
debug.dump(`onCodePathEnd ${codePath.id}`);
|
684
|
-
analyzer.
|
684
|
+
analyzer.emit("onCodePathEnd", [codePath, node]);
|
685
685
|
debug.dumpDot(codePath);
|
686
686
|
|
687
687
|
codePath = analyzer.codePath = analyzer.codePath.upper;
|
@@ -747,7 +747,7 @@ class CodePathAnalyzer {
|
|
747
747
|
*/
|
748
748
|
constructor(eventGenerator) {
|
749
749
|
this.original = eventGenerator;
|
750
|
-
this.
|
750
|
+
this.emit = eventGenerator.emit;
|
751
751
|
this.codePath = null;
|
752
752
|
this.idGenerator = new IdGenerator("s");
|
753
753
|
this.currentNode = null;
|
@@ -816,12 +816,11 @@ class CodePathAnalyzer {
|
|
816
816
|
debug.dump(
|
817
817
|
`onCodePathSegmentLoop ${fromSegment.id} -> ${toSegment.id}`,
|
818
818
|
);
|
819
|
-
this.
|
820
|
-
"onCodePathSegmentLoop",
|
819
|
+
this.emit("onCodePathSegmentLoop", [
|
821
820
|
fromSegment,
|
822
821
|
toSegment,
|
823
822
|
this.currentNode,
|
824
|
-
);
|
823
|
+
]);
|
825
824
|
}
|
826
825
|
}
|
827
826
|
}
|
package/lib/linter/linter.js
CHANGED
@@ -29,8 +29,8 @@ const path = require("node:path"),
|
|
29
29
|
{ ConfigCommentParser } = require("@eslint/plugin-kit"),
|
30
30
|
createReportTranslator = require("./report-translator"),
|
31
31
|
Rules = require("./rules"),
|
32
|
-
createEmitter = require("./safe-emitter"),
|
33
32
|
SourceCodeFixer = require("./source-code-fixer"),
|
33
|
+
{ SourceCodeVisitor } = require("./source-code-visitor"),
|
34
34
|
timing = require("./timing"),
|
35
35
|
ruleReplacements = require("../../conf/replacements.json");
|
36
36
|
const { FlatConfigArray } = require("../config/flat-config-array");
|
@@ -1175,7 +1175,7 @@ function runRules(
|
|
1175
1175
|
stats,
|
1176
1176
|
slots,
|
1177
1177
|
) {
|
1178
|
-
const
|
1178
|
+
const visitor = new SourceCodeVisitor();
|
1179
1179
|
|
1180
1180
|
/*
|
1181
1181
|
* Create a frozen object with the ruleContext properties and methods that are shared by all rules.
|
@@ -1339,13 +1339,13 @@ function runRules(
|
|
1339
1339
|
? timing.time(ruleId, ruleListeners[selector], stats)
|
1340
1340
|
: ruleListeners[selector];
|
1341
1341
|
|
1342
|
-
|
1342
|
+
visitor.add(selector, addRuleErrorHandler(ruleListener));
|
1343
1343
|
});
|
1344
1344
|
});
|
1345
1345
|
|
1346
1346
|
const traverser = SourceCodeTraverser.getInstance(language);
|
1347
1347
|
|
1348
|
-
traverser.traverseSync(sourceCode,
|
1348
|
+
traverser.traverseSync(sourceCode, visitor, { steps });
|
1349
1349
|
|
1350
1350
|
return lintingProblems;
|
1351
1351
|
}
|
@@ -19,6 +19,7 @@ const vk = require("eslint-visitor-keys");
|
|
19
19
|
/**
|
20
20
|
* @import { ESQueryParsedSelector } from "./esquery.js";
|
21
21
|
* @import { Language, SourceCode } from "@eslint/core";
|
22
|
+
* @import { SourceCodeVisitor } from "./source-code-visitor.js";
|
22
23
|
*/
|
23
24
|
|
24
25
|
//-----------------------------------------------------------------------------
|
@@ -43,19 +44,17 @@ function compareSpecificity(a, b) {
|
|
43
44
|
*/
|
44
45
|
class ESQueryHelper {
|
45
46
|
/**
|
46
|
-
*
|
47
|
-
*
|
48
|
-
* have registered listeners for all of the events that it needs to listen for.
|
49
|
-
* (See lib/linter/safe-emitter.js for more details on `SafeEmitter`.)
|
47
|
+
* Creates a new instance.
|
48
|
+
* @param {SourceCodeVisitor} visitor The visitor containing the functions to call.
|
50
49
|
* @param {ESQueryOptions} esqueryOptions `esquery` options for traversing custom nodes.
|
51
50
|
* @returns {NodeEventGenerator} new instance
|
52
51
|
*/
|
53
|
-
constructor(
|
52
|
+
constructor(visitor, esqueryOptions) {
|
54
53
|
/**
|
55
54
|
* The emitter to use during traversal.
|
56
|
-
* @type {
|
55
|
+
* @type {SourceCodeVisitor}
|
57
56
|
*/
|
58
|
-
this.
|
57
|
+
this.visitor = visitor;
|
59
58
|
|
60
59
|
/**
|
61
60
|
* The options for `esquery` to use during matching.
|
@@ -91,7 +90,7 @@ class ESQueryHelper {
|
|
91
90
|
*/
|
92
91
|
this.anyTypeExitSelectors = [];
|
93
92
|
|
94
|
-
|
93
|
+
visitor.forEachName(rawSelector => {
|
95
94
|
const selector = parse(rawSelector);
|
96
95
|
|
97
96
|
/*
|
@@ -137,27 +136,26 @@ class ESQueryHelper {
|
|
137
136
|
}
|
138
137
|
|
139
138
|
/**
|
140
|
-
* Checks
|
139
|
+
* Checks if a node matches a given selector.
|
141
140
|
* @param {ASTNode} node The node to check
|
142
141
|
* @param {ASTNode[]} ancestry The ancestry of the node being checked.
|
143
142
|
* @param {ESQueryParsedSelector} selector An AST selector descriptor
|
144
|
-
* @returns {
|
143
|
+
* @returns {boolean} `true` if the selector matches the node, `false` otherwise
|
145
144
|
*/
|
146
|
-
|
147
|
-
|
148
|
-
this.emitter.emit(selector.source, node);
|
149
|
-
}
|
145
|
+
matches(node, ancestry, selector) {
|
146
|
+
return matches(node, selector.root, ancestry, this.esqueryOptions);
|
150
147
|
}
|
151
148
|
|
152
149
|
/**
|
153
|
-
*
|
150
|
+
* Calculates all appropriate selectors to a node, in specificity order
|
154
151
|
* @param {ASTNode} node The node to check
|
155
152
|
* @param {ASTNode[]} ancestry The ancestry of the node being checked.
|
156
153
|
* @param {boolean} isExit `false` if the node is currently being entered, `true` if it's currently being exited
|
157
|
-
* @returns {
|
154
|
+
* @returns {string[]} An array of selectors that match the node.
|
158
155
|
*/
|
159
|
-
|
156
|
+
calculateSelectors(node, ancestry, isExit) {
|
160
157
|
const nodeTypeKey = this.esqueryOptions?.nodeTypeKey || "type";
|
158
|
+
const selectors = [];
|
161
159
|
|
162
160
|
/*
|
163
161
|
* Get the selectors that may match this node. First, check
|
@@ -189,27 +187,36 @@ class ESQueryHelper {
|
|
189
187
|
* or if the next any type selector is more specific than the
|
190
188
|
* next selector for this node type, apply the any type selector.
|
191
189
|
*/
|
192
|
-
|
193
|
-
selectorsByNodeTypeIndex
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
190
|
+
const hasMoreNodeTypeSelectors =
|
191
|
+
selectorsByNodeTypeIndex < selectorsByNodeType.length;
|
192
|
+
const hasMoreAnyTypeSelectors =
|
193
|
+
anyTypeSelectorsIndex < anyTypeSelectors.length;
|
194
|
+
const anyTypeSelector = anyTypeSelectors[anyTypeSelectorsIndex];
|
195
|
+
const nodeTypeSelector =
|
196
|
+
selectorsByNodeType[selectorsByNodeTypeIndex];
|
197
|
+
|
198
|
+
// Only compare specificity if both selectors exist
|
199
|
+
const isAnyTypeSelectorLessSpecific =
|
200
|
+
hasMoreAnyTypeSelectors &&
|
201
|
+
hasMoreNodeTypeSelectors &&
|
202
|
+
anyTypeSelector.compare(nodeTypeSelector) < 0;
|
203
|
+
|
204
|
+
if (!hasMoreNodeTypeSelectors || isAnyTypeSelectorLessSpecific) {
|
205
|
+
anyTypeSelectorsIndex++;
|
206
|
+
|
207
|
+
if (this.matches(node, ancestry, anyTypeSelector)) {
|
208
|
+
selectors.push(anyTypeSelector.source);
|
209
|
+
}
|
204
210
|
} else {
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
);
|
211
|
+
selectorsByNodeTypeIndex++;
|
212
|
+
|
213
|
+
if (this.matches(node, ancestry, nodeTypeSelector)) {
|
214
|
+
selectors.push(nodeTypeSelector.source);
|
215
|
+
}
|
211
216
|
}
|
212
217
|
}
|
218
|
+
|
219
|
+
return selectors;
|
213
220
|
}
|
214
221
|
}
|
215
222
|
|
@@ -253,14 +260,14 @@ class SourceCodeTraverser {
|
|
253
260
|
/**
|
254
261
|
* Traverses the given source code synchronously.
|
255
262
|
* @param {SourceCode} sourceCode The source code to traverse.
|
256
|
-
* @param {
|
263
|
+
* @param {SourceCodeVisitor} visitor The emitter to use for events.
|
257
264
|
* @param {Object} options Options for traversal.
|
258
265
|
* @param {ReturnType<SourceCode["traverse"]>} options.steps The steps to take during traversal.
|
259
266
|
* @returns {void}
|
260
267
|
* @throws {Error} If an error occurs during traversal.
|
261
268
|
*/
|
262
|
-
traverseSync(sourceCode,
|
263
|
-
const esquery = new ESQueryHelper(
|
269
|
+
traverseSync(sourceCode, visitor, { steps } = {}) {
|
270
|
+
const esquery = new ESQueryHelper(visitor, {
|
264
271
|
visitorKeys: sourceCode.visitorKeys ?? this.#language.visitorKeys,
|
265
272
|
fallback: vk.getKeys,
|
266
273
|
matchClass: this.#language.matchesSelectorClass ?? (() => false),
|
@@ -274,19 +281,27 @@ class SourceCodeTraverser {
|
|
274
281
|
case STEP_KIND_VISIT: {
|
275
282
|
try {
|
276
283
|
if (step.phase === 1) {
|
277
|
-
esquery
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
284
|
+
esquery
|
285
|
+
.calculateSelectors(
|
286
|
+
step.target,
|
287
|
+
currentAncestry,
|
288
|
+
false,
|
289
|
+
)
|
290
|
+
.forEach(selector => {
|
291
|
+
visitor.callSync(selector, step.target);
|
292
|
+
});
|
282
293
|
currentAncestry.unshift(step.target);
|
283
294
|
} else {
|
284
295
|
currentAncestry.shift();
|
285
|
-
esquery
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
296
|
+
esquery
|
297
|
+
.calculateSelectors(
|
298
|
+
step.target,
|
299
|
+
currentAncestry,
|
300
|
+
true,
|
301
|
+
)
|
302
|
+
.forEach(selector => {
|
303
|
+
visitor.callSync(selector, step.target);
|
304
|
+
});
|
290
305
|
}
|
291
306
|
} catch (err) {
|
292
307
|
err.currentNode = step.target;
|
@@ -296,7 +311,7 @@ class SourceCodeTraverser {
|
|
296
311
|
}
|
297
312
|
|
298
313
|
case STEP_KIND_CALL: {
|
299
|
-
|
314
|
+
visitor.callSync(step.target, ...step.args);
|
300
315
|
break;
|
301
316
|
}
|
302
317
|
|