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