Skip to content

Commit

Permalink
feat: add reportUnusedFallthroughComment option to no-fallthrough r…
Browse files Browse the repository at this point in the history
…ule (#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 <milos.djermanovic@gmail.com>

* add some test coverage

---------

Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com>
  • Loading branch information
kirkwaiblinger and mdjermanovic committed Mar 14, 2024
1 parent ae8103d commit b8fb572
Show file tree
Hide file tree
Showing 3 changed files with 334 additions and 16 deletions.
56 changes: 56 additions & 0 deletions docs/src/rules/no-fallthrough.md
Expand Up @@ -178,6 +178,8 @@ This rule has an object option:

* 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.

* 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.

### commentPattern

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

:::

### reportUnusedFallthroughComment

Examples of **incorrect** code for the `{ "reportUnusedFallthroughComment": true }` option:

::: incorrect

```js
/* eslint no-fallthrough: ["error", { "reportUnusedFallthroughComment": true }] */

switch(foo){
case 1:
doSomething();
break;
// falls through
case 2: doSomething();
}

function f() {
switch(foo){
case 1:
if (a) {
throw new Error();
} else if (b) {
break;
} else {
return;
}
// falls through
case 2:
break;
}
}
```

:::

Examples of **correct** code for the `{ "reportUnusedFallthroughComment": true }` option:

::: correct

```js
/* eslint no-fallthrough: ["error", { "reportUnusedFallthroughComment": true }] */

switch(foo){
case 1:
doSomething();
break;
// just a comment
case 2: doSomething();
}
```

:::

## When Not To Use It

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.
57 changes: 41 additions & 16 deletions lib/rules/no-fallthrough.js
Expand Up @@ -48,23 +48,27 @@ function isFallThroughComment(comment, fallthroughCommentPattern) {
* @param {ASTNode} subsequentCase The case after caseWhichFallsThrough.
* @param {RuleContext} context A rule context which stores comments.
* @param {RegExp} fallthroughCommentPattern A pattern to match comment to.
* @returns {boolean} `true` if the case has a valid fallthrough comment.
* @returns {null | object} the comment if the case has a valid fallthrough comment, otherwise null
*/
function hasFallthroughComment(caseWhichFallsThrough, subsequentCase, context, fallthroughCommentPattern) {
function getFallthroughComment(caseWhichFallsThrough, subsequentCase, context, fallthroughCommentPattern) {
const sourceCode = context.sourceCode;

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

if (commentInBlock && isFallThroughComment(commentInBlock.value, fallthroughCommentPattern)) {
return true;
return commentInBlock;
}
}

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

return Boolean(comment && isFallThroughComment(comment.value, fallthroughCommentPattern));
if (comment && isFallThroughComment(comment.value, fallthroughCommentPattern)) {
return comment;
}

return null;
}

/**
Expand Down Expand Up @@ -103,12 +107,17 @@ module.exports = {
allowEmptyCase: {
type: "boolean",
default: false
},
reportUnusedFallthroughComment: {
type: "boolean",
default: false
}
},
additionalProperties: false
}
],
messages: {
unusedFallthroughComment: "Found a comment that would permit fallthrough, but case cannot fall through.",
case: "Expected a 'break' statement before 'case'.",
default: "Expected a 'break' statement before 'default'."
}
Expand All @@ -120,12 +129,13 @@ module.exports = {
let currentCodePathSegments = new Set();
const sourceCode = context.sourceCode;
const allowEmptyCase = options.allowEmptyCase || false;
const reportUnusedFallthroughComment = options.reportUnusedFallthroughComment || false;

/*
* We need to use leading comments of the next SwitchCase node because
* trailing comments is wrong if semicolons are omitted.
*/
let fallthroughCase = null;
let previousCase = null;
let fallthroughCommentPattern = null;

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

if (fallthroughCase && (!hasFallthroughComment(fallthroughCase, node, context, fallthroughCommentPattern))) {
context.report({
messageId: node.test ? "case" : "default",
node
});
if (previousCase && previousCase.node.parent === node.parent) {
const previousCaseFallthroughComment = getFallthroughComment(previousCase.node, node, context, fallthroughCommentPattern);

if (previousCase.isFallthrough && !(previousCaseFallthroughComment)) {
context.report({
messageId: node.test ? "case" : "default",
node
});
} else if (reportUnusedFallthroughComment && !previousCase.isSwitchExitReachable && previousCaseFallthroughComment) {
context.report({
messageId: "unusedFallthroughComment",
node: previousCaseFallthroughComment
});
}

}
fallthroughCase = null;
previousCase = null;
},

"SwitchCase:exit"(node) {
Expand All @@ -185,11 +205,16 @@ module.exports = {
* `break`, `return`, or `throw` are unreachable.
* And allows empty cases and the last case.
*/
if (isAnySegmentReachable(currentCodePathSegments) &&
(node.consequent.length > 0 || (!allowEmptyCase && hasBlankLinesBetween(node, nextToken))) &&
node.parent.cases.at(-1) !== node) {
fallthroughCase = node;
}
const isSwitchExitReachable = isAnySegmentReachable(currentCodePathSegments);
const isFallthrough = isSwitchExitReachable && (node.consequent.length > 0 || (!allowEmptyCase && hasBlankLinesBetween(node, nextToken))) &&
node.parent.cases.at(-1) !== node;

previousCase = {
node,
isSwitchExitReachable,
isFallthrough
};

}
};
}
Expand Down

0 comments on commit b8fb572

Please sign in to comment.