UNPKG

7.27 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 // If the upper function is IIFE, checks the destination of the return value.
89 // e.g.
90 // foo.every((function() {
91 // // setup...
92 // return function callback() { ... };
93 // })());
94 case "ReturnStatement": {
95 const func = astUtils.getUpperFunction(parent);
96
97 if (func === null || !astUtils.isCallee(func)) {
98 return false;
99 }
100 node = func.parent;
101 break;
102 }
103
104 // e.g.
105 // Array.from([], function() {});
106 // list.every(function() {});
107 case "CallExpression":
108 if (astUtils.isArrayFromMethod(parent.callee)) {
109 return (
110 parent.arguments.length >= 2 &&
111 parent.arguments[1] === node
112 );
113 }
114 if (isTargetMethod(parent.callee)) {
115 return (
116 parent.arguments.length >= 1 &&
117 parent.arguments[0] === node
118 );
119 }
120 return false;
121
122 // Otherwise this node is not target.
123 default:
124 return false;
125 }
126 }
127
128 /* istanbul ignore next: unreachable */
129 return false;
130}
131
132//------------------------------------------------------------------------------
133// Rule Definition
134//------------------------------------------------------------------------------
135
136module.exports = {
137 meta: {
138 docs: {
139 description: "enforce `return` statements in callbacks of array methods",
140 category: "Best Practices",
141 recommended: false
142 },
143
144 schema: []
145 },
146
147 create(context) {
148 let funcInfo = {
149 upper: null,
150 codePath: null,
151 hasReturn: false,
152 shouldCheck: false,
153 node: null
154 };
155
156 /**
157 * Checks whether or not the last code path segment is reachable.
158 * Then reports this function if the segment is reachable.
159 *
160 * If the last code path segment is reachable, there are paths which are not
161 * returned or thrown.
162 *
163 * @param {ASTNode} node - A node to check.
164 * @returns {void}
165 */
166 function checkLastSegment(node) {
167 if (funcInfo.shouldCheck &&
168 funcInfo.codePath.currentSegments.some(isReachable)
169 ) {
170 context.report({
171 node,
172 loc: getLocation(node, context.getSourceCode()).loc.start,
173 message: funcInfo.hasReturn
174 ? "Expected to return a value at the end of {{name}}."
175 : "Expected to return a value in {{name}}.",
176 data: {
177 name: astUtils.getFunctionNameWithKind(funcInfo.node)
178 }
179 });
180 }
181 }
182
183 return {
184
185 // Stacks this function's information.
186 onCodePathStart(codePath, node) {
187 funcInfo = {
188 upper: funcInfo,
189 codePath,
190 hasReturn: false,
191 shouldCheck:
192 TARGET_NODE_TYPE.test(node.type) &&
193 node.body.type === "BlockStatement" &&
194 isCallbackOfArrayMethod(node) &&
195 !node.async &&
196 !node.generator,
197 node
198 };
199 },
200
201 // Pops this function's information.
202 onCodePathEnd() {
203 funcInfo = funcInfo.upper;
204 },
205
206 // Checks the return statement is valid.
207 ReturnStatement(node) {
208 if (funcInfo.shouldCheck) {
209 funcInfo.hasReturn = true;
210
211 if (!node.argument) {
212 context.report({
213 node,
214 message: "{{name}} expected a return value.",
215 data: {
216 name: lodash.upperFirst(astUtils.getFunctionNameWithKind(funcInfo.node))
217 }
218 });
219 }
220 }
221 },
222
223 // Reports a given function if the last path is reachable.
224 "FunctionExpression:exit": checkLastSegment,
225 "ArrowFunctionExpression:exit": checkLastSegment
226 };
227 }
228};