UNPKG

8.1 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 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 docs: {
145 description: "enforce `return` statements in callbacks of array methods",
146 category: "Best Practices",
147 recommended: false,
148 url: "https://eslint.org/docs/rules/array-callback-return"
149 },
150
151 schema: [
152 {
153 type: "object",
154 properties: {
155 allowImplicit: {
156 type: "boolean"
157 }
158 },
159 additionalProperties: false
160 }
161 ],
162
163 messages: {
164 expectedAtEnd: "Expected to return a value at the end of {{name}}.",
165 expectedInside: "Expected to return a value in {{name}}.",
166 expectedReturnValue: "{{name}} expected a return value."
167 }
168 },
169
170 create(context) {
171
172 const options = context.options[0] || { allowImplicit: false };
173
174 let funcInfo = {
175 upper: null,
176 codePath: null,
177 hasReturn: false,
178 shouldCheck: false,
179 node: null
180 };
181
182 /**
183 * Checks whether or not the last code path segment is reachable.
184 * Then reports this function if the segment is reachable.
185 *
186 * If the last code path segment is reachable, there are paths which are not
187 * returned or thrown.
188 *
189 * @param {ASTNode} node - A node to check.
190 * @returns {void}
191 */
192 function checkLastSegment(node) {
193 if (funcInfo.shouldCheck &&
194 funcInfo.codePath.currentSegments.some(isReachable)
195 ) {
196 context.report({
197 node,
198 loc: getLocation(node, context.getSourceCode()).loc.start,
199 messageId: funcInfo.hasReturn
200 ? "expectedAtEnd"
201 : "expectedInside",
202 data: {
203 name: astUtils.getFunctionNameWithKind(funcInfo.node)
204 }
205 });
206 }
207 }
208
209 return {
210
211 // Stacks this function's information.
212 onCodePathStart(codePath, node) {
213 funcInfo = {
214 upper: funcInfo,
215 codePath,
216 hasReturn: false,
217 shouldCheck:
218 TARGET_NODE_TYPE.test(node.type) &&
219 node.body.type === "BlockStatement" &&
220 isCallbackOfArrayMethod(node) &&
221 !node.async &&
222 !node.generator,
223 node
224 };
225 },
226
227 // Pops this function's information.
228 onCodePathEnd() {
229 funcInfo = funcInfo.upper;
230 },
231
232 // Checks the return statement is valid.
233 ReturnStatement(node) {
234 if (funcInfo.shouldCheck) {
235 funcInfo.hasReturn = true;
236
237 // if allowImplicit: false, should also check node.argument
238 if (!options.allowImplicit && !node.argument) {
239 context.report({
240 node,
241 messageId: "expectedReturnValue",
242 data: {
243 name: lodash.upperFirst(astUtils.getFunctionNameWithKind(funcInfo.node))
244 }
245 });
246 }
247 }
248 },
249
250 // Reports a given function if the last path is reachable.
251 "FunctionExpression:exit": checkLastSegment,
252 "ArrowFunctionExpression:exit": checkLastSegment
253 };
254 }
255};