UNPKG

12 kBJavaScriptView Raw
1/**
2 * @fileoverview Disallow redundant return statements
3 * @author Teddy Katz
4 */
5"use strict";
6
7//------------------------------------------------------------------------------
8// Requirements
9//------------------------------------------------------------------------------
10
11const astUtils = require("../ast-utils"),
12 FixTracker = require("../util/fix-tracker");
13
14//------------------------------------------------------------------------------
15// Helpers
16//------------------------------------------------------------------------------
17
18/**
19 * Adds all elements of 2nd argument into 1st argument.
20 *
21 * @param {Array} array - The destination array to add.
22 * @param {Array} elements - The source array to add.
23 * @returns {void}
24 */
25const pushAll = Function.apply.bind(Array.prototype.push);
26
27/**
28 * Removes the given element from the array.
29 *
30 * @param {Array} array - The source array to remove.
31 * @param {any} element - The target item to remove.
32 * @returns {void}
33 */
34function remove(array, element) {
35 const index = array.indexOf(element);
36
37 if (index !== -1) {
38 array.splice(index, 1);
39 }
40}
41
42/**
43 * Checks whether it can remove the given return statement or not.
44 *
45 * @param {ASTNode} node - The return statement node to check.
46 * @returns {boolean} `true` if the node is removeable.
47 */
48function isRemovable(node) {
49 return astUtils.STATEMENT_LIST_PARENTS.has(node.parent.type);
50}
51
52/**
53 * Checks whether the given return statement is in a `finally` block or not.
54 *
55 * @param {ASTNode} node - The return statement node to check.
56 * @returns {boolean} `true` if the node is in a `finally` block.
57 */
58function isInFinally(node) {
59 for (
60 let currentNode = node;
61 currentNode && currentNode.parent && !astUtils.isFunction(currentNode);
62 currentNode = currentNode.parent
63 ) {
64 if (currentNode.parent.type === "TryStatement" && currentNode.parent.finalizer === currentNode) {
65 return true;
66 }
67 }
68
69 return false;
70}
71
72//------------------------------------------------------------------------------
73// Rule Definition
74//------------------------------------------------------------------------------
75
76module.exports = {
77 meta: {
78 docs: {
79 description: "disallow redundant return statements",
80 category: "Best Practices",
81 recommended: false,
82 url: "https://eslint.org/docs/rules/no-useless-return"
83 },
84 fixable: "code",
85 schema: []
86 },
87
88 create(context) {
89 const segmentInfoMap = new WeakMap();
90 const usedUnreachableSegments = new WeakSet();
91 let scopeInfo = null;
92
93 /**
94 * Checks whether the given segment is terminated by a return statement or not.
95 *
96 * @param {CodePathSegment} segment - The segment to check.
97 * @returns {boolean} `true` if the segment is terminated by a return statement, or if it's still a part of unreachable.
98 */
99 function isReturned(segment) {
100 const info = segmentInfoMap.get(segment);
101
102 return !info || info.returned;
103 }
104
105 /**
106 * Collects useless return statements from the given previous segments.
107 *
108 * A previous segment may be an unreachable segment.
109 * In that case, the information object of the unreachable segment is not
110 * initialized because `onCodePathSegmentStart` event is not notified for
111 * unreachable segments.
112 * This goes to the previous segments of the unreachable segment recursively
113 * if the unreachable segment was generated by a return statement. Otherwise,
114 * this ignores the unreachable segment.
115 *
116 * This behavior would simulate code paths for the case that the return
117 * statement does not exist.
118 *
119 * @param {ASTNode[]} uselessReturns - The collected return statements.
120 * @param {CodePathSegment[]} prevSegments - The previous segments to traverse.
121 * @param {WeakSet<CodePathSegment>} [providedTraversedSegments] A set of segments that have already been traversed in this call
122 * @returns {ASTNode[]} `uselessReturns`.
123 */
124 function getUselessReturns(uselessReturns, prevSegments, providedTraversedSegments) {
125 const traversedSegments = providedTraversedSegments || new WeakSet();
126
127 for (const segment of prevSegments) {
128 if (!segment.reachable) {
129 if (!traversedSegments.has(segment)) {
130 traversedSegments.add(segment);
131 getUselessReturns(
132 uselessReturns,
133 segment.allPrevSegments.filter(isReturned),
134 traversedSegments
135 );
136 }
137 continue;
138 }
139
140 pushAll(uselessReturns, segmentInfoMap.get(segment).uselessReturns);
141 }
142
143 return uselessReturns;
144 }
145
146 /**
147 * Removes the return statements on the given segment from the useless return
148 * statement list.
149 *
150 * This segment may be an unreachable segment.
151 * In that case, the information object of the unreachable segment is not
152 * initialized because `onCodePathSegmentStart` event is not notified for
153 * unreachable segments.
154 * This goes to the previous segments of the unreachable segment recursively
155 * if the unreachable segment was generated by a return statement. Otherwise,
156 * this ignores the unreachable segment.
157 *
158 * This behavior would simulate code paths for the case that the return
159 * statement does not exist.
160 *
161 * @param {CodePathSegment} segment - The segment to get return statements.
162 * @returns {void}
163 */
164 function markReturnStatementsOnSegmentAsUsed(segment) {
165 if (!segment.reachable) {
166 usedUnreachableSegments.add(segment);
167 segment.allPrevSegments
168 .filter(isReturned)
169 .filter(prevSegment => !usedUnreachableSegments.has(prevSegment))
170 .forEach(markReturnStatementsOnSegmentAsUsed);
171 return;
172 }
173
174 const info = segmentInfoMap.get(segment);
175
176 for (const node of info.uselessReturns) {
177 remove(scopeInfo.uselessReturns, node);
178 }
179 info.uselessReturns = [];
180 }
181
182 /**
183 * Removes the return statements on the current segments from the useless
184 * return statement list.
185 *
186 * This function will be called at every statement except FunctionDeclaration,
187 * BlockStatement, and BreakStatement.
188 *
189 * - FunctionDeclarations are always executed whether it's returned or not.
190 * - BlockStatements do nothing.
191 * - BreakStatements go the next merely.
192 *
193 * @returns {void}
194 */
195 function markReturnStatementsOnCurrentSegmentsAsUsed() {
196 scopeInfo
197 .codePath
198 .currentSegments
199 .forEach(markReturnStatementsOnSegmentAsUsed);
200 }
201
202 //----------------------------------------------------------------------
203 // Public
204 //----------------------------------------------------------------------
205
206 return {
207
208 // Makes and pushs a new scope information.
209 onCodePathStart(codePath) {
210 scopeInfo = {
211 upper: scopeInfo,
212 uselessReturns: [],
213 codePath
214 };
215 },
216
217 // Reports useless return statements if exist.
218 onCodePathEnd() {
219 for (const node of scopeInfo.uselessReturns) {
220 context.report({
221 node,
222 loc: node.loc,
223 message: "Unnecessary return statement.",
224 fix(fixer) {
225 if (isRemovable(node)) {
226
227 /*
228 * Extend the replacement range to include the
229 * entire function to avoid conflicting with
230 * no-else-return.
231 * https://github.com/eslint/eslint/issues/8026
232 */
233 return new FixTracker(fixer, context.getSourceCode())
234 .retainEnclosingFunction(node)
235 .remove(node);
236 }
237 return null;
238 }
239 });
240 }
241
242 scopeInfo = scopeInfo.upper;
243 },
244
245 /*
246 * Initializes segments.
247 * NOTE: This event is notified for only reachable segments.
248 */
249 onCodePathSegmentStart(segment) {
250 const info = {
251 uselessReturns: getUselessReturns([], segment.allPrevSegments),
252 returned: false
253 };
254
255 // Stores the info.
256 segmentInfoMap.set(segment, info);
257 },
258
259 // Adds ReturnStatement node to check whether it's useless or not.
260 ReturnStatement(node) {
261 if (node.argument) {
262 markReturnStatementsOnCurrentSegmentsAsUsed();
263 }
264 if (node.argument || astUtils.isInLoop(node) || isInFinally(node)) {
265 return;
266 }
267
268 for (const segment of scopeInfo.codePath.currentSegments) {
269 const info = segmentInfoMap.get(segment);
270
271 if (info) {
272 info.uselessReturns.push(node);
273 info.returned = true;
274 }
275 }
276 scopeInfo.uselessReturns.push(node);
277 },
278
279 /*
280 * Registers for all statement nodes except FunctionDeclaration, BlockStatement, BreakStatement.
281 * Removes return statements of the current segments from the useless return statement list.
282 */
283 ClassDeclaration: markReturnStatementsOnCurrentSegmentsAsUsed,
284 ContinueStatement: markReturnStatementsOnCurrentSegmentsAsUsed,
285 DebuggerStatement: markReturnStatementsOnCurrentSegmentsAsUsed,
286 DoWhileStatement: markReturnStatementsOnCurrentSegmentsAsUsed,
287 EmptyStatement: markReturnStatementsOnCurrentSegmentsAsUsed,
288 ExpressionStatement: markReturnStatementsOnCurrentSegmentsAsUsed,
289 ForInStatement: markReturnStatementsOnCurrentSegmentsAsUsed,
290 ForOfStatement: markReturnStatementsOnCurrentSegmentsAsUsed,
291 ForStatement: markReturnStatementsOnCurrentSegmentsAsUsed,
292 IfStatement: markReturnStatementsOnCurrentSegmentsAsUsed,
293 ImportDeclaration: markReturnStatementsOnCurrentSegmentsAsUsed,
294 LabeledStatement: markReturnStatementsOnCurrentSegmentsAsUsed,
295 SwitchStatement: markReturnStatementsOnCurrentSegmentsAsUsed,
296 ThrowStatement: markReturnStatementsOnCurrentSegmentsAsUsed,
297 TryStatement: markReturnStatementsOnCurrentSegmentsAsUsed,
298 VariableDeclaration: markReturnStatementsOnCurrentSegmentsAsUsed,
299 WhileStatement: markReturnStatementsOnCurrentSegmentsAsUsed,
300 WithStatement: markReturnStatementsOnCurrentSegmentsAsUsed,
301 ExportNamedDeclaration: markReturnStatementsOnCurrentSegmentsAsUsed,
302 ExportDefaultDeclaration: markReturnStatementsOnCurrentSegmentsAsUsed,
303 ExportAllDeclaration: markReturnStatementsOnCurrentSegmentsAsUsed
304 };
305 }
306};