UNPKG

eslint

Version:

An AST-based pattern checker for JavaScript.

217 lines (189 loc) 5.88 kB
/** * @fileoverview Enforce return after a callback. * @author Jamund Ferguson * @deprecated in ESLint v7.0.0 */ "use strict"; //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ /** @type {import('../types').Rule.RuleModule} */ module.exports = { meta: { deprecated: { message: "Node.js rules were moved out of ESLint core.", url: "https://eslint.org/docs/latest/use/migrating-to-7.0.0#deprecate-node-rules", deprecatedSince: "7.0.0", availableUntil: null, replacedBy: [ { message: "eslint-plugin-n now maintains deprecated Node.js-related rules.", plugin: { name: "eslint-plugin-n", url: "https://github.com/eslint-community/eslint-plugin-n", }, rule: { name: "callback-return", url: "https://github.com/eslint-community/eslint-plugin-n/tree/master/docs/rules/callback-return.md", }, }, ], }, type: "suggestion", docs: { description: "Require `return` statements after callbacks", recommended: false, url: "https://eslint.org/docs/latest/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.sourceCode; //-------------------------------------------------------------------------- // 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.includes(node.parent.type)) { return findClosestParentOfType(node.parent, types); } return node.parent; } /** * Check to see if a node contains only identifiers * @param {ASTNode} node The node to check * @returns {boolean} Whether or not the node contains only identifiers */ 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.includes(sourceCode.getText(node.callee)) ); } /** * 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.at(-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.at(-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" }); } }, }; }, };