UNPKG

6.46 kBJavaScriptView Raw
1/**
2 * @fileoverview Enforce return after a callback.
3 * @author Jamund Ferguson
4 */
5"use strict";
6
7//------------------------------------------------------------------------------
8// Rule Definition
9//------------------------------------------------------------------------------
10
11module.exports = {
12 meta: {
13 docs: {
14 description: "require `return` statements after callbacks",
15 category: "Node.js and CommonJS",
16 recommended: false,
17 url: "https://eslint.org/docs/rules/callback-return"
18 },
19
20 schema: [{
21 type: "array",
22 items: { type: "string" }
23 }],
24
25 messages: {
26 missingReturn: "Expected return with your callback function."
27 }
28 },
29
30 create(context) {
31
32 const callbacks = context.options[0] || ["callback", "cb", "next"],
33 sourceCode = context.getSourceCode();
34
35 //--------------------------------------------------------------------------
36 // Helpers
37 //--------------------------------------------------------------------------
38
39 /**
40 * Find the closest parent matching a list of types.
41 * @param {ASTNode} node The node whose parents we are searching
42 * @param {Array} types The node types to match
43 * @returns {ASTNode} The matched node or undefined.
44 */
45 function findClosestParentOfType(node, types) {
46 if (!node.parent) {
47 return null;
48 }
49 if (types.indexOf(node.parent.type) === -1) {
50 return findClosestParentOfType(node.parent, types);
51 }
52 return node.parent;
53 }
54
55 /**
56 * Check to see if a node contains only identifers
57 * @param {ASTNode} node The node to check
58 * @returns {boolean} Whether or not the node contains only identifers
59 */
60 function containsOnlyIdentifiers(node) {
61 if (node.type === "Identifier") {
62 return true;
63 }
64
65 if (node.type === "MemberExpression") {
66 if (node.object.type === "Identifier") {
67 return true;
68 }
69 if (node.object.type === "MemberExpression") {
70 return containsOnlyIdentifiers(node.object);
71 }
72 }
73
74 return false;
75 }
76
77 /**
78 * Check to see if a CallExpression is in our callback list.
79 * @param {ASTNode} node The node to check against our callback names list.
80 * @returns {boolean} Whether or not this function matches our callback name.
81 */
82 function isCallback(node) {
83 return containsOnlyIdentifiers(node.callee) && callbacks.indexOf(sourceCode.getText(node.callee)) > -1;
84 }
85
86 /**
87 * Determines whether or not the callback is part of a callback expression.
88 * @param {ASTNode} node The callback node
89 * @param {ASTNode} parentNode The expression node
90 * @returns {boolean} Whether or not this is part of a callback expression
91 */
92 function isCallbackExpression(node, parentNode) {
93
94 // ensure the parent node exists and is an expression
95 if (!parentNode || parentNode.type !== "ExpressionStatement") {
96 return false;
97 }
98
99 // cb()
100 if (parentNode.expression === node) {
101 return true;
102 }
103
104 // special case for cb && cb() and similar
105 if (parentNode.expression.type === "BinaryExpression" || parentNode.expression.type === "LogicalExpression") {
106 if (parentNode.expression.right === node) {
107 return true;
108 }
109 }
110
111 return false;
112 }
113
114 //--------------------------------------------------------------------------
115 // Public
116 //--------------------------------------------------------------------------
117
118 return {
119 CallExpression(node) {
120
121 // if we're not a callback we can return
122 if (!isCallback(node)) {
123 return;
124 }
125
126 // find the closest block, return or loop
127 const closestBlock = findClosestParentOfType(node, ["BlockStatement", "ReturnStatement", "ArrowFunctionExpression"]) || {};
128
129 // if our parent is a return we know we're ok
130 if (closestBlock.type === "ReturnStatement") {
131 return;
132 }
133
134 // arrow functions don't always have blocks and implicitly return
135 if (closestBlock.type === "ArrowFunctionExpression") {
136 return;
137 }
138
139 // block statements are part of functions and most if statements
140 if (closestBlock.type === "BlockStatement") {
141
142 // find the last item in the block
143 const lastItem = closestBlock.body[closestBlock.body.length - 1];
144
145 // if the callback is the last thing in a block that might be ok
146 if (isCallbackExpression(node, lastItem)) {
147
148 const parentType = closestBlock.parent.type;
149
150 // but only if the block is part of a function
151 if (parentType === "FunctionExpression" ||
152 parentType === "FunctionDeclaration" ||
153 parentType === "ArrowFunctionExpression"
154 ) {
155 return;
156 }
157
158 }
159
160 // ending a block with a return is also ok
161 if (lastItem.type === "ReturnStatement") {
162
163 // but only if the callback is immediately before
164 if (isCallbackExpression(node, closestBlock.body[closestBlock.body.length - 2])) {
165 return;
166 }
167 }
168
169 }
170
171 // as long as you're the child of a function at this point you should be asked to return
172 if (findClosestParentOfType(node, ["FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression"])) {
173 context.report({ node, messageId: "missingReturn" });
174 }
175
176 }
177
178 };
179 }
180};