2 * @fileoverview Rule to enforce return statements in callbacks of array's methods
3 * @author Toru Nagashima
8 //------------------------------------------------------------------------------
10 //------------------------------------------------------------------------------
12 const astUtils = require("./utils/ast-utils");
14 //------------------------------------------------------------------------------
16 //------------------------------------------------------------------------------
18 const TARGET_NODE_TYPE = /^(?:Arrow)?FunctionExpression$/u;
19 const TARGET_METHODS = /^(?:every|filter|find(?:Index)?|flatMap|forEach|map|reduce(?:Right)?|some|sort)$/u;
22 * Checks a given code path segment is reachable.
23 * @param {CodePathSegment} segment A segment to check.
24 * @returns {boolean} `true` if the segment is reachable.
26 function isReachable(segment) {
27 return segment.reachable;
31 * Checks a given node is a member access which has the specified name's
33 * @param {ASTNode} node A node to check.
34 * @returns {boolean} `true` if the node is a member access which has
35 * the specified name's property. The node may be a `(Chain|Member)Expression` node.
37 function isTargetMethod(node) {
38 return astUtils.isSpecificMemberAccess(node, null, TARGET_METHODS);
42 * Returns a human-legible description of an array method
43 * @param {string} arrayMethodName A method name to fully qualify
44 * @returns {string} the method name prefixed with `Array.` if it is a class method,
45 * or else `Array.prototype.` if it is an instance method.
47 function fullMethodName(arrayMethodName) {
48 if (["from", "of", "isArray"].includes(arrayMethodName)) {
49 return "Array.".concat(arrayMethodName);
51 return "Array.prototype.".concat(arrayMethodName);
55 * Checks whether or not a given node is a function expression which is the
56 * callback of an array method, returning the method name.
57 * @param {ASTNode} node A node to check. This is one of
58 * FunctionExpression or ArrowFunctionExpression.
59 * @returns {string} The method name if the node is a callback method,
62 function getArrayMethodName(node) {
63 let currentNode = node;
66 const parent = currentNode.parent;
68 switch (parent.type) {
71 * Looks up the destination. e.g.,
72 * foo.every(nativeFoo || function foo() { ... });
74 case "LogicalExpression":
75 case "ConditionalExpression":
76 case "ChainExpression":
81 * If the upper function is IIFE, checks the destination of the return value.
83 * foo.every((function() {
85 * return function callback() { ... };
88 case "ReturnStatement": {
89 const func = astUtils.getUpperFunction(parent);
91 if (func === null || !astUtils.isCallee(func)) {
94 currentNode = func.parent;
100 * Array.from([], function() {});
101 * list.every(function() {});
103 case "CallExpression":
104 if (astUtils.isArrayFromMethod(parent.callee)) {
106 parent.arguments.length >= 2 &&
107 parent.arguments[1] === currentNode
112 if (isTargetMethod(parent.callee)) {
114 parent.arguments.length >= 1 &&
115 parent.arguments[0] === currentNode
117 return astUtils.getStaticPropertyName(parent.callee);
122 // Otherwise this node is not target.
128 /* istanbul ignore next: unreachable */
132 //------------------------------------------------------------------------------
134 //------------------------------------------------------------------------------
141 description: "enforce `return` statements in callbacks of array methods",
142 category: "Best Practices",
144 url: "https://eslint.org/docs/rules/array-callback-return"
160 additionalProperties: false
165 expectedAtEnd: "{{arrayMethodName}}() expects a value to be returned at the end of {{name}}.",
166 expectedInside: "{{arrayMethodName}}() expects a return value from {{name}}.",
167 expectedReturnValue: "{{arrayMethodName}}() expects a return value from {{name}}.",
168 expectedNoReturnValue: "{{arrayMethodName}}() expects no useless return value from {{name}}."
174 const options = context.options[0] || { allowImplicit: false, checkForEach: false };
175 const sourceCode = context.getSourceCode();
178 arrayMethodName: null,
187 * Checks whether or not the last code path segment is reachable.
188 * Then reports this function if the segment is reachable.
190 * If the last code path segment is reachable, there are paths which are not
191 * returned or thrown.
192 * @param {ASTNode} node A node to check.
195 function checkLastSegment(node) {
197 if (!funcInfo.shouldCheck) {
201 let messageId = null;
203 if (funcInfo.arrayMethodName === "forEach") {
204 if (options.checkForEach && node.type === "ArrowFunctionExpression" && node.expression) {
205 messageId = "expectedNoReturnValue";
208 if (node.body.type === "BlockStatement" && funcInfo.codePath.currentSegments.some(isReachable)) {
209 messageId = funcInfo.hasReturn ? "expectedAtEnd" : "expectedInside";
214 const name = astUtils.getFunctionNameWithKind(node);
218 loc: astUtils.getFunctionHeadLoc(node, sourceCode),
220 data: { name, arrayMethodName: fullMethodName(funcInfo.arrayMethodName) }
227 // Stacks this function's information.
228 onCodePathStart(codePath, node) {
230 let methodName = null;
232 if (TARGET_NODE_TYPE.test(node.type)) {
233 methodName = getArrayMethodName(node);
237 arrayMethodName: methodName,
249 // Pops this function's information.
251 funcInfo = funcInfo.upper;
254 // Checks the return statement is valid.
255 ReturnStatement(node) {
257 if (!funcInfo.shouldCheck) {
261 funcInfo.hasReturn = true;
263 let messageId = null;
265 if (funcInfo.arrayMethodName === "forEach") {
267 // if checkForEach: true, returning a value at any path inside a forEach is not allowed
268 if (options.checkForEach && node.argument) {
269 messageId = "expectedNoReturnValue";
273 // if allowImplicit: false, should also check node.argument
274 if (!options.allowImplicit && !node.argument) {
275 messageId = "expectedReturnValue";
284 name: astUtils.getFunctionNameWithKind(funcInfo.node),
285 arrayMethodName: fullMethodName(funcInfo.arrayMethodName)
291 // Reports a given function if the last path is reachable.
292 "FunctionExpression:exit": checkLastSegment,
293 "ArrowFunctionExpression:exit": checkLastSegment