eslint 9.27.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 +20 -23
- package/lib/config/config-loader.js +32 -21
- package/lib/config/config.js +34 -11
- package/lib/eslint/eslint.js +18 -21
- package/lib/languages/js/source-code/source-code.js +104 -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 +30 -61
- package/lib/linter/source-code-traverser.js +327 -0
- package/lib/linter/source-code-visitor.js +81 -0
- package/lib/options.js +7 -0
- package/lib/rules/class-methods-use-this.js +7 -0
- package/lib/rules/func-style.js +57 -7
- package/lib/rules/no-implicit-globals.js +31 -15
- package/lib/rules/no-magic-numbers.js +98 -5
- 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-shadow.js +262 -6
- package/lib/rules/no-unassigned-vars.js +14 -6
- package/lib/rules/no-use-before-define.js +99 -1
- package/lib/rules/no-var.js +14 -2
- package/lib/rules/prefer-arrow-callback.js +9 -0
- package/lib/rules/prefer-regex-literals.js +1 -18
- package/lib/services/suppressions-service.js +8 -0
- package/lib/services/warning-service.js +85 -0
- package/lib/shared/naming.js +109 -0
- package/lib/shared/relative-module-resolver.js +28 -0
- package/lib/types/index.d.ts +18 -7
- package/lib/types/rules.d.ts +52 -2
- package/package.json +12 -10
- package/lib/linter/node-event-generator.js +0 -256
- package/lib/linter/safe-emitter.js +0 -52
@@ -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
@@ -27,11 +27,10 @@ const path = require("node:path"),
|
|
27
27
|
{ SourceCode } = require("../languages/js/source-code"),
|
28
28
|
applyDisableDirectives = require("./apply-disable-directives"),
|
29
29
|
{ ConfigCommentParser } = require("@eslint/plugin-kit"),
|
30
|
-
NodeEventGenerator = require("./node-event-generator"),
|
31
30
|
createReportTranslator = require("./report-translator"),
|
32
31
|
Rules = require("./rules"),
|
33
|
-
createEmitter = require("./safe-emitter"),
|
34
32
|
SourceCodeFixer = require("./source-code-fixer"),
|
33
|
+
{ SourceCodeVisitor } = require("./source-code-visitor"),
|
35
34
|
timing = require("./timing"),
|
36
35
|
ruleReplacements = require("../../conf/replacements.json");
|
37
36
|
const { FlatConfigArray } = require("../config/flat-config-array");
|
@@ -65,8 +64,8 @@ const { FileContext } = require("./file-context");
|
|
65
64
|
const { ProcessorService } = require("../services/processor-service");
|
66
65
|
const { containsDifferentProperty } = require("../shared/option-utils");
|
67
66
|
const { Config } = require("../config/config");
|
68
|
-
const
|
69
|
-
const
|
67
|
+
const { WarningService } = require("../services/warning-service");
|
68
|
+
const { SourceCodeTraverser } = require("./source-code-traverser");
|
70
69
|
|
71
70
|
//------------------------------------------------------------------------------
|
72
71
|
// Typedefs
|
@@ -113,6 +112,7 @@ const STEP_KIND_CALL = 2;
|
|
113
112
|
* @property {Map<string, Parser>} parserMap The loaded parsers.
|
114
113
|
* @property {{ passes: TimePass[]; }} times The times spent on applying a rule to a file (see `stats` option).
|
115
114
|
* @property {Rules} ruleMap The loaded rules.
|
115
|
+
* @property {WarningService} warningService The warning service.
|
116
116
|
*/
|
117
117
|
|
118
118
|
/**
|
@@ -1175,10 +1175,7 @@ function runRules(
|
|
1175
1175
|
stats,
|
1176
1176
|
slots,
|
1177
1177
|
) {
|
1178
|
-
const
|
1179
|
-
|
1180
|
-
// must happen first to assign all node.parent properties
|
1181
|
-
const eventQueue = sourceCode.traverse();
|
1178
|
+
const visitor = new SourceCodeVisitor();
|
1182
1179
|
|
1183
1180
|
/*
|
1184
1181
|
* Create a frozen object with the ruleContext properties and methods that are shared by all rules.
|
@@ -1199,6 +1196,7 @@ function runRules(
|
|
1199
1196
|
});
|
1200
1197
|
|
1201
1198
|
const lintingProblems = [];
|
1199
|
+
const steps = sourceCode.traverse();
|
1202
1200
|
|
1203
1201
|
Object.keys(configuredRules).forEach(ruleId => {
|
1204
1202
|
const severity = Config.getRuleNumericSeverity(configuredRules[ruleId]);
|
@@ -1341,44 +1339,13 @@ function runRules(
|
|
1341
1339
|
? timing.time(ruleId, ruleListeners[selector], stats)
|
1342
1340
|
: ruleListeners[selector];
|
1343
1341
|
|
1344
|
-
|
1342
|
+
visitor.add(selector, addRuleErrorHandler(ruleListener));
|
1345
1343
|
});
|
1346
1344
|
});
|
1347
1345
|
|
1348
|
-
const
|
1349
|
-
visitorKeys: sourceCode.visitorKeys ?? language.visitorKeys,
|
1350
|
-
fallback: Traverser.getKeys,
|
1351
|
-
matchClass: language.matchesSelectorClass ?? (() => false),
|
1352
|
-
nodeTypeKey: language.nodeTypeKey,
|
1353
|
-
});
|
1354
|
-
|
1355
|
-
for (const step of eventQueue) {
|
1356
|
-
switch (step.kind) {
|
1357
|
-
case STEP_KIND_VISIT: {
|
1358
|
-
try {
|
1359
|
-
if (step.phase === 1) {
|
1360
|
-
eventGenerator.enterNode(step.target);
|
1361
|
-
} else {
|
1362
|
-
eventGenerator.leaveNode(step.target);
|
1363
|
-
}
|
1364
|
-
} catch (err) {
|
1365
|
-
err.currentNode = step.target;
|
1366
|
-
throw err;
|
1367
|
-
}
|
1368
|
-
break;
|
1369
|
-
}
|
1370
|
-
|
1371
|
-
case STEP_KIND_CALL: {
|
1372
|
-
emitter.emit(step.target, ...step.args);
|
1373
|
-
break;
|
1374
|
-
}
|
1346
|
+
const traverser = SourceCodeTraverser.getInstance(language);
|
1375
1347
|
|
1376
|
-
|
1377
|
-
throw new Error(
|
1378
|
-
`Invalid traversal step found: "${step.type}".`,
|
1379
|
-
);
|
1380
|
-
}
|
1381
|
-
}
|
1348
|
+
traverser.traverseSync(sourceCode, visitor, { steps });
|
1382
1349
|
|
1383
1350
|
return lintingProblems;
|
1384
1351
|
}
|
@@ -1483,8 +1450,14 @@ class Linter {
|
|
1483
1450
|
* @param {string} [config.cwd] path to a directory that should be considered as the current working directory, can be undefined.
|
1484
1451
|
* @param {Array<string>} [config.flags] the feature flags to enable.
|
1485
1452
|
* @param {"flat"|"eslintrc"} [config.configType="flat"] the type of config used.
|
1453
|
+
* @param {WarningService} [config.warningService] The warning service to use.
|
1486
1454
|
*/
|
1487
|
-
constructor({
|
1455
|
+
constructor({
|
1456
|
+
cwd,
|
1457
|
+
configType = "flat",
|
1458
|
+
flags = [],
|
1459
|
+
warningService = new WarningService(),
|
1460
|
+
} = {}) {
|
1488
1461
|
const processedFlags = [];
|
1489
1462
|
|
1490
1463
|
flags.forEach(flag => {
|
@@ -1492,11 +1465,10 @@ class Linter {
|
|
1492
1465
|
const inactiveFlagData = inactiveFlags.get(flag);
|
1493
1466
|
const inactivityReason =
|
1494
1467
|
getInactivityReasonMessage(inactiveFlagData);
|
1468
|
+
const message = `The flag '${flag}' is inactive: ${inactivityReason}`;
|
1495
1469
|
|
1496
1470
|
if (typeof inactiveFlagData.replacedBy === "undefined") {
|
1497
|
-
throw new Error(
|
1498
|
-
`The flag '${flag}' is inactive: ${inactivityReason}`,
|
1499
|
-
);
|
1471
|
+
throw new Error(message);
|
1500
1472
|
}
|
1501
1473
|
|
1502
1474
|
// if there's a replacement, enable it instead of original
|
@@ -1504,10 +1476,7 @@ class Linter {
|
|
1504
1476
|
processedFlags.push(inactiveFlagData.replacedBy);
|
1505
1477
|
}
|
1506
1478
|
|
1507
|
-
|
1508
|
-
`The flag '${flag}' is inactive: ${inactivityReason}`,
|
1509
|
-
`ESLintInactiveFlag_${flag}`,
|
1510
|
-
);
|
1479
|
+
warningService.emitInactiveFlagWarning(flag, message);
|
1511
1480
|
|
1512
1481
|
return;
|
1513
1482
|
}
|
@@ -1528,6 +1497,7 @@ class Linter {
|
|
1528
1497
|
configType, // TODO: Remove after flat config conversion
|
1529
1498
|
parserMap: new Map([["espree", espree]]),
|
1530
1499
|
ruleMap: new Rules(),
|
1500
|
+
warningService,
|
1531
1501
|
});
|
1532
1502
|
|
1533
1503
|
this.version = pkg.version;
|
@@ -2710,6 +2680,14 @@ class Linter {
|
|
2710
2680
|
options && typeof options.fix !== "undefined" ? options.fix : true;
|
2711
2681
|
const stats = options?.stats;
|
2712
2682
|
|
2683
|
+
const slots = internalSlotsMap.get(this);
|
2684
|
+
|
2685
|
+
// Remove lint times from the last run.
|
2686
|
+
if (stats) {
|
2687
|
+
delete slots.times;
|
2688
|
+
slots.fixPasses = 0;
|
2689
|
+
}
|
2690
|
+
|
2713
2691
|
/**
|
2714
2692
|
* This loop continues until one of the following is true:
|
2715
2693
|
*
|
@@ -2719,14 +2697,6 @@ class Linter {
|
|
2719
2697
|
* That means anytime a fix is successfully applied, there will be another pass.
|
2720
2698
|
* Essentially, guaranteeing a minimum of two passes.
|
2721
2699
|
*/
|
2722
|
-
const slots = internalSlotsMap.get(this);
|
2723
|
-
|
2724
|
-
// Remove lint times from the last run.
|
2725
|
-
if (stats) {
|
2726
|
-
delete slots.times;
|
2727
|
-
slots.fixPasses = 0;
|
2728
|
-
}
|
2729
|
-
|
2730
2700
|
do {
|
2731
2701
|
passNumber++;
|
2732
2702
|
let tTotal;
|
@@ -2798,9 +2768,8 @@ class Linter {
|
|
2798
2768
|
debug(
|
2799
2769
|
`Circular fixes detected after pass ${passNumber}. Exiting fix loop.`,
|
2800
2770
|
);
|
2801
|
-
|
2802
|
-
|
2803
|
-
"ESLintCircularFixesWarning",
|
2771
|
+
slots.warningService.emitCircularFixesWarning(
|
2772
|
+
options?.filename ?? "text",
|
2804
2773
|
);
|
2805
2774
|
break;
|
2806
2775
|
}
|
@@ -0,0 +1,327 @@
|
|
1
|
+
/**
|
2
|
+
* @fileoverview Traverser for SourceCode objects.
|
3
|
+
* @author Nicholas C. Zakas
|
4
|
+
*/
|
5
|
+
|
6
|
+
"use strict";
|
7
|
+
|
8
|
+
//------------------------------------------------------------------------------
|
9
|
+
// Requirements
|
10
|
+
//------------------------------------------------------------------------------
|
11
|
+
|
12
|
+
const { parse, matches } = require("./esquery");
|
13
|
+
const vk = require("eslint-visitor-keys");
|
14
|
+
|
15
|
+
//-----------------------------------------------------------------------------
|
16
|
+
// Typedefs
|
17
|
+
//-----------------------------------------------------------------------------
|
18
|
+
|
19
|
+
/**
|
20
|
+
* @import { ESQueryParsedSelector } from "./esquery.js";
|
21
|
+
* @import { Language, SourceCode } from "@eslint/core";
|
22
|
+
* @import { SourceCodeVisitor } from "./source-code-visitor.js";
|
23
|
+
*/
|
24
|
+
|
25
|
+
//-----------------------------------------------------------------------------
|
26
|
+
// Helpers
|
27
|
+
//-----------------------------------------------------------------------------
|
28
|
+
|
29
|
+
const STEP_KIND_VISIT = 1;
|
30
|
+
const STEP_KIND_CALL = 2;
|
31
|
+
|
32
|
+
/**
|
33
|
+
* Compares two ESQuery selectors by specificity.
|
34
|
+
* @param {ESQueryParsedSelector} a The first selector to compare.
|
35
|
+
* @param {ESQueryParsedSelector} b The second selector to compare.
|
36
|
+
* @returns {number} A negative number if `a` is less specific than `b` or they are equally specific and `a` <= `b` alphabetically, a positive number if `a` is more specific than `b`.
|
37
|
+
*/
|
38
|
+
function compareSpecificity(a, b) {
|
39
|
+
return a.compare(b);
|
40
|
+
}
|
41
|
+
|
42
|
+
/**
|
43
|
+
* Helper to wrap ESQuery operations.
|
44
|
+
*/
|
45
|
+
class ESQueryHelper {
|
46
|
+
/**
|
47
|
+
* Creates a new instance.
|
48
|
+
* @param {SourceCodeVisitor} visitor The visitor containing the functions to call.
|
49
|
+
* @param {ESQueryOptions} esqueryOptions `esquery` options for traversing custom nodes.
|
50
|
+
* @returns {NodeEventGenerator} new instance
|
51
|
+
*/
|
52
|
+
constructor(visitor, esqueryOptions) {
|
53
|
+
/**
|
54
|
+
* The emitter to use during traversal.
|
55
|
+
* @type {SourceCodeVisitor}
|
56
|
+
*/
|
57
|
+
this.visitor = visitor;
|
58
|
+
|
59
|
+
/**
|
60
|
+
* The options for `esquery` to use during matching.
|
61
|
+
* @type {ESQueryOptions}
|
62
|
+
*/
|
63
|
+
this.esqueryOptions = esqueryOptions;
|
64
|
+
|
65
|
+
/**
|
66
|
+
* A map of node type to selectors targeting that node type on the
|
67
|
+
* enter phase of traversal.
|
68
|
+
* @type {Map<string, ESQueryParsedSelector[]>}
|
69
|
+
*/
|
70
|
+
this.enterSelectorsByNodeType = new Map();
|
71
|
+
|
72
|
+
/**
|
73
|
+
* A map of node type to selectors targeting that node type on the
|
74
|
+
* exit phase of traversal.
|
75
|
+
* @type {Map<string, ESQueryParsedSelector[]>}
|
76
|
+
*/
|
77
|
+
this.exitSelectorsByNodeType = new Map();
|
78
|
+
|
79
|
+
/**
|
80
|
+
* An array of selectors that match any node type on the
|
81
|
+
* enter phase of traversal.
|
82
|
+
* @type {ESQueryParsedSelector[]}
|
83
|
+
*/
|
84
|
+
this.anyTypeEnterSelectors = [];
|
85
|
+
|
86
|
+
/**
|
87
|
+
* An array of selectors that match any node type on the
|
88
|
+
* exit phase of traversal.
|
89
|
+
* @type {ESQueryParsedSelector[]}
|
90
|
+
*/
|
91
|
+
this.anyTypeExitSelectors = [];
|
92
|
+
|
93
|
+
visitor.forEachName(rawSelector => {
|
94
|
+
const selector = parse(rawSelector);
|
95
|
+
|
96
|
+
/*
|
97
|
+
* If this selector has identified specific node types,
|
98
|
+
* add it to the map for these node types for faster lookup.
|
99
|
+
*/
|
100
|
+
if (selector.nodeTypes) {
|
101
|
+
const typeMap = selector.isExit
|
102
|
+
? this.exitSelectorsByNodeType
|
103
|
+
: this.enterSelectorsByNodeType;
|
104
|
+
|
105
|
+
selector.nodeTypes.forEach(nodeType => {
|
106
|
+
if (!typeMap.has(nodeType)) {
|
107
|
+
typeMap.set(nodeType, []);
|
108
|
+
}
|
109
|
+
typeMap.get(nodeType).push(selector);
|
110
|
+
});
|
111
|
+
return;
|
112
|
+
}
|
113
|
+
|
114
|
+
/*
|
115
|
+
* Remaining selectors are added to the "any type" selectors
|
116
|
+
* list for the appropriate phase of traversal. This ensures
|
117
|
+
* that all selectors will still be applied even if no
|
118
|
+
* specific node type is matched.
|
119
|
+
*/
|
120
|
+
const selectors = selector.isExit
|
121
|
+
? this.anyTypeExitSelectors
|
122
|
+
: this.anyTypeEnterSelectors;
|
123
|
+
|
124
|
+
selectors.push(selector);
|
125
|
+
});
|
126
|
+
|
127
|
+
// sort all selectors by specificity for prioritizing call order
|
128
|
+
this.anyTypeEnterSelectors.sort(compareSpecificity);
|
129
|
+
this.anyTypeExitSelectors.sort(compareSpecificity);
|
130
|
+
this.enterSelectorsByNodeType.forEach(selectorList =>
|
131
|
+
selectorList.sort(compareSpecificity),
|
132
|
+
);
|
133
|
+
this.exitSelectorsByNodeType.forEach(selectorList =>
|
134
|
+
selectorList.sort(compareSpecificity),
|
135
|
+
);
|
136
|
+
}
|
137
|
+
|
138
|
+
/**
|
139
|
+
* Checks if a node matches a given selector.
|
140
|
+
* @param {ASTNode} node The node to check
|
141
|
+
* @param {ASTNode[]} ancestry The ancestry of the node being checked.
|
142
|
+
* @param {ESQueryParsedSelector} selector An AST selector descriptor
|
143
|
+
* @returns {boolean} `true` if the selector matches the node, `false` otherwise
|
144
|
+
*/
|
145
|
+
matches(node, ancestry, selector) {
|
146
|
+
return matches(node, selector.root, ancestry, this.esqueryOptions);
|
147
|
+
}
|
148
|
+
|
149
|
+
/**
|
150
|
+
* Calculates all appropriate selectors to a node, in specificity order
|
151
|
+
* @param {ASTNode} node The node to check
|
152
|
+
* @param {ASTNode[]} ancestry The ancestry of the node being checked.
|
153
|
+
* @param {boolean} isExit `false` if the node is currently being entered, `true` if it's currently being exited
|
154
|
+
* @returns {string[]} An array of selectors that match the node.
|
155
|
+
*/
|
156
|
+
calculateSelectors(node, ancestry, isExit) {
|
157
|
+
const nodeTypeKey = this.esqueryOptions?.nodeTypeKey || "type";
|
158
|
+
const selectors = [];
|
159
|
+
|
160
|
+
/*
|
161
|
+
* Get the selectors that may match this node. First, check
|
162
|
+
* to see if the node type has specific selectors,
|
163
|
+
* then gather the "any type" selectors.
|
164
|
+
*/
|
165
|
+
const selectorsByNodeType =
|
166
|
+
(isExit
|
167
|
+
? this.exitSelectorsByNodeType
|
168
|
+
: this.enterSelectorsByNodeType
|
169
|
+
).get(node[nodeTypeKey]) || [];
|
170
|
+
const anyTypeSelectors = isExit
|
171
|
+
? this.anyTypeExitSelectors
|
172
|
+
: this.anyTypeEnterSelectors;
|
173
|
+
|
174
|
+
/*
|
175
|
+
* selectorsByNodeType and anyTypeSelectors were already sorted by specificity in the constructor.
|
176
|
+
* Iterate through each of them, applying selectors in the right order.
|
177
|
+
*/
|
178
|
+
let selectorsByNodeTypeIndex = 0;
|
179
|
+
let anyTypeSelectorsIndex = 0;
|
180
|
+
|
181
|
+
while (
|
182
|
+
selectorsByNodeTypeIndex < selectorsByNodeType.length ||
|
183
|
+
anyTypeSelectorsIndex < anyTypeSelectors.length
|
184
|
+
) {
|
185
|
+
/*
|
186
|
+
* If we've already exhausted the selectors for this node type,
|
187
|
+
* or if the next any type selector is more specific than the
|
188
|
+
* next selector for this node type, apply the any type selector.
|
189
|
+
*/
|
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
|
+
}
|
210
|
+
} else {
|
211
|
+
selectorsByNodeTypeIndex++;
|
212
|
+
|
213
|
+
if (this.matches(node, ancestry, nodeTypeSelector)) {
|
214
|
+
selectors.push(nodeTypeSelector.source);
|
215
|
+
}
|
216
|
+
}
|
217
|
+
}
|
218
|
+
|
219
|
+
return selectors;
|
220
|
+
}
|
221
|
+
}
|
222
|
+
|
223
|
+
//------------------------------------------------------------------------------
|
224
|
+
// Public Interface
|
225
|
+
//------------------------------------------------------------------------------
|
226
|
+
|
227
|
+
/**
|
228
|
+
* Traverses source code and ensures that visitor methods are called when
|
229
|
+
* entering and leaving each node.
|
230
|
+
*/
|
231
|
+
class SourceCodeTraverser {
|
232
|
+
/**
|
233
|
+
* The language of the source code being traversed.
|
234
|
+
* @type {Language}
|
235
|
+
*/
|
236
|
+
#language;
|
237
|
+
|
238
|
+
/**
|
239
|
+
* Map of languages to instances of this class.
|
240
|
+
* @type {WeakMap<Language, SourceCodeTraverser>}
|
241
|
+
*/
|
242
|
+
static instances = new WeakMap();
|
243
|
+
|
244
|
+
/**
|
245
|
+
* Creates a new instance.
|
246
|
+
* @param {Language} language The language of the source code being traversed.
|
247
|
+
*/
|
248
|
+
constructor(language) {
|
249
|
+
this.#language = language;
|
250
|
+
}
|
251
|
+
|
252
|
+
static getInstance(language) {
|
253
|
+
if (!this.instances.has(language)) {
|
254
|
+
this.instances.set(language, new this(language));
|
255
|
+
}
|
256
|
+
|
257
|
+
return this.instances.get(language);
|
258
|
+
}
|
259
|
+
|
260
|
+
/**
|
261
|
+
* Traverses the given source code synchronously.
|
262
|
+
* @param {SourceCode} sourceCode The source code to traverse.
|
263
|
+
* @param {SourceCodeVisitor} visitor The emitter to use for events.
|
264
|
+
* @param {Object} options Options for traversal.
|
265
|
+
* @param {ReturnType<SourceCode["traverse"]>} options.steps The steps to take during traversal.
|
266
|
+
* @returns {void}
|
267
|
+
* @throws {Error} If an error occurs during traversal.
|
268
|
+
*/
|
269
|
+
traverseSync(sourceCode, visitor, { steps } = {}) {
|
270
|
+
const esquery = new ESQueryHelper(visitor, {
|
271
|
+
visitorKeys: sourceCode.visitorKeys ?? this.#language.visitorKeys,
|
272
|
+
fallback: vk.getKeys,
|
273
|
+
matchClass: this.#language.matchesSelectorClass ?? (() => false),
|
274
|
+
nodeTypeKey: this.#language.nodeTypeKey,
|
275
|
+
});
|
276
|
+
|
277
|
+
const currentAncestry = [];
|
278
|
+
|
279
|
+
for (const step of steps ?? sourceCode.traverse()) {
|
280
|
+
switch (step.kind) {
|
281
|
+
case STEP_KIND_VISIT: {
|
282
|
+
try {
|
283
|
+
if (step.phase === 1) {
|
284
|
+
esquery
|
285
|
+
.calculateSelectors(
|
286
|
+
step.target,
|
287
|
+
currentAncestry,
|
288
|
+
false,
|
289
|
+
)
|
290
|
+
.forEach(selector => {
|
291
|
+
visitor.callSync(selector, step.target);
|
292
|
+
});
|
293
|
+
currentAncestry.unshift(step.target);
|
294
|
+
} else {
|
295
|
+
currentAncestry.shift();
|
296
|
+
esquery
|
297
|
+
.calculateSelectors(
|
298
|
+
step.target,
|
299
|
+
currentAncestry,
|
300
|
+
true,
|
301
|
+
)
|
302
|
+
.forEach(selector => {
|
303
|
+
visitor.callSync(selector, step.target);
|
304
|
+
});
|
305
|
+
}
|
306
|
+
} catch (err) {
|
307
|
+
err.currentNode = step.target;
|
308
|
+
throw err;
|
309
|
+
}
|
310
|
+
break;
|
311
|
+
}
|
312
|
+
|
313
|
+
case STEP_KIND_CALL: {
|
314
|
+
visitor.callSync(step.target, ...step.args);
|
315
|
+
break;
|
316
|
+
}
|
317
|
+
|
318
|
+
default:
|
319
|
+
throw new Error(
|
320
|
+
`Invalid traversal step found: "${step.kind}".`,
|
321
|
+
);
|
322
|
+
}
|
323
|
+
}
|
324
|
+
}
|
325
|
+
}
|
326
|
+
|
327
|
+
module.exports = { SourceCodeTraverser };
|
@@ -0,0 +1,81 @@
|
|
1
|
+
/**
|
2
|
+
* @fileoverview SourceCodeVisitor class
|
3
|
+
* @author Nicholas C. Zakas
|
4
|
+
*/
|
5
|
+
|
6
|
+
"use strict";
|
7
|
+
|
8
|
+
//-----------------------------------------------------------------------------
|
9
|
+
// Helpers
|
10
|
+
//-----------------------------------------------------------------------------
|
11
|
+
|
12
|
+
const emptyArray = Object.freeze([]);
|
13
|
+
|
14
|
+
//------------------------------------------------------------------------------
|
15
|
+
// Exports
|
16
|
+
//------------------------------------------------------------------------------
|
17
|
+
|
18
|
+
/**
|
19
|
+
* A structure to hold a list of functions to call for a given name.
|
20
|
+
* This is used to allow multiple rules to register functions for a given name
|
21
|
+
* without having to know about each other.
|
22
|
+
*/
|
23
|
+
class SourceCodeVisitor {
|
24
|
+
/**
|
25
|
+
* The functions to call for a given name.
|
26
|
+
* @type {Map<string, Function[]>}
|
27
|
+
*/
|
28
|
+
#functions = new Map();
|
29
|
+
|
30
|
+
/**
|
31
|
+
* Adds a function to the list of functions to call for a given name.
|
32
|
+
* @param {string} name The name of the function to call.
|
33
|
+
* @param {Function} func The function to call.
|
34
|
+
* @returns {void}
|
35
|
+
*/
|
36
|
+
add(name, func) {
|
37
|
+
if (this.#functions.has(name)) {
|
38
|
+
this.#functions.get(name).push(func);
|
39
|
+
} else {
|
40
|
+
this.#functions.set(name, [func]);
|
41
|
+
}
|
42
|
+
}
|
43
|
+
|
44
|
+
/**
|
45
|
+
* Gets the list of functions to call for a given name.
|
46
|
+
* @param {string} name The name of the function to call.
|
47
|
+
* @returns {Function[]} The list of functions to call.
|
48
|
+
*/
|
49
|
+
get(name) {
|
50
|
+
if (this.#functions.has(name)) {
|
51
|
+
return this.#functions.get(name);
|
52
|
+
}
|
53
|
+
|
54
|
+
return emptyArray;
|
55
|
+
}
|
56
|
+
|
57
|
+
/**
|
58
|
+
* Iterates over all names and calls the callback with the name.
|
59
|
+
* @param {(name:string) => void} callback The callback to call for each name.
|
60
|
+
* @returns {void}
|
61
|
+
*/
|
62
|
+
forEachName(callback) {
|
63
|
+
this.#functions.forEach((funcs, name) => {
|
64
|
+
callback(name);
|
65
|
+
});
|
66
|
+
}
|
67
|
+
|
68
|
+
/**
|
69
|
+
* Calls the functions for a given name with the given arguments.
|
70
|
+
* @param {string} name The name of the function to call.
|
71
|
+
* @param {any[]} args The arguments to pass to the function.
|
72
|
+
* @returns {void}
|
73
|
+
*/
|
74
|
+
callSync(name, ...args) {
|
75
|
+
if (this.#functions.has(name)) {
|
76
|
+
this.#functions.get(name).forEach(func => func(...args));
|
77
|
+
}
|
78
|
+
}
|
79
|
+
}
|
80
|
+
|
81
|
+
module.exports = { SourceCodeVisitor };
|
package/lib/options.js
CHANGED
@@ -66,6 +66,7 @@ const optionator = require("optionator");
|
|
66
66
|
* @property {string[]} [suppressRule] Suppress specific rules
|
67
67
|
* @property {string} [suppressionsLocation] Path to the suppressions file or directory
|
68
68
|
* @property {boolean} [pruneSuppressions] Prune unused suppressions
|
69
|
+
* @property {boolean} [passOnUnprunedSuppressions] Ignore unused suppressions
|
69
70
|
*/
|
70
71
|
|
71
72
|
//------------------------------------------------------------------------------
|
@@ -449,6 +450,12 @@ module.exports = function (usingFlatConfig) {
|
|
449
450
|
default: "false",
|
450
451
|
description: "Prune unused suppressions",
|
451
452
|
},
|
453
|
+
{
|
454
|
+
option: "pass-on-unpruned-suppressions",
|
455
|
+
type: "Boolean",
|
456
|
+
default: "false",
|
457
|
+
description: "Ignore unused suppressions",
|
458
|
+
},
|
452
459
|
{
|
453
460
|
heading: "Miscellaneous",
|
454
461
|
},
|