no-fallthrough.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. /**
  2. * @fileoverview Rule to flag fall-through cases in switch statements.
  3. * @author Matt DuVall <http://mattduvall.com/>
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const { directivesPattern } = require("../shared/directives");
  10. //------------------------------------------------------------------------------
  11. // Helpers
  12. //------------------------------------------------------------------------------
  13. const DEFAULT_FALLTHROUGH_COMMENT = /falls?\s?through/iu;
  14. /**
  15. * Checks whether or not a given comment string is really a fallthrough comment and not an ESLint directive.
  16. * @param {string} comment The comment string to check.
  17. * @param {RegExp} fallthroughCommentPattern The regular expression used for checking for fallthrough comments.
  18. * @returns {boolean} `true` if the comment string is truly a fallthrough comment.
  19. */
  20. function isFallThroughComment(comment, fallthroughCommentPattern) {
  21. return fallthroughCommentPattern.test(comment) && !directivesPattern.test(comment.trim());
  22. }
  23. /**
  24. * Checks whether or not a given case has a fallthrough comment.
  25. * @param {ASTNode} caseWhichFallsThrough SwitchCase node which falls through.
  26. * @param {ASTNode} subsequentCase The case after caseWhichFallsThrough.
  27. * @param {RuleContext} context A rule context which stores comments.
  28. * @param {RegExp} fallthroughCommentPattern A pattern to match comment to.
  29. * @returns {boolean} `true` if the case has a valid fallthrough comment.
  30. */
  31. function hasFallthroughComment(caseWhichFallsThrough, subsequentCase, context, fallthroughCommentPattern) {
  32. const sourceCode = context.sourceCode;
  33. if (caseWhichFallsThrough.consequent.length === 1 && caseWhichFallsThrough.consequent[0].type === "BlockStatement") {
  34. const trailingCloseBrace = sourceCode.getLastToken(caseWhichFallsThrough.consequent[0]);
  35. const commentInBlock = sourceCode.getCommentsBefore(trailingCloseBrace).pop();
  36. if (commentInBlock && isFallThroughComment(commentInBlock.value, fallthroughCommentPattern)) {
  37. return true;
  38. }
  39. }
  40. const comment = sourceCode.getCommentsBefore(subsequentCase).pop();
  41. return Boolean(comment && isFallThroughComment(comment.value, fallthroughCommentPattern));
  42. }
  43. /**
  44. * Checks whether or not a given code path segment is reachable.
  45. * @param {CodePathSegment} segment A CodePathSegment to check.
  46. * @returns {boolean} `true` if the segment is reachable.
  47. */
  48. function isReachable(segment) {
  49. return segment.reachable;
  50. }
  51. /**
  52. * Checks whether a node and a token are separated by blank lines
  53. * @param {ASTNode} node The node to check
  54. * @param {Token} token The token to compare against
  55. * @returns {boolean} `true` if there are blank lines between node and token
  56. */
  57. function hasBlankLinesBetween(node, token) {
  58. return token.loc.start.line > node.loc.end.line + 1;
  59. }
  60. //------------------------------------------------------------------------------
  61. // Rule Definition
  62. //------------------------------------------------------------------------------
  63. /** @type {import('../shared/types').Rule} */
  64. module.exports = {
  65. meta: {
  66. type: "problem",
  67. docs: {
  68. description: "Disallow fallthrough of `case` statements",
  69. recommended: true,
  70. url: "https://eslint.org/docs/latest/rules/no-fallthrough"
  71. },
  72. schema: [
  73. {
  74. type: "object",
  75. properties: {
  76. commentPattern: {
  77. type: "string",
  78. default: ""
  79. },
  80. allowEmptyCase: {
  81. type: "boolean",
  82. default: false
  83. }
  84. },
  85. additionalProperties: false
  86. }
  87. ],
  88. messages: {
  89. case: "Expected a 'break' statement before 'case'.",
  90. default: "Expected a 'break' statement before 'default'."
  91. }
  92. },
  93. create(context) {
  94. const options = context.options[0] || {};
  95. let currentCodePath = null;
  96. const sourceCode = context.sourceCode;
  97. const allowEmptyCase = options.allowEmptyCase || false;
  98. /*
  99. * We need to use leading comments of the next SwitchCase node because
  100. * trailing comments is wrong if semicolons are omitted.
  101. */
  102. let fallthroughCase = null;
  103. let fallthroughCommentPattern = null;
  104. if (options.commentPattern) {
  105. fallthroughCommentPattern = new RegExp(options.commentPattern, "u");
  106. } else {
  107. fallthroughCommentPattern = DEFAULT_FALLTHROUGH_COMMENT;
  108. }
  109. return {
  110. onCodePathStart(codePath) {
  111. currentCodePath = codePath;
  112. },
  113. onCodePathEnd() {
  114. currentCodePath = currentCodePath.upper;
  115. },
  116. SwitchCase(node) {
  117. /*
  118. * Checks whether or not there is a fallthrough comment.
  119. * And reports the previous fallthrough node if that does not exist.
  120. */
  121. if (fallthroughCase && (!hasFallthroughComment(fallthroughCase, node, context, fallthroughCommentPattern))) {
  122. context.report({
  123. messageId: node.test ? "case" : "default",
  124. node
  125. });
  126. }
  127. fallthroughCase = null;
  128. },
  129. "SwitchCase:exit"(node) {
  130. const nextToken = sourceCode.getTokenAfter(node);
  131. /*
  132. * `reachable` meant fall through because statements preceded by
  133. * `break`, `return`, or `throw` are unreachable.
  134. * And allows empty cases and the last case.
  135. */
  136. if (currentCodePath.currentSegments.some(isReachable) &&
  137. (node.consequent.length > 0 || (!allowEmptyCase && hasBlankLinesBetween(node, nextToken))) &&
  138. node.parent.cases[node.parent.cases.length - 1] !== node) {
  139. fallthroughCase = node;
  140. }
  141. }
  142. };
  143. }
  144. };