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