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 | 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 |
|
74 | module.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 | };
|