UNPKG

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