UNPKG

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