• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/**
2 * @fileoverview Rule to enforce return statements in callbacks of array's methods
3 * @author Toru Nagashima
4 */
5
6"use strict";
7
8//------------------------------------------------------------------------------
9// Requirements
10//------------------------------------------------------------------------------
11
12const lodash = require("lodash");
13
14const astUtils = require("./utils/ast-utils");
15
16//------------------------------------------------------------------------------
17// Helpers
18//------------------------------------------------------------------------------
19
20const TARGET_NODE_TYPE = /^(?:Arrow)?FunctionExpression$/u;
21const TARGET_METHODS = /^(?:every|filter|find(?:Index)?|flatMap|forEach|map|reduce(?:Right)?|some|sort)$/u;
22
23/**
24 * Checks a given code path segment is reachable.
25 * @param {CodePathSegment} segment A segment to check.
26 * @returns {boolean} `true` if the segment is reachable.
27 */
28function isReachable(segment) {
29    return segment.reachable;
30}
31
32/**
33 * Checks a given node is a member access which has the specified name's
34 * property.
35 * @param {ASTNode} node A node to check.
36 * @returns {boolean} `true` if the node is a member access which has
37 *      the specified name's property. The node may be a `(Chain|Member)Expression` node.
38 */
39function isTargetMethod(node) {
40    return astUtils.isSpecificMemberAccess(node, null, TARGET_METHODS);
41}
42
43/**
44 * Checks whether or not a given node is a function expression which is the
45 * callback of an array method, returning the method name.
46 * @param {ASTNode} node A node to check. This is one of
47 *      FunctionExpression or ArrowFunctionExpression.
48 * @returns {string} The method name if the node is a callback method,
49 *      null otherwise.
50 */
51function getArrayMethodName(node) {
52    let currentNode = node;
53
54    while (currentNode) {
55        const parent = currentNode.parent;
56
57        switch (parent.type) {
58
59            /*
60             * Looks up the destination. e.g.,
61             * foo.every(nativeFoo || function foo() { ... });
62             */
63            case "LogicalExpression":
64            case "ConditionalExpression":
65            case "ChainExpression":
66                currentNode = parent;
67                break;
68
69            /*
70             * If the upper function is IIFE, checks the destination of the return value.
71             * e.g.
72             *   foo.every((function() {
73             *     // setup...
74             *     return function callback() { ... };
75             *   })());
76             */
77            case "ReturnStatement": {
78                const func = astUtils.getUpperFunction(parent);
79
80                if (func === null || !astUtils.isCallee(func)) {
81                    return null;
82                }
83                currentNode = func.parent;
84                break;
85            }
86
87            /*
88             * e.g.
89             *   Array.from([], function() {});
90             *   list.every(function() {});
91             */
92            case "CallExpression":
93                if (astUtils.isArrayFromMethod(parent.callee)) {
94                    if (
95                        parent.arguments.length >= 2 &&
96                        parent.arguments[1] === currentNode
97                    ) {
98                        return "from";
99                    }
100                }
101                if (isTargetMethod(parent.callee)) {
102                    if (
103                        parent.arguments.length >= 1 &&
104                        parent.arguments[0] === currentNode
105                    ) {
106                        return astUtils.getStaticPropertyName(parent.callee);
107                    }
108                }
109                return null;
110
111            // Otherwise this node is not target.
112            default:
113                return null;
114        }
115    }
116
117    /* istanbul ignore next: unreachable */
118    return null;
119}
120
121//------------------------------------------------------------------------------
122// Rule Definition
123//------------------------------------------------------------------------------
124
125module.exports = {
126    meta: {
127        type: "problem",
128
129        docs: {
130            description: "enforce `return` statements in callbacks of array methods",
131            category: "Best Practices",
132            recommended: false,
133            url: "https://eslint.org/docs/rules/array-callback-return"
134        },
135
136        schema: [
137            {
138                type: "object",
139                properties: {
140                    allowImplicit: {
141                        type: "boolean",
142                        default: false
143                    },
144                    checkForEach: {
145                        type: "boolean",
146                        default: false
147                    }
148                },
149                additionalProperties: false
150            }
151        ],
152
153        messages: {
154            expectedAtEnd: "Expected to return a value at the end of {{name}}.",
155            expectedInside: "Expected to return a value in {{name}}.",
156            expectedReturnValue: "{{name}} expected a return value.",
157            expectedNoReturnValue: "{{name}} did not expect a return value."
158        }
159    },
160
161    create(context) {
162
163        const options = context.options[0] || { allowImplicit: false, checkForEach: false };
164        const sourceCode = context.getSourceCode();
165
166        let funcInfo = {
167            arrayMethodName: null,
168            upper: null,
169            codePath: null,
170            hasReturn: false,
171            shouldCheck: false,
172            node: null
173        };
174
175        /**
176         * Checks whether or not the last code path segment is reachable.
177         * Then reports this function if the segment is reachable.
178         *
179         * If the last code path segment is reachable, there are paths which are not
180         * returned or thrown.
181         * @param {ASTNode} node A node to check.
182         * @returns {void}
183         */
184        function checkLastSegment(node) {
185
186            if (!funcInfo.shouldCheck) {
187                return;
188            }
189
190            let messageId = null;
191
192            if (funcInfo.arrayMethodName === "forEach") {
193                if (options.checkForEach && node.type === "ArrowFunctionExpression" && node.expression) {
194                    messageId = "expectedNoReturnValue";
195                }
196            } else {
197                if (node.body.type === "BlockStatement" && funcInfo.codePath.currentSegments.some(isReachable)) {
198                    messageId = funcInfo.hasReturn ? "expectedAtEnd" : "expectedInside";
199                }
200            }
201
202            if (messageId) {
203                let name = astUtils.getFunctionNameWithKind(node);
204
205                name = messageId === "expectedNoReturnValue" ? lodash.upperFirst(name) : name;
206                context.report({
207                    node,
208                    loc: astUtils.getFunctionHeadLoc(node, sourceCode),
209                    messageId,
210                    data: { name }
211                });
212            }
213        }
214
215        return {
216
217            // Stacks this function's information.
218            onCodePathStart(codePath, node) {
219
220                let methodName = null;
221
222                if (TARGET_NODE_TYPE.test(node.type)) {
223                    methodName = getArrayMethodName(node);
224                }
225
226                funcInfo = {
227                    arrayMethodName: methodName,
228                    upper: funcInfo,
229                    codePath,
230                    hasReturn: false,
231                    shouldCheck:
232                        methodName &&
233                        !node.async &&
234                        !node.generator,
235                    node
236                };
237            },
238
239            // Pops this function's information.
240            onCodePathEnd() {
241                funcInfo = funcInfo.upper;
242            },
243
244            // Checks the return statement is valid.
245            ReturnStatement(node) {
246
247                if (!funcInfo.shouldCheck) {
248                    return;
249                }
250
251                funcInfo.hasReturn = true;
252
253                let messageId = null;
254
255                if (funcInfo.arrayMethodName === "forEach") {
256
257                    // if checkForEach: true, returning a value at any path inside a forEach is not allowed
258                    if (options.checkForEach && node.argument) {
259                        messageId = "expectedNoReturnValue";
260                    }
261                } else {
262
263                    // if allowImplicit: false, should also check node.argument
264                    if (!options.allowImplicit && !node.argument) {
265                        messageId = "expectedReturnValue";
266                    }
267                }
268
269                if (messageId) {
270                    context.report({
271                        node,
272                        messageId,
273                        data: {
274                            name: lodash.upperFirst(astUtils.getFunctionNameWithKind(funcInfo.node))
275                        }
276                    });
277                }
278            },
279
280            // Reports a given function if the last path is reachable.
281            "FunctionExpression:exit": checkLastSegment,
282            "ArrowFunctionExpression:exit": checkLastSegment
283        };
284    }
285};
286