UNPKG

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