• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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