2 * @fileoverview Rule to flag `else` after a `return` in `if`
3 * @author Ian Christian Myers
8 //------------------------------------------------------------------------------
10 //------------------------------------------------------------------------------
12 const astUtils = require("./utils/ast-utils");
13 const FixTracker = require("./utils/fix-tracker");
15 //------------------------------------------------------------------------------
17 //------------------------------------------------------------------------------
24 description: "disallow `else` blocks after `return` statements in `if` statements",
25 category: "Best Practices",
27 url: "https://eslint.org/docs/rules/no-else-return"
38 additionalProperties: false
44 unexpected: "Unnecessary 'else' after 'return'."
50 //--------------------------------------------------------------------------
52 //--------------------------------------------------------------------------
55 * Checks whether the given names can be safely used to declare block-scoped variables
56 * in the given scope. Name collisions can produce redeclaration syntax errors,
57 * or silently change references and modify behavior of the original code.
59 * This is not a generic function. In particular, it is assumed that the scope is a function scope or
60 * a function's inner scope, and that the names can be valid identifiers in the given scope.
61 * @param {string[]} names Array of variable names.
62 * @param {eslint-scope.Scope} scope Function scope or a function's inner scope.
63 * @returns {boolean} True if all names can be safely declared, false otherwise.
65 function isSafeToDeclare(names, scope) {
67 if (names.length === 0) {
71 const functionScope = scope.variableScope;
74 * If this is a function scope, scope.variables will contain parameters, implicit variables such as "arguments",
75 * all function-scoped variables ('var'), and block-scoped variables defined in the scope.
76 * If this is an inner scope, scope.variables will contain block-scoped variables defined in the scope.
78 * Redeclaring any of these would cause a syntax error, except for the implicit variables.
80 const declaredVariables = scope.variables.filter(({ defs }) => defs.length > 0);
82 if (declaredVariables.some(({ name }) => names.includes(name))) {
86 // Redeclaring a catch variable would also cause a syntax error.
87 if (scope !== functionScope && scope.upper.type === "catch") {
88 if (scope.upper.variables.some(({ name }) => names.includes(name))) {
94 * Redeclaring an implicit variable, such as "arguments", would not cause a syntax error.
95 * However, if the variable was used, declaring a new one with the same name would change references
96 * and modify behavior.
98 const usedImplicitVariables = scope.variables.filter(({ defs, references }) =>
99 defs.length === 0 && references.length > 0);
101 if (usedImplicitVariables.some(({ name }) => names.includes(name))) {
106 * Declaring a variable with a name that was already used to reference a variable from an upper scope
107 * would change references and modify behavior.
109 if (scope.through.some(t => names.includes(t.identifier.name))) {
114 * If the scope is an inner scope (not the function scope), an uninitialized `var` variable declared inside
115 * the scope node (directly or in one of its descendants) is neither declared nor 'through' in the scope.
117 * For example, this would be a syntax error "Identifier 'a' has already been declared":
118 * function foo() { if (bar) { let a; if (baz) { var a; } } }
120 if (scope !== functionScope) {
121 const scopeNodeRange = scope.block.range;
122 const variablesToCheck = functionScope.variables.filter(({ name }) => names.includes(name));
124 if (variablesToCheck.some(v => v.defs.some(({ node: { range } }) =>
125 scopeNodeRange[0] <= range[0] && range[1] <= scopeNodeRange[1]))) {
135 * Checks whether the removal of `else` and its braces is safe from variable name collisions.
136 * @param {Node} node The 'else' node.
137 * @param {eslint-scope.Scope} scope The scope in which the node and the whole 'if' statement is.
138 * @returns {boolean} True if it is safe, false otherwise.
140 function isSafeFromNameCollisions(node, scope) {
142 if (node.type === "FunctionDeclaration") {
144 // Conditional function declaration. Scope and hoisting are unpredictable, different engines work differently.
148 if (node.type !== "BlockStatement") {
152 const elseBlockScope = scope.childScopes.find(({ block }) => block === node);
154 if (!elseBlockScope) {
156 // ecmaVersion < 6, `else` block statement cannot have its own scope, no possible collisions.
161 * elseBlockScope is supposed to merge into its upper scope. elseBlockScope.variables array contains
162 * only block-scoped variables (such as let and const variables or class and function declarations)
163 * defined directly in the elseBlockScope. These are exactly the only names that could cause collisions.
165 const namesToCheck = elseBlockScope.variables.map(({ name }) => name);
167 return isSafeToDeclare(namesToCheck, scope);
171 * Display the context report if rule is violated
172 * @param {Node} node The 'else' node
175 function displayReport(node) {
176 const currentScope = context.getScope();
180 messageId: "unexpected",
183 if (!isSafeFromNameCollisions(node, currentScope)) {
187 const sourceCode = context.getSourceCode();
188 const startToken = sourceCode.getFirstToken(node);
189 const elseToken = sourceCode.getTokenBefore(startToken);
190 const source = sourceCode.getText(node);
191 const lastIfToken = sourceCode.getTokenBefore(elseToken);
192 let fixedSource, firstTokenOfElseBlock;
194 if (startToken.type === "Punctuator" && startToken.value === "{") {
195 firstTokenOfElseBlock = sourceCode.getTokenAfter(startToken);
197 firstTokenOfElseBlock = startToken;
201 * If the if block does not have curly braces and does not end in a semicolon
202 * and the else block starts with (, [, /, +, ` or -, then it is not
203 * safe to remove the else keyword, because ASI will not add a semicolon
206 const ifBlockMaybeUnsafe = node.parent.consequent.type !== "BlockStatement" && lastIfToken.value !== ";";
207 const elseBlockUnsafe = /^[([/+`-]/u.test(firstTokenOfElseBlock.value);
209 if (ifBlockMaybeUnsafe && elseBlockUnsafe) {
213 const endToken = sourceCode.getLastToken(node);
214 const lastTokenOfElseBlock = sourceCode.getTokenBefore(endToken);
216 if (lastTokenOfElseBlock.value !== ";") {
217 const nextToken = sourceCode.getTokenAfter(endToken);
219 const nextTokenUnsafe = nextToken && /^[([/+`-]/u.test(nextToken.value);
220 const nextTokenOnSameLine = nextToken && nextToken.loc.start.line === lastTokenOfElseBlock.loc.start.line;
223 * If the else block contents does not end in a semicolon,
224 * and the else block starts with (, [, /, +, ` or -, then it is not
225 * safe to remove the else block, because ASI will not add a semicolon
226 * after the remaining else block contents
228 if (nextTokenUnsafe || (nextTokenOnSameLine && nextToken.value !== "}")) {
233 if (startToken.type === "Punctuator" && startToken.value === "{") {
234 fixedSource = source.slice(1, -1);
236 fixedSource = source;
240 * Extend the replacement range to include the entire
241 * function to avoid conflicting with no-useless-return.
242 * https://github.com/eslint/eslint/issues/8026
244 * Also, to avoid name collisions between two else blocks.
246 return new FixTracker(fixer, sourceCode)
247 .retainEnclosingFunction(node)
248 .replaceTextRange([elseToken.range[0], node.range[1]], fixedSource);
254 * Check to see if the node is a ReturnStatement
255 * @param {Node} node The node being evaluated
256 * @returns {boolean} True if node is a return
258 function checkForReturn(node) {
259 return node.type === "ReturnStatement";
263 * Naive return checking, does not iterate through the whole
264 * BlockStatement because we make the assumption that the ReturnStatement
265 * will be the last node in the body of the BlockStatement.
266 * @param {Node} node The consequent/alternate node
267 * @returns {boolean} True if it has a return
269 function naiveHasReturn(node) {
270 if (node.type === "BlockStatement") {
271 const body = node.body,
272 lastChildNode = body[body.length - 1];
274 return lastChildNode && checkForReturn(lastChildNode);
276 return checkForReturn(node);
280 * Check to see if the node is valid for evaluation,
281 * meaning it has an else.
282 * @param {Node} node The node being evaluated
283 * @returns {boolean} True if the node is valid
285 function hasElse(node) {
286 return node.alternate && node.consequent;
290 * If the consequent is an IfStatement, check to see if it has an else
291 * and both its consequent and alternate path return, meaning this is
292 * a nested case of rule violation. If-Else not considered currently.
293 * @param {Node} node The consequent node
294 * @returns {boolean} True if this is a nested rule violation
296 function checkForIf(node) {
297 return node.type === "IfStatement" && hasElse(node) &&
298 naiveHasReturn(node.alternate) && naiveHasReturn(node.consequent);
302 * Check the consequent/body node to make sure it is not
303 * a ReturnStatement or an IfStatement that returns on both
305 * @param {Node} node The consequent or body node
306 * @returns {boolean} `true` if it is a Return/If node that always returns.
308 function checkForReturnOrIf(node) {
309 return checkForReturn(node) || checkForIf(node);
314 * Check whether a node returns in every codepath.
315 * @param {Node} node The node to be checked
316 * @returns {boolean} `true` if it returns on every codepath.
318 function alwaysReturns(node) {
319 if (node.type === "BlockStatement") {
321 // If we have a BlockStatement, check each consequent body node.
322 return node.body.some(checkForReturnOrIf);
326 * If not a block statement, make sure the consequent isn't a
327 * ReturnStatement or an IfStatement with returns on both paths.
329 return checkForReturnOrIf(node);
334 * Check the if statement, but don't catch else-if blocks.
336 * @param {Node} node The node for the if statement to check
339 function checkIfWithoutElse(node) {
340 const parent = node.parent;
343 * Fixing this would require splitting one statement into two, so no error should
344 * be reported if this node is in a position where only one statement is allowed.
346 if (!astUtils.STATEMENT_LIST_PARENTS.has(parent.type)) {
350 const consequents = [];
353 for (let currentNode = node; currentNode.type === "IfStatement"; currentNode = currentNode.alternate) {
354 if (!currentNode.alternate) {
357 consequents.push(currentNode.consequent);
358 alternate = currentNode.alternate;
361 if (consequents.every(alwaysReturns)) {
362 displayReport(alternate);
367 * Check the if statement
369 * @param {Node} node The node for the if statement to check
372 function checkIfWithElse(node) {
373 const parent = node.parent;
377 * Fixing this would require splitting one statement into two, so no error should
378 * be reported if this node is in a position where only one statement is allowed.
380 if (!astUtils.STATEMENT_LIST_PARENTS.has(parent.type)) {
384 const alternate = node.alternate;
386 if (alternate && alwaysReturns(node.consequent)) {
387 displayReport(alternate);
391 const allowElseIf = !(context.options[0] && context.options[0].allowElseIf === false);
393 //--------------------------------------------------------------------------
395 //--------------------------------------------------------------------------
399 "IfStatement:exit": allowElseIf ? checkIfWithoutElse : checkIfWithElse