197 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			197 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /**
 | |
|  * @fileoverview Rule to flag fall-through cases in switch statements.
 | |
|  * @author Matt DuVall <http://mattduvall.com/>
 | |
|  */
 | |
| "use strict";
 | |
| 
 | |
| //------------------------------------------------------------------------------
 | |
| // Requirements
 | |
| //------------------------------------------------------------------------------
 | |
| 
 | |
| const { directivesPattern } = require("../shared/directives");
 | |
| 
 | |
| //------------------------------------------------------------------------------
 | |
| // Helpers
 | |
| //------------------------------------------------------------------------------
 | |
| 
 | |
| const DEFAULT_FALLTHROUGH_COMMENT = /falls?\s?through/iu;
 | |
| 
 | |
| /**
 | |
|  * Checks all segments in a set and returns true if any are reachable.
 | |
|  * @param {Set<CodePathSegment>} segments The segments to check.
 | |
|  * @returns {boolean} True if any segment is reachable; false otherwise.
 | |
|  */
 | |
| function isAnySegmentReachable(segments) {
 | |
| 
 | |
|     for (const segment of segments) {
 | |
|         if (segment.reachable) {
 | |
|             return true;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     return false;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Checks whether or not a given comment string is really a fallthrough comment and not an ESLint directive.
 | |
|  * @param {string} comment The comment string to check.
 | |
|  * @param {RegExp} fallthroughCommentPattern The regular expression used for checking for fallthrough comments.
 | |
|  * @returns {boolean} `true` if the comment string is truly a fallthrough comment.
 | |
|  */
 | |
| function isFallThroughComment(comment, fallthroughCommentPattern) {
 | |
|     return fallthroughCommentPattern.test(comment) && !directivesPattern.test(comment.trim());
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Checks whether or not a given case has a fallthrough comment.
 | |
|  * @param {ASTNode} caseWhichFallsThrough SwitchCase node which falls through.
 | |
|  * @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.
 | |
|  */
 | |
| function hasFallthroughComment(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;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     const comment = sourceCode.getCommentsBefore(subsequentCase).pop();
 | |
| 
 | |
|     return Boolean(comment && isFallThroughComment(comment.value, fallthroughCommentPattern));
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Checks whether a node and a token are separated by blank lines
 | |
|  * @param {ASTNode} node The node to check
 | |
|  * @param {Token} token The token to compare against
 | |
|  * @returns {boolean} `true` if there are blank lines between node and token
 | |
|  */
 | |
| function hasBlankLinesBetween(node, token) {
 | |
|     return token.loc.start.line > node.loc.end.line + 1;
 | |
| }
 | |
| 
 | |
| //------------------------------------------------------------------------------
 | |
| // Rule Definition
 | |
| //------------------------------------------------------------------------------
 | |
| 
 | |
| /** @type {import('../shared/types').Rule} */
 | |
| module.exports = {
 | |
|     meta: {
 | |
|         type: "problem",
 | |
| 
 | |
|         docs: {
 | |
|             description: "Disallow fallthrough of `case` statements",
 | |
|             recommended: true,
 | |
|             url: "https://eslint.org/docs/latest/rules/no-fallthrough"
 | |
|         },
 | |
| 
 | |
|         schema: [
 | |
|             {
 | |
|                 type: "object",
 | |
|                 properties: {
 | |
|                     commentPattern: {
 | |
|                         type: "string",
 | |
|                         default: ""
 | |
|                     },
 | |
|                     allowEmptyCase: {
 | |
|                         type: "boolean",
 | |
|                         default: false
 | |
|                     }
 | |
|                 },
 | |
|                 additionalProperties: false
 | |
|             }
 | |
|         ],
 | |
|         messages: {
 | |
|             case: "Expected a 'break' statement before 'case'.",
 | |
|             default: "Expected a 'break' statement before 'default'."
 | |
|         }
 | |
|     },
 | |
| 
 | |
|     create(context) {
 | |
|         const options = context.options[0] || {};
 | |
|         const codePathSegments = [];
 | |
|         let currentCodePathSegments = new Set();
 | |
|         const sourceCode = context.sourceCode;
 | |
|         const allowEmptyCase = options.allowEmptyCase || 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 fallthroughCommentPattern = null;
 | |
| 
 | |
|         if (options.commentPattern) {
 | |
|             fallthroughCommentPattern = new RegExp(options.commentPattern, "u");
 | |
|         } else {
 | |
|             fallthroughCommentPattern = DEFAULT_FALLTHROUGH_COMMENT;
 | |
|         }
 | |
|         return {
 | |
| 
 | |
|             onCodePathStart() {
 | |
|                 codePathSegments.push(currentCodePathSegments);
 | |
|                 currentCodePathSegments = new Set();
 | |
|             },
 | |
| 
 | |
|             onCodePathEnd() {
 | |
|                 currentCodePathSegments = codePathSegments.pop();
 | |
|             },
 | |
| 
 | |
|             onUnreachableCodePathSegmentStart(segment) {
 | |
|                 currentCodePathSegments.add(segment);
 | |
|             },
 | |
| 
 | |
|             onUnreachableCodePathSegmentEnd(segment) {
 | |
|                 currentCodePathSegments.delete(segment);
 | |
|             },
 | |
| 
 | |
|             onCodePathSegmentStart(segment) {
 | |
|                 currentCodePathSegments.add(segment);
 | |
|             },
 | |
| 
 | |
|             onCodePathSegmentEnd(segment) {
 | |
|                 currentCodePathSegments.delete(segment);
 | |
|             },
 | |
| 
 | |
| 
 | |
|             SwitchCase(node) {
 | |
| 
 | |
|                 /*
 | |
|                  * Checks whether or not there is a fallthrough comment.
 | |
|                  * 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
 | |
|                     });
 | |
|                 }
 | |
|                 fallthroughCase = null;
 | |
|             },
 | |
| 
 | |
|             "SwitchCase:exit"(node) {
 | |
|                 const nextToken = sourceCode.getTokenAfter(node);
 | |
| 
 | |
|                 /*
 | |
|                  * `reachable` meant fall through because statements preceded by
 | |
|                  * `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[node.parent.cases.length - 1] !== node) {
 | |
|                     fallthroughCase = node;
 | |
|                 }
 | |
|             }
 | |
|         };
 | |
|     }
 | |
| };
 |