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