UNPKG

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