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.
Files changed (37) hide show
  1. package/README.md +1 -1
  2. package/conf/ecma-version.js +1 -1
  3. package/conf/globals.js +10 -0
  4. package/lib/cli.js +20 -23
  5. package/lib/config/config-loader.js +32 -21
  6. package/lib/config/config.js +34 -11
  7. package/lib/eslint/eslint.js +18 -21
  8. package/lib/languages/js/source-code/source-code.js +104 -27
  9. package/lib/linter/apply-disable-directives.js +2 -4
  10. package/lib/linter/code-path-analysis/code-path-analyzer.js +8 -9
  11. package/lib/linter/linter.js +30 -61
  12. package/lib/linter/source-code-traverser.js +327 -0
  13. package/lib/linter/source-code-visitor.js +81 -0
  14. package/lib/options.js +7 -0
  15. package/lib/rules/class-methods-use-this.js +7 -0
  16. package/lib/rules/func-style.js +57 -7
  17. package/lib/rules/no-implicit-globals.js +31 -15
  18. package/lib/rules/no-magic-numbers.js +98 -5
  19. package/lib/rules/no-promise-executor-return.js +4 -35
  20. package/lib/rules/no-restricted-globals.js +35 -2
  21. package/lib/rules/no-restricted-properties.js +24 -10
  22. package/lib/rules/no-setter-return.js +13 -48
  23. package/lib/rules/no-shadow.js +262 -6
  24. package/lib/rules/no-unassigned-vars.js +14 -6
  25. package/lib/rules/no-use-before-define.js +99 -1
  26. package/lib/rules/no-var.js +14 -2
  27. package/lib/rules/prefer-arrow-callback.js +9 -0
  28. package/lib/rules/prefer-regex-literals.js +1 -18
  29. package/lib/services/suppressions-service.js +8 -0
  30. package/lib/services/warning-service.js +85 -0
  31. package/lib/shared/naming.js +109 -0
  32. package/lib/shared/relative-module-resolver.js +28 -0
  33. package/lib/types/index.d.ts +18 -7
  34. package/lib/types/rules.d.ts +52 -2
  35. package/package.json +12 -10
  36. package/lib/linter/node-event-generator.js +0 -256
  37. 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.emitter.emit(eventName, currentSegment, node);
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.emitter.emit(eventName, headSegment, node);
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.emitter.emit(eventName, currentSegment, node);
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.emitter.emit("onCodePathStart", codePath, node);
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.emitter.emit("onCodePathEnd", codePath, node);
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.emitter = eventGenerator.emitter;
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.emitter.emit(
820
- "onCodePathSegmentLoop",
819
+ this.emit("onCodePathSegmentLoop", [
821
820
  fromSegment,
822
821
  toSegment,
823
822
  this.currentNode,
824
- );
823
+ ]);
825
824
  }
826
825
  }
827
826
  }
@@ -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 STEP_KIND_VISIT = 1;
69
- const STEP_KIND_CALL = 2;
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 emitter = createEmitter();
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
- emitter.on(selector, addRuleErrorHandler(ruleListener));
1342
+ visitor.add(selector, addRuleErrorHandler(ruleListener));
1345
1343
  });
1346
1344
  });
1347
1345
 
1348
- const eventGenerator = new NodeEventGenerator(emitter, {
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
- default:
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({ cwd, configType = "flat", flags = [] } = {}) {
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
- globalThis.process?.emitWarning?.(
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
- globalThis?.process?.emitWarning?.(
2802
- `Circular fixes detected while fixing ${options?.filename ?? "text"}. It is likely that you have conflicting rules in your configuration.`,
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
  },