UNPKG

8.03 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("../ast-utils");
15
16//------------------------------------------------------------------------------
17// Helpers
18//------------------------------------------------------------------------------
19
20const TARGET_NODE_TYPE = /^(?:Arrow)?FunctionExpression$/;
21const TARGET_METHODS = /^(?:every|filter|find(?:Index)?|map|reduce(?:Right)?|some|sort)$/;
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 while (node) {
75 const parent = node.parent;
76
77 switch (parent.type) {
78
79 /*
80 * Looks up the destination. e.g.,
81 * foo.every(nativeFoo || function foo() { ... });
82 */
83 case "LogicalExpression":
84 case "ConditionalExpression":
85 node = parent;
86 break;
87
88 /*
89 * If the upper function is IIFE, checks the destination of the return value.
90 * e.g.
91 * foo.every((function() {
92 * // setup...
93 * return function callback() { ... };
94 * })());
95 */
96 case "ReturnStatement": {
97 const func = astUtils.getUpperFunction(parent);
98
99 if (func === null || !astUtils.isCallee(func)) {
100 return false;
101 }
102 node = func.parent;
103 break;
104 }
105
106 /*
107 * e.g.
108 * Array.from([], function() {});
109 * list.every(function() {});
110 */
111 case "CallExpression":
112 if (astUtils.isArrayFromMethod(parent.callee)) {
113 return (
114 parent.arguments.length >= 2 &&
115 parent.arguments[1] === node
116 );
117 }
118 if (isTargetMethod(parent.callee)) {
119 return (
120 parent.arguments.length >= 1 &&
121 parent.arguments[0] === node
122 );
123 }
124 return false;
125
126 // Otherwise this node is not target.
127 default:
128 return false;
129 }
130 }
131
132 /* istanbul ignore next: unreachable */
133 return false;
134}
135
136//------------------------------------------------------------------------------
137// Rule Definition
138//------------------------------------------------------------------------------
139
140module.exports = {
141 meta: {
142 docs: {
143 description: "enforce `return` statements in callbacks of array methods",
144 category: "Best Practices",
145 recommended: false,
146 url: "https://eslint.org/docs/rules/array-callback-return"
147 },
148
149 schema: [
150 {
151 type: "object",
152 properties: {
153 allowImplicit: {
154 type: "boolean"
155 }
156 },
157 additionalProperties: false
158 }
159 ],
160
161 messages: {
162 expectedAtEnd: "Expected to return a value at the end of {{name}}.",
163 expectedInside: "Expected to return a value in {{name}}.",
164 expectedReturnValue: "{{name}} expected a return value."
165 }
166 },
167
168 create(context) {
169
170 const options = context.options[0] || { allowImplicit: false };
171
172 let funcInfo = {
173 upper: null,
174 codePath: null,
175 hasReturn: false,
176 shouldCheck: false,
177 node: null
178 };
179
180 /**
181 * Checks whether or not the last code path segment is reachable.
182 * Then reports this function if the segment is reachable.
183 *
184 * If the last code path segment is reachable, there are paths which are not
185 * returned or thrown.
186 *
187 * @param {ASTNode} node - A node to check.
188 * @returns {void}
189 */
190 function checkLastSegment(node) {
191 if (funcInfo.shouldCheck &&
192 funcInfo.codePath.currentSegments.some(isReachable)
193 ) {
194 context.report({
195 node,
196 loc: getLocation(node, context.getSourceCode()).loc.start,
197 messageId: funcInfo.hasReturn
198 ? "expectedAtEnd"
199 : "expectedInside",
200 data: {
201 name: astUtils.getFunctionNameWithKind(funcInfo.node)
202 }
203 });
204 }
205 }
206
207 return {
208
209 // Stacks this function's information.
210 onCodePathStart(codePath, node) {
211 funcInfo = {
212 upper: funcInfo,
213 codePath,
214 hasReturn: false,
215 shouldCheck:
216 TARGET_NODE_TYPE.test(node.type) &&
217 node.body.type === "BlockStatement" &&
218 isCallbackOfArrayMethod(node) &&
219 !node.async &&
220 !node.generator,
221 node
222 };
223 },
224
225 // Pops this function's information.
226 onCodePathEnd() {
227 funcInfo = funcInfo.upper;
228 },
229
230 // Checks the return statement is valid.
231 ReturnStatement(node) {
232 if (funcInfo.shouldCheck) {
233 funcInfo.hasReturn = true;
234
235 // if allowImplicit: false, should also check node.argument
236 if (!options.allowImplicit && !node.argument) {
237 context.report({
238 node,
239 messageId: "expectedReturnValue",
240 data: {
241 name: lodash.upperFirst(astUtils.getFunctionNameWithKind(funcInfo.node))
242 }
243 });
244 }
245 }
246 },
247
248 // Reports a given function if the last path is reachable.
249 "FunctionExpression:exit": checkLastSegment,
250 "ArrowFunctionExpression:exit": checkLastSegment
251 };
252 }
253};