eslint 9.0.0-beta.1 → 9.0.0-rc.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 CHANGED
@@ -127,6 +127,18 @@ In other cases (including if rules need to warn on more or fewer cases due to ne
127
127
 
128
128
  Once a language feature has been adopted into the ECMAScript standard (stage 4 according to the [TC39 process](https://tc39.github.io/process-document/)), we will accept issues and pull requests related to the new feature, subject to our [contributing guidelines](https://eslint.org/docs/latest/contribute). Until then, please use the appropriate parser and plugin(s) for your experimental feature.
129
129
 
130
+ ### Which Node.js versions does ESLint support?
131
+
132
+ ESLint updates the supported Node.js versions with each major release of ESLint. At that time, ESLint's supported Node.js versions are updated to be:
133
+
134
+ 1. The most recent maintenance release of Node.js
135
+ 1. The lowest minor version of the Node.js LTS release that includes the features the ESLint team wants to use.
136
+ 1. The Node.js Current release
137
+
138
+ ESLint is also expected to work with Node.js versions released after the Node.js Current release.
139
+
140
+ Refer to the [Quick Start Guide](https://eslint.org/docs/latest/use/getting-started#prerequisites) for the officially supported Node.js versions for a given ESLint release.
141
+
130
142
  ### Where to ask for help?
131
143
 
132
144
  Open a [discussion](https://github.com/eslint/eslint/discussions) or stop by our [Discord server](https://eslint.org/chat).
@@ -213,6 +225,11 @@ The people who manage releases, review feature requests, and meet regularly to e
213
225
  Nicholas C. Zakas
214
226
  </a>
215
227
  </td><td align="center" valign="top" width="11%">
228
+ <a href="https://github.com/fasttime">
229
+ <img src="https://github.com/fasttime.png?s=75" width="75" height="75" alt="Francesco Trotta's Avatar"><br />
230
+ Francesco Trotta
231
+ </a>
232
+ </td><td align="center" valign="top" width="11%">
216
233
  <a href="https://github.com/mdjermanovic">
217
234
  <img src="https://github.com/mdjermanovic.png?s=75" width="75" height="75" alt="Milos Djermanovic's Avatar"><br />
218
235
  Milos Djermanovic
@@ -250,11 +267,6 @@ Bryan Mishkin
250
267
  Josh Goldberg ✨
251
268
  </a>
252
269
  </td><td align="center" valign="top" width="11%">
253
- <a href="https://github.com/fasttime">
254
- <img src="https://github.com/fasttime.png?s=75" width="75" height="75" alt="Francesco Trotta's Avatar"><br />
255
- Francesco Trotta
256
- </a>
257
- </td><td align="center" valign="top" width="11%">
258
270
  <a href="https://github.com/Tanujkanti4441">
259
271
  <img src="https://github.com/Tanujkanti4441.png?s=75" width="75" height="75" alt="Tanuj Kanti's Avatar"><br />
260
272
  Tanuj Kanti
@@ -291,8 +303,8 @@ The following companies, organizations, and individuals support ESLint's ongoing
291
303
  <!-- NOTE: This section is autogenerated. Do not manually edit.-->
292
304
  <!--sponsorsstart-->
293
305
  <h3>Platinum Sponsors</h3>
294
- <p><a href="#"><img src="https://images.opencollective.com/2021-frameworks-fund/logo.png" alt="Chrome Frameworks Fund" height="undefined"></a> <a href="https://automattic.com"><img src="https://images.opencollective.com/automattic/d0ef3e1/logo.png" alt="Automattic" height="undefined"></a></p><h3>Gold Sponsors</h3>
295
- <p><a href="https://engineering.salesforce.com"><img src="https://images.opencollective.com/salesforce/ca8f997/logo.png" alt="Salesforce" height="96"></a> <a href="https://www.airbnb.com/"><img src="https://images.opencollective.com/airbnb/d327d66/logo.png" alt="Airbnb" height="96"></a></p><h3>Silver Sponsors</h3>
306
+ <p><a href="https://automattic.com"><img src="https://images.opencollective.com/automattic/d0ef3e1/logo.png" alt="Automattic" height="undefined"></a></p><h3>Gold Sponsors</h3>
307
+ <p><a href="https://bitwarden.com"><img src="https://avatars.githubusercontent.com/u/15990069?v=4" alt="Bitwarden" height="96"></a> <a href="https://engineering.salesforce.com"><img src="https://images.opencollective.com/salesforce/ca8f997/logo.png" alt="Salesforce" height="96"></a> <a href="https://www.airbnb.com/"><img src="https://images.opencollective.com/airbnb/d327d66/logo.png" alt="Airbnb" height="96"></a></p><h3>Silver Sponsors</h3>
296
308
  <p><a href="https://www.jetbrains.com/"><img src="https://images.opencollective.com/jetbrains/eb04ddc/logo.png" alt="JetBrains" 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?v=4" alt="American Express" height="64"></a> <a href="https://www.workleap.com"><img src="https://avatars.githubusercontent.com/u/53535748?u=d1e55d7661d724bf2281c1bfd33cb8f99fe2465f&v=4" alt="Workleap" height="64"></a></p><h3>Bronze Sponsors</h3>
297
309
  <p><a href="https://www.notion.so"><img src="https://images.opencollective.com/notion/bf3b117/logo.png" alt="notion" height="32"></a> <a href="https://themeisle.com"><img src="https://images.opencollective.com/themeisle/d5592fe/logo.png" alt="ThemeIsle" 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://transloadit.com/"><img src="https://avatars.githubusercontent.com/u/125754?v=4" alt="Transloadit" height="32"></a> <a href="https://www.ignitionapp.com"><img src="https://avatars.githubusercontent.com/u/5753491?v=4" alt="Ignition" height="32"></a> <a href="https://nx.dev"><img src="https://avatars.githubusercontent.com/u/23692104?v=4" alt="Nx" height="32"></a> <a href="https://herocoders.com"><img src="https://avatars.githubusercontent.com/u/37549774?v=4" alt="HeroCoders" height="32"></a> <a href="https://usenextbase.com"><img src="https://avatars.githubusercontent.com/u/145838380?v=4" alt="Nextbase Starter Kit" height="32"></a></p>
298
310
  <!--sponsorsend-->
package/bin/eslint.js CHANGED
@@ -149,7 +149,8 @@ ${getErrorMessage(error)}`;
149
149
  }
150
150
 
151
151
  // Otherwise, call the CLI.
152
- const exitCode = await require("../lib/cli").execute(
152
+ const cli = require("../lib/cli");
153
+ const exitCode = await cli.execute(
153
154
  process.argv,
154
155
  process.argv.includes("--stdin") ? await readStdin() : null,
155
156
  true
package/lib/cli.js CHANGED
@@ -37,6 +37,7 @@ const debug = require("debug")("eslint:cli");
37
37
  /** @typedef {import("./eslint/eslint").LintMessage} LintMessage */
38
38
  /** @typedef {import("./eslint/eslint").LintResult} LintResult */
39
39
  /** @typedef {import("./options").ParsedCLIOptions} ParsedCLIOptions */
40
+ /** @typedef {import("./shared/types").Plugin} Plugin */
40
41
  /** @typedef {import("./shared/types").ResultsMeta} ResultsMeta */
41
42
 
42
43
  //------------------------------------------------------------------------------
@@ -47,6 +48,32 @@ const mkdir = promisify(fs.mkdir);
47
48
  const stat = promisify(fs.stat);
48
49
  const writeFile = promisify(fs.writeFile);
49
50
 
51
+ /**
52
+ * Loads plugins with the specified names.
53
+ * @param {{ "import": (name: string) => Promise<any> }} importer An object with an `import` method called once for each plugin.
54
+ * @param {string[]} pluginNames The names of the plugins to be loaded, with or without the "eslint-plugin-" prefix.
55
+ * @returns {Promise<Record<string, Plugin>>} A mapping of plugin short names to implementations.
56
+ */
57
+ async function loadPlugins(importer, pluginNames) {
58
+ const plugins = {};
59
+
60
+ await Promise.all(pluginNames.map(async pluginName => {
61
+
62
+ const longName = naming.normalizePackageName(pluginName, "eslint-plugin");
63
+ const module = await importer.import(longName);
64
+
65
+ if (!("default" in module)) {
66
+ throw new Error(`"${longName}" cannot be used with the \`--plugin\` option because its default module does not provide a \`default\` export`);
67
+ }
68
+
69
+ const shortName = naming.getShorthandName(pluginName, "eslint-plugin");
70
+
71
+ plugins[shortName] = module.default;
72
+ }));
73
+
74
+ return plugins;
75
+ }
76
+
50
77
  /**
51
78
  * Predicate function for whether or not to apply fixes in quiet mode.
52
79
  * If a message is a warning, do not apply a fix.
@@ -152,17 +179,7 @@ async function translateOptions({
152
179
  }
153
180
 
154
181
  if (plugin) {
155
- const plugins = {};
156
-
157
- for (const pluginName of plugin) {
158
-
159
- const shortName = naming.getShorthandName(pluginName, "eslint-plugin");
160
- const longName = naming.normalizePackageName(pluginName, "eslint-plugin");
161
-
162
- plugins[shortName] = await importer.import(longName);
163
- }
164
-
165
- overrideConfig[0].plugins = plugins;
182
+ overrideConfig[0].plugins = await loadPlugins(importer, plugin);
166
183
  }
167
184
 
168
185
  } else {
@@ -588,6 +588,5 @@ const flatConfigSchema = {
588
588
 
589
589
  module.exports = {
590
590
  flatConfigSchema,
591
- assertIsRuleSeverity,
592
- assertIsRuleOptions
591
+ assertIsRuleSeverity
593
592
  };
@@ -167,9 +167,22 @@ class RuleValidator {
167
167
  validateRule(ruleOptions.slice(1));
168
168
 
169
169
  if (validateRule.errors) {
170
- throw new Error(`Key "rules": Key "${ruleId}": ${
170
+ throw new Error(`Key "rules": Key "${ruleId}":\n${
171
171
  validateRule.errors.map(
172
- error => `\tValue ${JSON.stringify(error.data)} ${error.message}.\n`
172
+ error => {
173
+ if (
174
+ error.keyword === "additionalProperties" &&
175
+ error.schema === false &&
176
+ typeof error.parentSchema?.properties === "object" &&
177
+ typeof error.params?.additionalProperty === "string"
178
+ ) {
179
+ const expectedProperties = Object.keys(error.parentSchema.properties).map(property => `"${property}"`);
180
+
181
+ return `\tValue ${JSON.stringify(error.data)} ${error.message}.\n\t\tUnexpected property "${error.params.additionalProperty}". Expected properties: ${expectedProperties.join(", ")}.\n`;
182
+ }
183
+
184
+ return `\tValue ${JSON.stringify(error.data)} ${error.message}.\n`;
185
+ }
173
186
  ).join("")
174
187
  }`);
175
188
  }
@@ -907,7 +907,6 @@ function getCacheFile(cacheFile, cwd) {
907
907
  //-----------------------------------------------------------------------------
908
908
 
909
909
  module.exports = {
910
- isGlobPattern,
911
910
  findFiles,
912
911
 
913
912
  isNonEmptyString,
@@ -838,6 +838,7 @@ class ESLint {
838
838
  configs,
839
839
  errorOnUnmatchedPattern
840
840
  });
841
+ const controller = new AbortController();
841
842
 
842
843
  debug(`${filePaths.length} files found in: ${Date.now() - startTime}ms`);
843
844
 
@@ -906,9 +907,12 @@ class ESLint {
906
907
  fixer = message => shouldMessageBeFixed(message, config, fixTypesSet) && originalFix(message);
907
908
  }
908
909
 
909
- return fs.readFile(filePath, "utf8")
910
+ return fs.readFile(filePath, { encoding: "utf8", signal: controller.signal })
910
911
  .then(text => {
911
912
 
913
+ // fail immediately if an error occurred in another file
914
+ controller.signal.throwIfAborted();
915
+
912
916
  // do the linting
913
917
  const result = verifyText({
914
918
  text,
@@ -932,6 +936,9 @@ class ESLint {
932
936
  }
933
937
 
934
938
  return result;
939
+ }).catch(error => {
940
+ controller.abort(error);
941
+ throw error;
935
942
  });
936
943
 
937
944
  })
@@ -222,7 +222,6 @@ function forwardCurrentToHead(analyzer, node) {
222
222
  : "onUnreachableCodePathSegmentStart";
223
223
 
224
224
  debug.dump(`${eventName} ${headSegment.id}`);
225
-
226
225
  CodePathSegment.markUsed(headSegment);
227
226
  analyzer.emitter.emit(
228
227
  eventName,
@@ -1,13 +1,11 @@
1
1
  "use strict";
2
2
 
3
3
  const { Linter } = require("./linter");
4
- const { interpolate } = require("./interpolate");
5
4
  const SourceCodeFixer = require("./source-code-fixer");
6
5
 
7
6
  module.exports = {
8
7
  Linter,
9
8
 
10
9
  // For testers.
11
- SourceCodeFixer,
12
- interpolate
10
+ SourceCodeFixer
13
11
  };
@@ -30,7 +30,6 @@ const
30
30
  } = require("@eslint/eslintrc/universal"),
31
31
  Traverser = require("../shared/traverser"),
32
32
  { SourceCode } = require("../source-code"),
33
- CodePathAnalyzer = require("./code-path-analysis/code-path-analyzer"),
34
33
  applyDisableDirectives = require("./apply-disable-directives"),
35
34
  ConfigCommentParser = require("./config-comment-parser"),
36
35
  NodeEventGenerator = require("./node-event-generator"),
@@ -53,13 +52,13 @@ const commentParser = new ConfigCommentParser();
53
52
  const DEFAULT_ERROR_LOC = { start: { line: 1, column: 0 }, end: { line: 1, column: 1 } };
54
53
  const parserSymbol = Symbol.for("eslint.RuleTester.parser");
55
54
  const { LATEST_ECMA_VERSION } = require("../../conf/ecma-version");
55
+ const STEP_KIND_VISIT = 1;
56
+ const STEP_KIND_CALL = 2;
56
57
 
57
58
  //------------------------------------------------------------------------------
58
59
  // Typedefs
59
60
  //------------------------------------------------------------------------------
60
61
 
61
- /** @typedef {InstanceType<import("../cli-engine/config-array").ConfigArray>} ConfigArray */
62
- /** @typedef {InstanceType<import("../cli-engine/config-array").ExtractedConfig>} ExtractedConfig */
63
62
  /** @typedef {import("../shared/types").ConfigData} ConfigData */
64
63
  /** @typedef {import("../shared/types").Environment} Environment */
65
64
  /** @typedef {import("../shared/types").GlobalConf} GlobalConf */
@@ -439,6 +438,14 @@ function getDirectiveComments(sourceCode, ruleMapper, warnInlineConfig, config)
439
438
  return;
440
439
  }
441
440
 
441
+ if (Object.hasOwn(configuredRules, name)) {
442
+ problems.push(createLintingProblem({
443
+ message: `Rule "${name}" is already configured by another configuration comment in the preceding code. This configuration is ignored.`,
444
+ loc: comment.loc
445
+ }));
446
+ return;
447
+ }
448
+
442
449
  let ruleOptions = Array.isArray(ruleValue) ? ruleValue : [ruleValue];
443
450
 
444
451
  /*
@@ -980,22 +987,13 @@ function createRuleListeners(rule, ruleContext) {
980
987
  * @param {string} physicalFilename The full path of the file on disk without any code block information
981
988
  * @param {Function} ruleFilter A predicate function to filter which rules should be executed.
982
989
  * @returns {LintMessage[]} An array of reported problems
990
+ * @throws {Error} If traversal into a node fails.
983
991
  */
984
992
  function runRules(sourceCode, configuredRules, ruleMapper, parserName, languageOptions, settings, filename, disableFixes, cwd, physicalFilename, ruleFilter) {
985
993
  const emitter = createEmitter();
986
- const nodeQueue = [];
987
- let currentNode = sourceCode.ast;
988
-
989
- Traverser.traverse(sourceCode.ast, {
990
- enter(node, parent) {
991
- node.parent = parent;
992
- nodeQueue.push({ isEntering: true, node });
993
- },
994
- leave(node) {
995
- nodeQueue.push({ isEntering: false, node });
996
- },
997
- visitorKeys: sourceCode.visitorKeys
998
- });
994
+
995
+ // must happen first to assign all node.parent properties
996
+ const eventQueue = sourceCode.traverse();
999
997
 
1000
998
  /*
1001
999
  * Create a frozen object with the ruleContext properties and methods that are shared by all rules.
@@ -1125,25 +1123,34 @@ function runRules(sourceCode, configuredRules, ruleMapper, parserName, languageO
1125
1123
  });
1126
1124
  });
1127
1125
 
1128
- // only run code path analyzer if the top level node is "Program", skip otherwise
1129
- const eventGenerator = nodeQueue[0].node.type === "Program"
1130
- ? new CodePathAnalyzer(new NodeEventGenerator(emitter, { visitorKeys: sourceCode.visitorKeys, fallback: Traverser.getKeys }))
1131
- : new NodeEventGenerator(emitter, { visitorKeys: sourceCode.visitorKeys, fallback: Traverser.getKeys });
1126
+ const eventGenerator = new NodeEventGenerator(emitter, { visitorKeys: sourceCode.visitorKeys, fallback: Traverser.getKeys });
1132
1127
 
1133
- nodeQueue.forEach(traversalInfo => {
1134
- currentNode = traversalInfo.node;
1128
+ for (const step of eventQueue) {
1129
+ switch (step.kind) {
1130
+ case STEP_KIND_VISIT: {
1131
+ try {
1132
+ if (step.phase === 1) {
1133
+ eventGenerator.enterNode(step.target);
1134
+ } else {
1135
+ eventGenerator.leaveNode(step.target);
1136
+ }
1137
+ } catch (err) {
1138
+ err.currentNode = step.target;
1139
+ throw err;
1140
+ }
1141
+ break;
1142
+ }
1135
1143
 
1136
- try {
1137
- if (traversalInfo.isEntering) {
1138
- eventGenerator.enterNode(currentNode);
1139
- } else {
1140
- eventGenerator.leaveNode(currentNode);
1144
+ case STEP_KIND_CALL: {
1145
+ emitter.emit(step.target, ...step.args);
1146
+ break;
1141
1147
  }
1142
- } catch (err) {
1143
- err.currentNode = currentNode;
1144
- throw err;
1148
+
1149
+ default:
1150
+ throw new Error(`Invalid traversal step found: "${step.type}".`);
1145
1151
  }
1146
- });
1152
+
1153
+ }
1147
1154
 
1148
1155
  return lintingProblems;
1149
1156
  }
@@ -1706,6 +1713,14 @@ class Linter {
1706
1713
  return;
1707
1714
  }
1708
1715
 
1716
+ if (Object.hasOwn(mergedInlineConfig.rules, ruleId)) {
1717
+ inlineConfigProblems.push(createLintingProblem({
1718
+ message: `Rule "${ruleId}" is already configured by another configuration comment in the preceding code. This configuration is ignored.`,
1719
+ loc: node.loc
1720
+ }));
1721
+ return;
1722
+ }
1723
+
1709
1724
  try {
1710
1725
 
1711
1726
  let ruleOptions = Array.isArray(ruleValue) ? ruleValue : [ruleValue];
@@ -1,5 +1,7 @@
1
1
  "use strict";
2
2
 
3
+ const RuleTester = require("./rule-tester");
4
+
3
5
  module.exports = {
4
- RuleTester: require("./rule-tester")
6
+ RuleTester
5
7
  };
@@ -109,6 +109,7 @@ module.exports = {
109
109
  IfStatement: increaseComplexity,
110
110
  WhileStatement: increaseComplexity,
111
111
  DoWhileStatement: increaseComplexity,
112
+ AssignmentPattern: increaseComplexity,
112
113
 
113
114
  // Avoid `default`
114
115
  "SwitchCase[test]": increaseComplexity,
@@ -120,6 +121,18 @@ module.exports = {
120
121
  }
121
122
  },
122
123
 
124
+ MemberExpression(node) {
125
+ if (node.optional === true) {
126
+ increaseComplexity();
127
+ }
128
+ },
129
+
130
+ CallExpression(node) {
131
+ if (node.optional === true) {
132
+ increaseComplexity();
133
+ }
134
+ },
135
+
123
136
  onCodePathEnd(codePath, node) {
124
137
  const complexity = complexities.pop();
125
138
 
@@ -119,6 +119,30 @@ function isPossibleConstructor(node) {
119
119
  }
120
120
  }
121
121
 
122
+ /**
123
+ * A class to store information about a code path segment.
124
+ */
125
+ class SegmentInfo {
126
+
127
+ /**
128
+ * Indicates if super() is called in all code paths.
129
+ * @type {boolean}
130
+ */
131
+ calledInEveryPaths = false;
132
+
133
+ /**
134
+ * Indicates if super() is called in any code paths.
135
+ * @type {boolean}
136
+ */
137
+ calledInSomePaths = false;
138
+
139
+ /**
140
+ * The nodes which have been validated and don't need to be reconsidered.
141
+ * @type {ASTNode[]}
142
+ */
143
+ validNodes = [];
144
+ }
145
+
122
146
  //------------------------------------------------------------------------------
123
147
  // Rule Definition
124
148
  //------------------------------------------------------------------------------
@@ -159,12 +183,8 @@ module.exports = {
159
183
  */
160
184
  let funcInfo = null;
161
185
 
162
- /*
163
- * {Map<string, {calledInSomePaths: boolean, calledInEveryPaths: boolean}>}
164
- * Information for each code path segment.
165
- * - calledInSomePaths: A flag of be called `super()` in some code paths.
166
- * - calledInEveryPaths: A flag of be called `super()` in all code paths.
167
- * - validNodes:
186
+ /**
187
+ * @type {Record<string, SegmentInfo>}
168
188
  */
169
189
  let segInfoMap = Object.create(null);
170
190
 
@@ -174,7 +194,16 @@ module.exports = {
174
194
  * @returns {boolean} The flag which shows `super()` is called in some paths
175
195
  */
176
196
  function isCalledInSomePath(segment) {
177
- return segment.reachable && segInfoMap[segment.id].calledInSomePaths;
197
+ return segment.reachable && segInfoMap[segment.id]?.calledInSomePaths;
198
+ }
199
+
200
+ /**
201
+ * Determines if a segment has been seen in the traversal.
202
+ * @param {CodePathSegment} segment A code path segment to check.
203
+ * @returns {boolean} `true` if the segment has been seen.
204
+ */
205
+ function hasSegmentBeenSeen(segment) {
206
+ return !!segInfoMap[segment.id];
178
207
  }
179
208
 
180
209
  /**
@@ -190,10 +219,10 @@ module.exports = {
190
219
  * If not skipped, this never becomes true after a loop.
191
220
  */
192
221
  if (segment.nextSegments.length === 1 &&
193
- segment.nextSegments[0].isLoopedPrevSegment(segment)
194
- ) {
222
+ segment.nextSegments[0]?.isLoopedPrevSegment(segment)) {
195
223
  return true;
196
224
  }
225
+
197
226
  return segment.reachable && segInfoMap[segment.id].calledInEveryPaths;
198
227
  }
199
228
 
@@ -250,9 +279,9 @@ module.exports = {
250
279
  }
251
280
 
252
281
  // Reports if `super()` lacked.
253
- const segments = codePath.returnedSegments;
254
- const calledInEveryPaths = segments.every(isCalledInEveryPath);
255
- const calledInSomePaths = segments.some(isCalledInSomePath);
282
+ const seenSegments = codePath.returnedSegments.filter(hasSegmentBeenSeen);
283
+ const calledInEveryPaths = seenSegments.every(isCalledInEveryPath);
284
+ const calledInSomePaths = seenSegments.some(isCalledInSomePath);
256
285
 
257
286
  if (!calledInEveryPaths) {
258
287
  context.report({
@@ -278,18 +307,16 @@ module.exports = {
278
307
  }
279
308
 
280
309
  // Initialize info.
281
- const info = segInfoMap[segment.id] = {
282
- calledInSomePaths: false,
283
- calledInEveryPaths: false,
284
- validNodes: []
285
- };
310
+ const info = segInfoMap[segment.id] = new SegmentInfo();
286
311
 
287
312
  // When there are previous segments, aggregates these.
288
313
  const prevSegments = segment.prevSegments;
289
314
 
290
315
  if (prevSegments.length > 0) {
291
- info.calledInSomePaths = prevSegments.some(isCalledInSomePath);
292
- info.calledInEveryPaths = prevSegments.every(isCalledInEveryPath);
316
+ const seenPrevSegments = prevSegments.filter(hasSegmentBeenSeen);
317
+
318
+ info.calledInSomePaths = seenPrevSegments.some(isCalledInSomePath);
319
+ info.calledInEveryPaths = seenPrevSegments.every(isCalledInEveryPath);
293
320
  }
294
321
  },
295
322
 
@@ -326,12 +353,12 @@ module.exports = {
326
353
  funcInfo.codePath.traverseSegments(
327
354
  { first: toSegment, last: fromSegment },
328
355
  segment => {
329
- const info = segInfoMap[segment.id];
330
- const prevSegments = segment.prevSegments;
356
+ const info = segInfoMap[segment.id] ?? new SegmentInfo();
357
+ const seenPrevSegments = segment.prevSegments.filter(hasSegmentBeenSeen);
331
358
 
332
359
  // Updates flags.
333
- info.calledInSomePaths = prevSegments.some(isCalledInSomePath);
334
- info.calledInEveryPaths = prevSegments.every(isCalledInEveryPath);
360
+ info.calledInSomePaths = seenPrevSegments.some(isCalledInSomePath);
361
+ info.calledInEveryPaths = seenPrevSegments.every(isCalledInEveryPath);
335
362
 
336
363
  // If flags become true anew, reports the valid nodes.
337
364
  if (info.calledInSomePaths || isRealLoop) {
@@ -348,6 +375,9 @@ module.exports = {
348
375
  });
349
376
  }
350
377
  }
378
+
379
+ // save just in case we created a new SegmentInfo object
380
+ segInfoMap[segment.id] = info;
351
381
  }
352
382
  );
353
383
  },
@@ -48,9 +48,9 @@ function isFallThroughComment(comment, fallthroughCommentPattern) {
48
48
  * @param {ASTNode} subsequentCase The case after caseWhichFallsThrough.
49
49
  * @param {RuleContext} context A rule context which stores comments.
50
50
  * @param {RegExp} fallthroughCommentPattern A pattern to match comment to.
51
- * @returns {boolean} `true` if the case has a valid fallthrough comment.
51
+ * @returns {null | object} the comment if the case has a valid fallthrough comment, otherwise null
52
52
  */
53
- function hasFallthroughComment(caseWhichFallsThrough, subsequentCase, context, fallthroughCommentPattern) {
53
+ function getFallthroughComment(caseWhichFallsThrough, subsequentCase, context, fallthroughCommentPattern) {
54
54
  const sourceCode = context.sourceCode;
55
55
 
56
56
  if (caseWhichFallsThrough.consequent.length === 1 && caseWhichFallsThrough.consequent[0].type === "BlockStatement") {
@@ -58,13 +58,17 @@ function hasFallthroughComment(caseWhichFallsThrough, subsequentCase, context, f
58
58
  const commentInBlock = sourceCode.getCommentsBefore(trailingCloseBrace).pop();
59
59
 
60
60
  if (commentInBlock && isFallThroughComment(commentInBlock.value, fallthroughCommentPattern)) {
61
- return true;
61
+ return commentInBlock;
62
62
  }
63
63
  }
64
64
 
65
65
  const comment = sourceCode.getCommentsBefore(subsequentCase).pop();
66
66
 
67
- return Boolean(comment && isFallThroughComment(comment.value, fallthroughCommentPattern));
67
+ if (comment && isFallThroughComment(comment.value, fallthroughCommentPattern)) {
68
+ return comment;
69
+ }
70
+
71
+ return null;
68
72
  }
69
73
 
70
74
  /**
@@ -103,12 +107,17 @@ module.exports = {
103
107
  allowEmptyCase: {
104
108
  type: "boolean",
105
109
  default: false
110
+ },
111
+ reportUnusedFallthroughComment: {
112
+ type: "boolean",
113
+ default: false
106
114
  }
107
115
  },
108
116
  additionalProperties: false
109
117
  }
110
118
  ],
111
119
  messages: {
120
+ unusedFallthroughComment: "Found a comment that would permit fallthrough, but case cannot fall through.",
112
121
  case: "Expected a 'break' statement before 'case'.",
113
122
  default: "Expected a 'break' statement before 'default'."
114
123
  }
@@ -120,12 +129,13 @@ module.exports = {
120
129
  let currentCodePathSegments = new Set();
121
130
  const sourceCode = context.sourceCode;
122
131
  const allowEmptyCase = options.allowEmptyCase || false;
132
+ const reportUnusedFallthroughComment = options.reportUnusedFallthroughComment || false;
123
133
 
124
134
  /*
125
135
  * We need to use leading comments of the next SwitchCase node because
126
136
  * trailing comments is wrong if semicolons are omitted.
127
137
  */
128
- let fallthroughCase = null;
138
+ let previousCase = null;
129
139
  let fallthroughCommentPattern = null;
130
140
 
131
141
  if (options.commentPattern) {
@@ -168,13 +178,23 @@ module.exports = {
168
178
  * And reports the previous fallthrough node if that does not exist.
169
179
  */
170
180
 
171
- if (fallthroughCase && (!hasFallthroughComment(fallthroughCase, node, context, fallthroughCommentPattern))) {
172
- context.report({
173
- messageId: node.test ? "case" : "default",
174
- node
175
- });
181
+ if (previousCase && previousCase.node.parent === node.parent) {
182
+ const previousCaseFallthroughComment = getFallthroughComment(previousCase.node, node, context, fallthroughCommentPattern);
183
+
184
+ if (previousCase.isFallthrough && !(previousCaseFallthroughComment)) {
185
+ context.report({
186
+ messageId: node.test ? "case" : "default",
187
+ node
188
+ });
189
+ } else if (reportUnusedFallthroughComment && !previousCase.isSwitchExitReachable && previousCaseFallthroughComment) {
190
+ context.report({
191
+ messageId: "unusedFallthroughComment",
192
+ node: previousCaseFallthroughComment
193
+ });
194
+ }
195
+
176
196
  }
177
- fallthroughCase = null;
197
+ previousCase = null;
178
198
  },
179
199
 
180
200
  "SwitchCase:exit"(node) {
@@ -185,11 +205,16 @@ module.exports = {
185
205
  * `break`, `return`, or `throw` are unreachable.
186
206
  * And allows empty cases and the last case.
187
207
  */
188
- if (isAnySegmentReachable(currentCodePathSegments) &&
189
- (node.consequent.length > 0 || (!allowEmptyCase && hasBlankLinesBetween(node, nextToken))) &&
190
- node.parent.cases.at(-1) !== node) {
191
- fallthroughCase = node;
192
- }
208
+ const isSwitchExitReachable = isAnySegmentReachable(currentCodePathSegments);
209
+ const isFallthrough = isSwitchExitReachable && (node.consequent.length > 0 || (!allowEmptyCase && hasBlankLinesBetween(node, nextToken))) &&
210
+ node.parent.cases.at(-1) !== node;
211
+
212
+ previousCase = {
213
+ node,
214
+ isSwitchExitReachable,
215
+ isFallthrough
216
+ };
217
+
193
218
  }
194
219
  };
195
220
  }