]> git.proxmox.com Git - pve-eslint.git/blob - eslint/lib/rules/array-callback-return.js
import 8.3.0 source
[pve-eslint.git] / eslint / lib / rules / array-callback-return.js
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
12 const astUtils = require("./utils/ast-utils");
13
14 //------------------------------------------------------------------------------
15 // Helpers
16 //------------------------------------------------------------------------------
17
18 const TARGET_NODE_TYPE = /^(?:Arrow)?FunctionExpression$/u;
19 const 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 */
26 function 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 */
37 function 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 */
47 function 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 */
62 function 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
136 module.exports = {
137 meta: {
138 type: "problem",
139
140 docs: {
141 description: "enforce `return` statements in callbacks of array methods",
142 recommended: false,
143 url: "https://eslint.org/docs/rules/array-callback-return"
144 },
145
146 schema: [
147 {
148 type: "object",
149 properties: {
150 allowImplicit: {
151 type: "boolean",
152 default: false
153 },
154 checkForEach: {
155 type: "boolean",
156 default: false
157 }
158 },
159 additionalProperties: false
160 }
161 ],
162
163 messages: {
164 expectedAtEnd: "{{arrayMethodName}}() expects a value to be returned at the end of {{name}}.",
165 expectedInside: "{{arrayMethodName}}() expects a return value from {{name}}.",
166 expectedReturnValue: "{{arrayMethodName}}() expects a return value from {{name}}.",
167 expectedNoReturnValue: "{{arrayMethodName}}() expects no useless return value from {{name}}."
168 }
169 },
170
171 create(context) {
172
173 const options = context.options[0] || { allowImplicit: false, checkForEach: false };
174 const sourceCode = context.getSourceCode();
175
176 let funcInfo = {
177 arrayMethodName: null,
178 upper: null,
179 codePath: null,
180 hasReturn: false,
181 shouldCheck: false,
182 node: null
183 };
184
185 /**
186 * Checks whether or not the last code path segment is reachable.
187 * Then reports this function if the segment is reachable.
188 *
189 * If the last code path segment is reachable, there are paths which are not
190 * returned or thrown.
191 * @param {ASTNode} node A node to check.
192 * @returns {void}
193 */
194 function checkLastSegment(node) {
195
196 if (!funcInfo.shouldCheck) {
197 return;
198 }
199
200 let messageId = null;
201
202 if (funcInfo.arrayMethodName === "forEach") {
203 if (options.checkForEach && node.type === "ArrowFunctionExpression" && node.expression) {
204 messageId = "expectedNoReturnValue";
205 }
206 } else {
207 if (node.body.type === "BlockStatement" && funcInfo.codePath.currentSegments.some(isReachable)) {
208 messageId = funcInfo.hasReturn ? "expectedAtEnd" : "expectedInside";
209 }
210 }
211
212 if (messageId) {
213 const name = astUtils.getFunctionNameWithKind(node);
214
215 context.report({
216 node,
217 loc: astUtils.getFunctionHeadLoc(node, sourceCode),
218 messageId,
219 data: { name, arrayMethodName: fullMethodName(funcInfo.arrayMethodName) }
220 });
221 }
222 }
223
224 return {
225
226 // Stacks this function's information.
227 onCodePathStart(codePath, node) {
228
229 let methodName = null;
230
231 if (TARGET_NODE_TYPE.test(node.type)) {
232 methodName = getArrayMethodName(node);
233 }
234
235 funcInfo = {
236 arrayMethodName: methodName,
237 upper: funcInfo,
238 codePath,
239 hasReturn: false,
240 shouldCheck:
241 methodName &&
242 !node.async &&
243 !node.generator,
244 node
245 };
246 },
247
248 // Pops this function's information.
249 onCodePathEnd() {
250 funcInfo = funcInfo.upper;
251 },
252
253 // Checks the return statement is valid.
254 ReturnStatement(node) {
255
256 if (!funcInfo.shouldCheck) {
257 return;
258 }
259
260 funcInfo.hasReturn = true;
261
262 let messageId = null;
263
264 if (funcInfo.arrayMethodName === "forEach") {
265
266 // if checkForEach: true, returning a value at any path inside a forEach is not allowed
267 if (options.checkForEach && node.argument) {
268 messageId = "expectedNoReturnValue";
269 }
270 } else {
271
272 // if allowImplicit: false, should also check node.argument
273 if (!options.allowImplicit && !node.argument) {
274 messageId = "expectedReturnValue";
275 }
276 }
277
278 if (messageId) {
279 context.report({
280 node,
281 messageId,
282 data: {
283 name: astUtils.getFunctionNameWithKind(funcInfo.node),
284 arrayMethodName: fullMethodName(funcInfo.arrayMethodName)
285 }
286 });
287 }
288 },
289
290 // Reports a given function if the last path is reachable.
291 "FunctionExpression:exit": checkLastSegment,
292 "ArrowFunctionExpression:exit": checkLastSegment
293 };
294 }
295 };