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