Skip to content

Commit b8fb572

Browse files
feat: add reportUnusedFallthroughComment option to no-fallthrough rule (#18188)
* feat: (no-fallthrough) Report unused fallthrough comments fixes #18182 * add space * add a few test cases to ensure state doesn't leak across multiple switches * add correct case in docs * fix leaked state * Fix docs typo Co-authored-by: Milos Djermanovic <[email protected]> * add some test coverage --------- Co-authored-by: Milos Djermanovic <[email protected]>
1 parent ae8103d commit b8fb572

File tree

3 files changed

+334
-16
lines changed

3 files changed

+334
-16
lines changed

docs/src/rules/no-fallthrough.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,8 @@ This rule has an object option:
178178

179179
* Set the `allowEmptyCase` option to `true` to allow empty cases regardless of the layout. By default, this rule does not require a fallthrough comment after an empty `case` only if the empty `case` and the next `case` are on the same line or on consecutive lines.
180180

181+
* Set the `reportUnusedFallthroughComment` option to `true` to prohibit a fallthrough comment from being present if the case cannot fallthrough due to being unreachable. This is mostly intended to help avoid misleading comments occurring as a result of refactoring.
182+
181183
### commentPattern
182184

183185
Examples of **correct** code for the `{ "commentPattern": "break[\\s\\w]*omitted" }` option:
@@ -235,6 +237,60 @@ switch(foo){
235237

236238
:::
237239

240+
### reportUnusedFallthroughComment
241+
242+
Examples of **incorrect** code for the `{ "reportUnusedFallthroughComment": true }` option:
243+
244+
::: incorrect
245+
246+
```js
247+
/* eslint no-fallthrough: ["error", { "reportUnusedFallthroughComment": true }] */
248+
249+
switch(foo){
250+
case 1:
251+
doSomething();
252+
break;
253+
// falls through
254+
case 2: doSomething();
255+
}
256+
257+
function f() {
258+
switch(foo){
259+
case 1:
260+
if (a) {
261+
throw new Error();
262+
} else if (b) {
263+
break;
264+
} else {
265+
return;
266+
}
267+
// falls through
268+
case 2:
269+
break;
270+
}
271+
}
272+
```
273+
274+
:::
275+
276+
Examples of **correct** code for the `{ "reportUnusedFallthroughComment": true }` option:
277+
278+
::: correct
279+
280+
```js
281+
/* eslint no-fallthrough: ["error", { "reportUnusedFallthroughComment": true }] */
282+
283+
switch(foo){
284+
case 1:
285+
doSomething();
286+
break;
287+
// just a comment
288+
case 2: doSomething();
289+
}
290+
```
291+
292+
:::
293+
238294
## When Not To Use It
239295

240296
If you don't want to enforce that each `case` statement should end with a `throw`, `return`, `break`, or comment, then you can safely turn this rule off.

lib/rules/no-fallthrough.js

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -48,23 +48,27 @@ function isFallThroughComment(comment, fallthroughCommentPattern) {
4848
* @param {ASTNode} subsequentCase The case after caseWhichFallsThrough.
4949
* @param {RuleContext} context A rule context which stores comments.
5050
* @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
5252
*/
53-
function hasFallthroughComment(caseWhichFallsThrough, subsequentCase, context, fallthroughCommentPattern) {
53+
function getFallthroughComment(caseWhichFallsThrough, subsequentCase, context, fallthroughCommentPattern) {
5454
const sourceCode = context.sourceCode;
5555

5656
if (caseWhichFallsThrough.consequent.length === 1 && caseWhichFallsThrough.consequent[0].type === "BlockStatement") {
5757
const trailingCloseBrace = sourceCode.getLastToken(caseWhichFallsThrough.consequent[0]);
5858
const commentInBlock = sourceCode.getCommentsBefore(trailingCloseBrace).pop();
5959

6060
if (commentInBlock && isFallThroughComment(commentInBlock.value, fallthroughCommentPattern)) {
61-
return true;
61+
return commentInBlock;
6262
}
6363
}
6464

6565
const comment = sourceCode.getCommentsBefore(subsequentCase).pop();
6666

67-
return Boolean(comment && isFallThroughComment(comment.value, fallthroughCommentPattern));
67+
if (comment && isFallThroughComment(comment.value, fallthroughCommentPattern)) {
68+
return comment;
69+
}
70+
71+
return null;
6872
}
6973

7074
/**
@@ -103,12 +107,17 @@ module.exports = {
103107
allowEmptyCase: {
104108
type: "boolean",
105109
default: false
110+
},
111+
reportUnusedFallthroughComment: {
112+
type: "boolean",
113+
default: false
106114
}
107115
},
108116
additionalProperties: false
109117
}
110118
],
111119
messages: {
120+
unusedFallthroughComment: "Found a comment that would permit fallthrough, but case cannot fall through.",
112121
case: "Expected a 'break' statement before 'case'.",
113122
default: "Expected a 'break' statement before 'default'."
114123
}
@@ -120,12 +129,13 @@ module.exports = {
120129
let currentCodePathSegments = new Set();
121130
const sourceCode = context.sourceCode;
122131
const allowEmptyCase = options.allowEmptyCase || false;
132+
const reportUnusedFallthroughComment = options.reportUnusedFallthroughComment || false;
123133

124134
/*
125135
* We need to use leading comments of the next SwitchCase node because
126136
* trailing comments is wrong if semicolons are omitted.
127137
*/
128-
let fallthroughCase = null;
138+
let previousCase = null;
129139
let fallthroughCommentPattern = null;
130140

131141
if (options.commentPattern) {
@@ -168,13 +178,23 @@ module.exports = {
168178
* And reports the previous fallthrough node if that does not exist.
169179
*/
170180

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+
176196
}
177-
fallthroughCase = null;
197+
previousCase = null;
178198
},
179199

180200
"SwitchCase:exit"(node) {
@@ -185,11 +205,16 @@ module.exports = {
185205
* `break`, `return`, or `throw` are unreachable.
186206
* And allows empty cases and the last case.
187207
*/
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+
193218
}
194219
};
195220
}

0 commit comments

Comments
 (0)