blob: 46b5f4b648fc99c3ef95a2bdb04feef54d1a6b57 [file] [log] [blame]
/**
* @fileoverview Disallow redundant return statements
* @author Teddy Katz
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("./utils/ast-utils"),
FixTracker = require("./utils/fix-tracker");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Removes the given element from the array.
*
* @param {Array} array - The source array to remove.
* @param {any} element - The target item to remove.
* @returns {void}
*/
function remove(array, element) {
const index = array.indexOf(element);
if (index !== -1) {
array.splice(index, 1);
}
}
/**
* Checks whether it can remove the given return statement or not.
*
* @param {ASTNode} node - The return statement node to check.
* @returns {boolean} `true` if the node is removeable.
*/
function isRemovable(node) {
return astUtils.STATEMENT_LIST_PARENTS.has(node.parent.type);
}
/**
* Checks whether the given return statement is in a `finally` block or not.
*
* @param {ASTNode} node - The return statement node to check.
* @returns {boolean} `true` if the node is in a `finally` block.
*/
function isInFinally(node) {
for (
let currentNode = node;
currentNode && currentNode.parent && !astUtils.isFunction(currentNode);
currentNode = currentNode.parent
) {
if (currentNode.parent.type === "TryStatement" && currentNode.parent.finalizer === currentNode) {
return true;
}
}
return false;
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
type: "suggestion",
docs: {
description: "disallow redundant return statements",
category: "Best Practices",
recommended: false,
url: "https://eslint.org/docs/rules/no-useless-return"
},
fixable: "code",
schema: []
},
create(context) {
const segmentInfoMap = new WeakMap();
const usedUnreachableSegments = new WeakSet();
let scopeInfo = null;
/**
* Checks whether the given segment is terminated by a return statement or not.
*
* @param {CodePathSegment} segment - The segment to check.
* @returns {boolean} `true` if the segment is terminated by a return statement, or if it's still a part of unreachable.
*/
function isReturned(segment) {
const info = segmentInfoMap.get(segment);
return !info || info.returned;
}
/**
* Collects useless return statements from the given previous segments.
*
* A previous segment may be an unreachable segment.
* In that case, the information object of the unreachable segment is not
* initialized because `onCodePathSegmentStart` event is not notified for
* unreachable segments.
* This goes to the previous segments of the unreachable segment recursively
* if the unreachable segment was generated by a return statement. Otherwise,
* this ignores the unreachable segment.
*
* This behavior would simulate code paths for the case that the return
* statement does not exist.
*
* @param {ASTNode[]} uselessReturns - The collected return statements.
* @param {CodePathSegment[]} prevSegments - The previous segments to traverse.
* @param {WeakSet<CodePathSegment>} [providedTraversedSegments] A set of segments that have already been traversed in this call
* @returns {ASTNode[]} `uselessReturns`.
*/
function getUselessReturns(uselessReturns, prevSegments, providedTraversedSegments) {
const traversedSegments = providedTraversedSegments || new WeakSet();
for (const segment of prevSegments) {
if (!segment.reachable) {
if (!traversedSegments.has(segment)) {
traversedSegments.add(segment);
getUselessReturns(
uselessReturns,
segment.allPrevSegments.filter(isReturned),
traversedSegments
);
}
continue;
}
uselessReturns.push(...segmentInfoMap.get(segment).uselessReturns);
}
return uselessReturns;
}
/**
* Removes the return statements on the given segment from the useless return
* statement list.
*
* This segment may be an unreachable segment.
* In that case, the information object of the unreachable segment is not
* initialized because `onCodePathSegmentStart` event is not notified for
* unreachable segments.
* This goes to the previous segments of the unreachable segment recursively
* if the unreachable segment was generated by a return statement. Otherwise,
* this ignores the unreachable segment.
*
* This behavior would simulate code paths for the case that the return
* statement does not exist.
*
* @param {CodePathSegment} segment - The segment to get return statements.
* @returns {void}
*/
function markReturnStatementsOnSegmentAsUsed(segment) {
if (!segment.reachable) {
usedUnreachableSegments.add(segment);
segment.allPrevSegments
.filter(isReturned)
.filter(prevSegment => !usedUnreachableSegments.has(prevSegment))
.forEach(markReturnStatementsOnSegmentAsUsed);
return;
}
const info = segmentInfoMap.get(segment);
for (const node of info.uselessReturns) {
remove(scopeInfo.uselessReturns, node);
}
info.uselessReturns = [];
}
/**
* Removes the return statements on the current segments from the useless
* return statement list.
*
* This function will be called at every statement except FunctionDeclaration,
* BlockStatement, and BreakStatement.
*
* - FunctionDeclarations are always executed whether it's returned or not.
* - BlockStatements do nothing.
* - BreakStatements go the next merely.
*
* @returns {void}
*/
function markReturnStatementsOnCurrentSegmentsAsUsed() {
scopeInfo
.codePath
.currentSegments
.forEach(markReturnStatementsOnSegmentAsUsed);
}
//----------------------------------------------------------------------
// Public
//----------------------------------------------------------------------
return {
// Makes and pushs a new scope information.
onCodePathStart(codePath) {
scopeInfo = {
upper: scopeInfo,
uselessReturns: [],
codePath
};
},
// Reports useless return statements if exist.
onCodePathEnd() {
for (const node of scopeInfo.uselessReturns) {
context.report({
node,
loc: node.loc,
message: "Unnecessary return statement.",
fix(fixer) {
if (isRemovable(node)) {
/*
* Extend the replacement range to include the
* entire function to avoid conflicting with
* no-else-return.
* https://github.com/eslint/eslint/issues/8026
*/
return new FixTracker(fixer, context.getSourceCode())
.retainEnclosingFunction(node)
.remove(node);
}
return null;
}
});
}
scopeInfo = scopeInfo.upper;
},
/*
* Initializes segments.
* NOTE: This event is notified for only reachable segments.
*/
onCodePathSegmentStart(segment) {
const info = {
uselessReturns: getUselessReturns([], segment.allPrevSegments),
returned: false
};
// Stores the info.
segmentInfoMap.set(segment, info);
},
// Adds ReturnStatement node to check whether it's useless or not.
ReturnStatement(node) {
if (node.argument) {
markReturnStatementsOnCurrentSegmentsAsUsed();
}
if (
node.argument ||
astUtils.isInLoop(node) ||
isInFinally(node) ||
// Ignore `return` statements in unreachable places (https://github.com/eslint/eslint/issues/11647).
!scopeInfo.codePath.currentSegments.some(s => s.reachable)
) {
return;
}
for (const segment of scopeInfo.codePath.currentSegments) {
const info = segmentInfoMap.get(segment);
if (info) {
info.uselessReturns.push(node);
info.returned = true;
}
}
scopeInfo.uselessReturns.push(node);
},
/*
* Registers for all statement nodes except FunctionDeclaration, BlockStatement, BreakStatement.
* Removes return statements of the current segments from the useless return statement list.
*/
ClassDeclaration: markReturnStatementsOnCurrentSegmentsAsUsed,
ContinueStatement: markReturnStatementsOnCurrentSegmentsAsUsed,
DebuggerStatement: markReturnStatementsOnCurrentSegmentsAsUsed,
DoWhileStatement: markReturnStatementsOnCurrentSegmentsAsUsed,
EmptyStatement: markReturnStatementsOnCurrentSegmentsAsUsed,
ExpressionStatement: markReturnStatementsOnCurrentSegmentsAsUsed,
ForInStatement: markReturnStatementsOnCurrentSegmentsAsUsed,
ForOfStatement: markReturnStatementsOnCurrentSegmentsAsUsed,
ForStatement: markReturnStatementsOnCurrentSegmentsAsUsed,
IfStatement: markReturnStatementsOnCurrentSegmentsAsUsed,
ImportDeclaration: markReturnStatementsOnCurrentSegmentsAsUsed,
LabeledStatement: markReturnStatementsOnCurrentSegmentsAsUsed,
SwitchStatement: markReturnStatementsOnCurrentSegmentsAsUsed,
ThrowStatement: markReturnStatementsOnCurrentSegmentsAsUsed,
TryStatement: markReturnStatementsOnCurrentSegmentsAsUsed,
VariableDeclaration: markReturnStatementsOnCurrentSegmentsAsUsed,
WhileStatement: markReturnStatementsOnCurrentSegmentsAsUsed,
WithStatement: markReturnStatementsOnCurrentSegmentsAsUsed,
ExportNamedDeclaration: markReturnStatementsOnCurrentSegmentsAsUsed,
ExportDefaultDeclaration: markReturnStatementsOnCurrentSegmentsAsUsed,
ExportAllDeclaration: markReturnStatementsOnCurrentSegmentsAsUsed
};
}
};