123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300 |
- /**
- * @fileoverview Disallow redundant return statements
- * @author Teddy Katz
- */
- "use strict";
-
- //------------------------------------------------------------------------------
- // Requirements
- //------------------------------------------------------------------------------
-
- const astUtils = require("../util/ast-utils"),
- FixTracker = require("../util/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)) {
- 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
- };
- }
- };
|