| /** |
| * @fileoverview Enforce return after a callback. |
| * @author Jamund Ferguson |
| */ |
| "use strict"; |
| |
| //------------------------------------------------------------------------------ |
| // Rule Definition |
| //------------------------------------------------------------------------------ |
| |
| module.exports = { |
| meta: { |
| type: "suggestion", |
| |
| docs: { |
| description: "require `return` statements after callbacks", |
| category: "Node.js and CommonJS", |
| recommended: false, |
| url: "https://eslint.org/docs/rules/callback-return" |
| }, |
| |
| schema: [{ |
| type: "array", |
| items: { type: "string" } |
| }], |
| |
| messages: { |
| missingReturn: "Expected return with your callback function." |
| } |
| }, |
| |
| create(context) { |
| |
| const callbacks = context.options[0] || ["callback", "cb", "next"], |
| sourceCode = context.getSourceCode(); |
| |
| //-------------------------------------------------------------------------- |
| // Helpers |
| //-------------------------------------------------------------------------- |
| |
| /** |
| * Find the closest parent matching a list of types. |
| * @param {ASTNode} node The node whose parents we are searching |
| * @param {Array} types The node types to match |
| * @returns {ASTNode} The matched node or undefined. |
| */ |
| function findClosestParentOfType(node, types) { |
| if (!node.parent) { |
| return null; |
| } |
| if (types.indexOf(node.parent.type) === -1) { |
| return findClosestParentOfType(node.parent, types); |
| } |
| return node.parent; |
| } |
| |
| /** |
| * Check to see if a node contains only identifers |
| * @param {ASTNode} node The node to check |
| * @returns {boolean} Whether or not the node contains only identifers |
| */ |
| function containsOnlyIdentifiers(node) { |
| if (node.type === "Identifier") { |
| return true; |
| } |
| |
| if (node.type === "MemberExpression") { |
| if (node.object.type === "Identifier") { |
| return true; |
| } |
| if (node.object.type === "MemberExpression") { |
| return containsOnlyIdentifiers(node.object); |
| } |
| } |
| |
| return false; |
| } |
| |
| /** |
| * Check to see if a CallExpression is in our callback list. |
| * @param {ASTNode} node The node to check against our callback names list. |
| * @returns {boolean} Whether or not this function matches our callback name. |
| */ |
| function isCallback(node) { |
| return containsOnlyIdentifiers(node.callee) && callbacks.indexOf(sourceCode.getText(node.callee)) > -1; |
| } |
| |
| /** |
| * Determines whether or not the callback is part of a callback expression. |
| * @param {ASTNode} node The callback node |
| * @param {ASTNode} parentNode The expression node |
| * @returns {boolean} Whether or not this is part of a callback expression |
| */ |
| function isCallbackExpression(node, parentNode) { |
| |
| // ensure the parent node exists and is an expression |
| if (!parentNode || parentNode.type !== "ExpressionStatement") { |
| return false; |
| } |
| |
| // cb() |
| if (parentNode.expression === node) { |
| return true; |
| } |
| |
| // special case for cb && cb() and similar |
| if (parentNode.expression.type === "BinaryExpression" || parentNode.expression.type === "LogicalExpression") { |
| if (parentNode.expression.right === node) { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| //-------------------------------------------------------------------------- |
| // Public |
| //-------------------------------------------------------------------------- |
| |
| return { |
| CallExpression(node) { |
| |
| // if we're not a callback we can return |
| if (!isCallback(node)) { |
| return; |
| } |
| |
| // find the closest block, return or loop |
| const closestBlock = findClosestParentOfType(node, ["BlockStatement", "ReturnStatement", "ArrowFunctionExpression"]) || {}; |
| |
| // if our parent is a return we know we're ok |
| if (closestBlock.type === "ReturnStatement") { |
| return; |
| } |
| |
| // arrow functions don't always have blocks and implicitly return |
| if (closestBlock.type === "ArrowFunctionExpression") { |
| return; |
| } |
| |
| // block statements are part of functions and most if statements |
| if (closestBlock.type === "BlockStatement") { |
| |
| // find the last item in the block |
| const lastItem = closestBlock.body[closestBlock.body.length - 1]; |
| |
| // if the callback is the last thing in a block that might be ok |
| if (isCallbackExpression(node, lastItem)) { |
| |
| const parentType = closestBlock.parent.type; |
| |
| // but only if the block is part of a function |
| if (parentType === "FunctionExpression" || |
| parentType === "FunctionDeclaration" || |
| parentType === "ArrowFunctionExpression" |
| ) { |
| return; |
| } |
| |
| } |
| |
| // ending a block with a return is also ok |
| if (lastItem.type === "ReturnStatement") { |
| |
| // but only if the callback is immediately before |
| if (isCallbackExpression(node, closestBlock.body[closestBlock.body.length - 2])) { |
| return; |
| } |
| } |
| |
| } |
| |
| // as long as you're the child of a function at this point you should be asked to return |
| if (findClosestParentOfType(node, ["FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression"])) { |
| context.report({ node, messageId: "missingReturn" }); |
| } |
| |
| } |
| |
| }; |
| } |
| }; |