UNPKG

8.17 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 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)?|map|reduce(?:Right)?|some|sort)$/u;
22
23/**
24 * Checks a given code path segment is reachable.
25 *
26 * @param {CodePathSegment} segment - A segment to check.
27 * @returns {boolean} `true` if the segment is reachable.
28 */
29function isReachable(segment) {
30 return segment.reachable;
31}
32
33/**
34 * Gets a readable location.
35 *
36 * - FunctionExpression -> the function name or `function` keyword.
37 * - ArrowFunctionExpression -> `=>` token.
38 *
39 * @param {ASTNode} node - A function node to get.
40 * @param {SourceCode} sourceCode - A source code to get tokens.
41 * @returns {ASTNode|Token} The node or the token of a location.
42 */
43function getLocation(node, sourceCode) {
44 if (node.type === "ArrowFunctionExpression") {
45 return sourceCode.getTokenBefore(node.body);
46 }
47 return node.id || node;
48}
49
50/**
51 * Checks a given node is a MemberExpression node which has the specified name's
52 * property.
53 *
54 * @param {ASTNode} node - A node to check.
55 * @returns {boolean} `true` if the node is a MemberExpression node which has
56 * the specified name's property
57 */
58function isTargetMethod(node) {
59 return (
60 node.type === "MemberExpression" &&
61 TARGET_METHODS.test(astUtils.getStaticPropertyName(node) || "")
62 );
63}
64
65/**
66 * Checks whether or not a given node is a function expression which is the
67 * callback of an array method.
68 *
69 * @param {ASTNode} node - A node to check. This is one of
70 * FunctionExpression or ArrowFunctionExpression.
71 * @returns {boolean} `true` if the node is the callback of an array method.
72 */
73function isCallbackOfArrayMethod(node) {
74 let currentNode = node;
75
76 while (currentNode) {
77 const parent = currentNode.parent;
78
79 switch (parent.type) {
80
81 /*
82 * Looks up the destination. e.g.,
83 * foo.every(nativeFoo || function foo() { ... });
84 */
85 case "LogicalExpression":
86 case "ConditionalExpression":
87 currentNode = parent;
88 break;
89
90 /*
91 * If the upper function is IIFE, checks the destination of the return value.
92 * e.g.
93 * foo.every((function() {
94 * // setup...
95 * return function callback() { ... };
96 * })());
97 */
98 case "ReturnStatement": {
99 const func = astUtils.getUpperFunction(parent);
100
101 if (func === null || !astUtils.isCallee(func)) {
102 return false;
103 }
104 currentNode = func.parent;
105 break;
106 }
107
108 /*
109 * e.g.
110 * Array.from([], function() {});
111 * list.every(function() {});
112 */
113 case "CallExpression":
114 if (astUtils.isArrayFromMethod(parent.callee)) {
115 return (
116 parent.arguments.length >= 2 &&
117 parent.arguments[1] === currentNode
118 );
119 }
120 if (isTargetMethod(parent.callee)) {
121 return (
122 parent.arguments.length >= 1 &&
123 parent.arguments[0] === currentNode
124 );
125 }
126 return false;
127
128 // Otherwise this node is not target.
129 default:
130 return false;
131 }
132 }
133
134 /* istanbul ignore next: unreachable */
135 return false;
136}
137
138//------------------------------------------------------------------------------
139// Rule Definition
140//------------------------------------------------------------------------------
141
142module.exports = {
143 meta: {
144 type: "problem",
145
146 docs: {
147 description: "enforce `return` statements in callbacks of array methods",
148 category: "Best Practices",
149 recommended: false,
150 url: "https://eslint.org/docs/rules/array-callback-return"
151 },
152
153 schema: [
154 {
155 type: "object",
156 properties: {
157 allowImplicit: {
158 type: "boolean",
159 default: false
160 }
161 },
162 additionalProperties: false
163 }
164 ],
165
166 messages: {
167 expectedAtEnd: "Expected to return a value at the end of {{name}}.",
168 expectedInside: "Expected to return a value in {{name}}.",
169 expectedReturnValue: "{{name}} expected a return value."
170 }
171 },
172
173 create(context) {
174
175 const options = context.options[0] || { allowImplicit: false };
176
177 let funcInfo = {
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 *
192 * @param {ASTNode} node - A node to check.
193 * @returns {void}
194 */
195 function checkLastSegment(node) {
196 if (funcInfo.shouldCheck &&
197 funcInfo.codePath.currentSegments.some(isReachable)
198 ) {
199 context.report({
200 node,
201 loc: getLocation(node, context.getSourceCode()).loc.start,
202 messageId: funcInfo.hasReturn
203 ? "expectedAtEnd"
204 : "expectedInside",
205 data: {
206 name: astUtils.getFunctionNameWithKind(funcInfo.node)
207 }
208 });
209 }
210 }
211
212 return {
213
214 // Stacks this function's information.
215 onCodePathStart(codePath, node) {
216 funcInfo = {
217 upper: funcInfo,
218 codePath,
219 hasReturn: false,
220 shouldCheck:
221 TARGET_NODE_TYPE.test(node.type) &&
222 node.body.type === "BlockStatement" &&
223 isCallbackOfArrayMethod(node) &&
224 !node.async &&
225 !node.generator,
226 node
227 };
228 },
229
230 // Pops this function's information.
231 onCodePathEnd() {
232 funcInfo = funcInfo.upper;
233 },
234
235 // Checks the return statement is valid.
236 ReturnStatement(node) {
237 if (funcInfo.shouldCheck) {
238 funcInfo.hasReturn = true;
239
240 // if allowImplicit: false, should also check node.argument
241 if (!options.allowImplicit && !node.argument) {
242 context.report({
243 node,
244 messageId: "expectedReturnValue",
245 data: {
246 name: lodash.upperFirst(astUtils.getFunctionNameWithKind(funcInfo.node))
247 }
248 });
249 }
250 }
251 },
252
253 // Reports a given function if the last path is reachable.
254 "FunctionExpression:exit": checkLastSegment,
255 "ArrowFunctionExpression:exit": checkLastSegment
256 };
257 }
258};