UNPKG

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