prefer-promise-reject-errors.js 5.31 KB
Newer Older
Rosanny Sihombing's avatar
Rosanny Sihombing committed
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
/**
 * @fileoverview restrict values that can be used as Promise rejection reasons
 * @author Teddy Katz
 */
"use strict";

const astUtils = require("./utils/ast-utils");

//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------

module.exports = {
    meta: {
        type: "suggestion",

        docs: {
            description: "require using Error objects as Promise rejection reasons",
            category: "Best Practices",
            recommended: false,
            url: "https://eslint.org/docs/rules/prefer-promise-reject-errors"
        },

        fixable: null,

        schema: [
            {
                type: "object",
                properties: {
                    allowEmptyReject: { type: "boolean", default: false }
                },
                additionalProperties: false
            }
        ],

        messages: {
            rejectAnError: "Expected the Promise rejection reason to be an Error."
        }
    },

    create(context) {

        const ALLOW_EMPTY_REJECT = context.options.length && context.options[0].allowEmptyReject;

        //----------------------------------------------------------------------
        // Helpers
        //----------------------------------------------------------------------

        /**
         * Checks the argument of a reject() or Promise.reject() CallExpression, and reports it if it can't be an Error
         * @param {ASTNode} callExpression A CallExpression node which is used to reject a Promise
         * @returns {void}
         */
        function checkRejectCall(callExpression) {
            if (!callExpression.arguments.length && ALLOW_EMPTY_REJECT) {
                return;
            }
            if (
                !callExpression.arguments.length ||
                !astUtils.couldBeError(callExpression.arguments[0]) ||
                callExpression.arguments[0].type === "Identifier" && callExpression.arguments[0].name === "undefined"
            ) {
                context.report({
                    node: callExpression,
                    messageId: "rejectAnError"
                });
            }
        }

        /**
         * Determines whether a function call is a Promise.reject() call
         * @param {ASTNode} node A CallExpression node
         * @returns {boolean} `true` if the call is a Promise.reject() call
         */
        function isPromiseRejectCall(node) {
            return astUtils.isSpecificMemberAccess(node.callee, "Promise", "reject");
        }

        //----------------------------------------------------------------------
        // Public
        //----------------------------------------------------------------------

        return {

            // Check `Promise.reject(value)` calls.
            CallExpression(node) {
                if (isPromiseRejectCall(node)) {
                    checkRejectCall(node);
                }
            },

            /*
             * Check for `new Promise((resolve, reject) => {})`, and check for reject() calls.
             * This function is run on "NewExpression:exit" instead of "NewExpression" to ensure that
             * the nodes in the expression already have the `parent` property.
             */
            "NewExpression:exit"(node) {
                if (
                    node.callee.type === "Identifier" && node.callee.name === "Promise" &&
                    node.arguments.length && astUtils.isFunction(node.arguments[0]) &&
                    node.arguments[0].params.length > 1 && node.arguments[0].params[1].type === "Identifier"
                ) {
                    context.getDeclaredVariables(node.arguments[0])

                        /*
                         * Find the first variable that matches the second parameter's name.
                         * If the first parameter has the same name as the second parameter, then the variable will actually
                         * be "declared" when the first parameter is evaluated, but then it will be immediately overwritten
                         * by the second parameter. It's not possible for an expression with the variable to be evaluated before
                         * the variable is overwritten, because functions with duplicate parameters cannot have destructuring or
                         * default assignments in their parameter lists. Therefore, it's not necessary to explicitly account for
                         * this case.
                         */
                        .find(variable => variable.name === node.arguments[0].params[1].name)

                        // Get the references to that variable.
                        .references

                        // Only check the references that read the parameter's value.
                        .filter(ref => ref.isRead())

                        // Only check the references that are used as the callee in a function call, e.g. `reject(foo)`.
                        .filter(ref => ref.identifier.parent.type === "CallExpression" && ref.identifier === ref.identifier.parent.callee)

                        // Check the argument of the function call to determine whether it's an Error.
                        .forEach(ref => checkRejectCall(ref.identifier.parent));
                }
            }
        };
    }
};