1 | /**
|
2 | * @fileoverview Disallow redundant return statements
|
3 | * @author Teddy Katz
|
4 | */
|
5 | ;
|
6 |
|
7 | //------------------------------------------------------------------------------
|
8 | // Requirements
|
9 | //------------------------------------------------------------------------------
|
10 |
|
11 | const 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 | */
|
25 | const 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 | */
|
34 | function 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 | */
|
48 | function 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 | */
|
58 | function 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 |
|
76 | module.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 | };
|