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