1 | /**
|
2 | * @fileoverview Rule to flag `else` after a `return` in `if`
|
3 | * @author Ian Christian Myers
|
4 | */
|
5 |
|
6 | ;
|
7 |
|
8 | //------------------------------------------------------------------------------
|
9 | // Requirements
|
10 | //------------------------------------------------------------------------------
|
11 |
|
12 | const astUtils = require("../util/ast-utils");
|
13 | const FixTracker = require("../util/fix-tracker");
|
14 |
|
15 | //------------------------------------------------------------------------------
|
16 | // Rule Definition
|
17 | //------------------------------------------------------------------------------
|
18 |
|
19 | module.exports = {
|
20 | meta: {
|
21 | type: "suggestion",
|
22 |
|
23 | docs: {
|
24 | description: "disallow `else` blocks after `return` statements in `if` statements",
|
25 | category: "Best Practices",
|
26 | recommended: false,
|
27 | url: "https://eslint.org/docs/rules/no-else-return"
|
28 | },
|
29 |
|
30 | schema: [{
|
31 | type: "object",
|
32 | properties: {
|
33 | allowElseIf: {
|
34 | type: "boolean",
|
35 | default: true
|
36 | }
|
37 | },
|
38 | additionalProperties: false
|
39 | }],
|
40 |
|
41 | fixable: "code",
|
42 |
|
43 | messages: {
|
44 | unexpected: "Unnecessary 'else' after 'return'."
|
45 | }
|
46 | },
|
47 |
|
48 | create(context) {
|
49 |
|
50 | //--------------------------------------------------------------------------
|
51 | // Helpers
|
52 | //--------------------------------------------------------------------------
|
53 |
|
54 | /**
|
55 | * Display the context report if rule is violated
|
56 | *
|
57 | * @param {Node} node The 'else' node
|
58 | * @returns {void}
|
59 | */
|
60 | function displayReport(node) {
|
61 | context.report({
|
62 | node,
|
63 | messageId: "unexpected",
|
64 | fix: fixer => {
|
65 | const sourceCode = context.getSourceCode();
|
66 | const startToken = sourceCode.getFirstToken(node);
|
67 | const elseToken = sourceCode.getTokenBefore(startToken);
|
68 | const source = sourceCode.getText(node);
|
69 | const lastIfToken = sourceCode.getTokenBefore(elseToken);
|
70 | let fixedSource, firstTokenOfElseBlock;
|
71 |
|
72 | if (startToken.type === "Punctuator" && startToken.value === "{") {
|
73 | firstTokenOfElseBlock = sourceCode.getTokenAfter(startToken);
|
74 | } else {
|
75 | firstTokenOfElseBlock = startToken;
|
76 | }
|
77 |
|
78 | /*
|
79 | * If the if block does not have curly braces and does not end in a semicolon
|
80 | * and the else block starts with (, [, /, +, ` or -, then it is not
|
81 | * safe to remove the else keyword, because ASI will not add a semicolon
|
82 | * after the if block
|
83 | */
|
84 | const ifBlockMaybeUnsafe = node.parent.consequent.type !== "BlockStatement" && lastIfToken.value !== ";";
|
85 | const elseBlockUnsafe = /^[([/+`-]/u.test(firstTokenOfElseBlock.value);
|
86 |
|
87 | if (ifBlockMaybeUnsafe && elseBlockUnsafe) {
|
88 | return null;
|
89 | }
|
90 |
|
91 | const endToken = sourceCode.getLastToken(node);
|
92 | const lastTokenOfElseBlock = sourceCode.getTokenBefore(endToken);
|
93 |
|
94 | if (lastTokenOfElseBlock.value !== ";") {
|
95 | const nextToken = sourceCode.getTokenAfter(endToken);
|
96 |
|
97 | const nextTokenUnsafe = nextToken && /^[([/+`-]/u.test(nextToken.value);
|
98 | const nextTokenOnSameLine = nextToken && nextToken.loc.start.line === lastTokenOfElseBlock.loc.start.line;
|
99 |
|
100 | /*
|
101 | * If the else block contents does not end in a semicolon,
|
102 | * and the else block starts with (, [, /, +, ` or -, then it is not
|
103 | * safe to remove the else block, because ASI will not add a semicolon
|
104 | * after the remaining else block contents
|
105 | */
|
106 | if (nextTokenUnsafe || (nextTokenOnSameLine && nextToken.value !== "}")) {
|
107 | return null;
|
108 | }
|
109 | }
|
110 |
|
111 | if (startToken.type === "Punctuator" && startToken.value === "{") {
|
112 | fixedSource = source.slice(1, -1);
|
113 | } else {
|
114 | fixedSource = source;
|
115 | }
|
116 |
|
117 | /*
|
118 | * Extend the replacement range to include the entire
|
119 | * function to avoid conflicting with no-useless-return.
|
120 | * https://github.com/eslint/eslint/issues/8026
|
121 | */
|
122 | return new FixTracker(fixer, sourceCode)
|
123 | .retainEnclosingFunction(node)
|
124 | .replaceTextRange([elseToken.range[0], node.range[1]], fixedSource);
|
125 | }
|
126 | });
|
127 | }
|
128 |
|
129 | /**
|
130 | * Check to see if the node is a ReturnStatement
|
131 | *
|
132 | * @param {Node} node The node being evaluated
|
133 | * @returns {boolean} True if node is a return
|
134 | */
|
135 | function checkForReturn(node) {
|
136 | return node.type === "ReturnStatement";
|
137 | }
|
138 |
|
139 | /**
|
140 | * Naive return checking, does not iterate through the whole
|
141 | * BlockStatement because we make the assumption that the ReturnStatement
|
142 | * will be the last node in the body of the BlockStatement.
|
143 | *
|
144 | * @param {Node} node The consequent/alternate node
|
145 | * @returns {boolean} True if it has a return
|
146 | */
|
147 | function naiveHasReturn(node) {
|
148 | if (node.type === "BlockStatement") {
|
149 | const body = node.body,
|
150 | lastChildNode = body[body.length - 1];
|
151 |
|
152 | return lastChildNode && checkForReturn(lastChildNode);
|
153 | }
|
154 | return checkForReturn(node);
|
155 | }
|
156 |
|
157 | /**
|
158 | * Check to see if the node is valid for evaluation,
|
159 | * meaning it has an else.
|
160 | *
|
161 | * @param {Node} node The node being evaluated
|
162 | * @returns {boolean} True if the node is valid
|
163 | */
|
164 | function hasElse(node) {
|
165 | return node.alternate && node.consequent;
|
166 | }
|
167 |
|
168 | /**
|
169 | * If the consequent is an IfStatement, check to see if it has an else
|
170 | * and both its consequent and alternate path return, meaning this is
|
171 | * a nested case of rule violation. If-Else not considered currently.
|
172 | *
|
173 | * @param {Node} node The consequent node
|
174 | * @returns {boolean} True if this is a nested rule violation
|
175 | */
|
176 | function checkForIf(node) {
|
177 | return node.type === "IfStatement" && hasElse(node) &&
|
178 | naiveHasReturn(node.alternate) && naiveHasReturn(node.consequent);
|
179 | }
|
180 |
|
181 | /**
|
182 | * Check the consequent/body node to make sure it is not
|
183 | * a ReturnStatement or an IfStatement that returns on both
|
184 | * code paths.
|
185 | *
|
186 | * @param {Node} node The consequent or body node
|
187 | * @returns {boolean} `true` if it is a Return/If node that always returns.
|
188 | */
|
189 | function checkForReturnOrIf(node) {
|
190 | return checkForReturn(node) || checkForIf(node);
|
191 | }
|
192 |
|
193 |
|
194 | /**
|
195 | * Check whether a node returns in every codepath.
|
196 | * @param {Node} node The node to be checked
|
197 | * @returns {boolean} `true` if it returns on every codepath.
|
198 | */
|
199 | function alwaysReturns(node) {
|
200 | if (node.type === "BlockStatement") {
|
201 |
|
202 | // If we have a BlockStatement, check each consequent body node.
|
203 | return node.body.some(checkForReturnOrIf);
|
204 | }
|
205 |
|
206 | /*
|
207 | * If not a block statement, make sure the consequent isn't a
|
208 | * ReturnStatement or an IfStatement with returns on both paths.
|
209 | */
|
210 | return checkForReturnOrIf(node);
|
211 | }
|
212 |
|
213 |
|
214 | /**
|
215 | * Check the if statement, but don't catch else-if blocks.
|
216 | * @returns {void}
|
217 | * @param {Node} node The node for the if statement to check
|
218 | * @private
|
219 | */
|
220 | function checkIfWithoutElse(node) {
|
221 | const parent = node.parent;
|
222 |
|
223 | /*
|
224 | * Fixing this would require splitting one statement into two, so no error should
|
225 | * be reported if this node is in a position where only one statement is allowed.
|
226 | */
|
227 | if (!astUtils.STATEMENT_LIST_PARENTS.has(parent.type)) {
|
228 | return;
|
229 | }
|
230 |
|
231 | const consequents = [];
|
232 | let alternate;
|
233 |
|
234 | for (let currentNode = node; currentNode.type === "IfStatement"; currentNode = currentNode.alternate) {
|
235 | if (!currentNode.alternate) {
|
236 | return;
|
237 | }
|
238 | consequents.push(currentNode.consequent);
|
239 | alternate = currentNode.alternate;
|
240 | }
|
241 |
|
242 | if (consequents.every(alwaysReturns)) {
|
243 | displayReport(alternate);
|
244 | }
|
245 | }
|
246 |
|
247 | /**
|
248 | * Check the if statement
|
249 | * @returns {void}
|
250 | * @param {Node} node The node for the if statement to check
|
251 | * @private
|
252 | */
|
253 | function checkIfWithElse(node) {
|
254 | const parent = node.parent;
|
255 |
|
256 |
|
257 | /*
|
258 | * Fixing this would require splitting one statement into two, so no error should
|
259 | * be reported if this node is in a position where only one statement is allowed.
|
260 | */
|
261 | if (!astUtils.STATEMENT_LIST_PARENTS.has(parent.type)) {
|
262 | return;
|
263 | }
|
264 |
|
265 | const alternate = node.alternate;
|
266 |
|
267 | if (alternate && alwaysReturns(node.consequent)) {
|
268 | displayReport(alternate);
|
269 | }
|
270 | }
|
271 |
|
272 | const allowElseIf = !(context.options[0] && context.options[0].allowElseIf === false);
|
273 |
|
274 | //--------------------------------------------------------------------------
|
275 | // Public API
|
276 | //--------------------------------------------------------------------------
|
277 |
|
278 | return {
|
279 |
|
280 | "IfStatement:exit": allowElseIf ? checkIfWithoutElse : checkIfWithElse
|
281 |
|
282 | };
|
283 |
|
284 | }
|
285 | };
|