UNPKG

10.5 kBJavaScriptView Raw
1/**
2 * @fileoverview Rule to flag `else` after a `return` in `if`
3 * @author Ian Christian Myers
4 */
5
6"use strict";
7
8//------------------------------------------------------------------------------
9// Requirements
10//------------------------------------------------------------------------------
11
12const astUtils = require("../util/ast-utils");
13const FixTracker = require("../util/fix-tracker");
14
15//------------------------------------------------------------------------------
16// Rule Definition
17//------------------------------------------------------------------------------
18
19module.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};