File: callback-return.js

package info (click to toggle)
node-eslint-plugin-node 11.1.0~ds-4
  • links: PTS, VCS
  • area: main
  • in suites: bullseye
  • size: 2,084 kB
  • sloc: javascript: 23,756; perl: 48; makefile: 38; sh: 32
file content (185 lines) | stat: -rw-r--r-- 6,559 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
/**
 * @author Jamund Ferguson
 * See LICENSE file in root directory for full license.
 */
"use strict"

module.exports = {
    meta: {
        docs: {
            description: "require `return` statements after callbacks",
            category: "Stylistic Issues",
            recommended: false,
            url:
                "https://github.com/mysticatea/eslint-plugin-node/blob/v11.1.0/docs/rules/callback-return.md",
        },
        fixable: null,
        messages: {
            missingReturn: "Expected return with your callback function.",
        },
        schema: [
            {
                type: "array",
                items: { type: "string" },
            },
        ],
        type: "suggestion",
    },

    create(context) {
        const callbacks = context.options[0] || ["callback", "cb", "next"]
        const sourceCode = context.getSourceCode()

        /**
         * 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
        }

        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" })
                }
            },
        }
    },
}