1/** 2 * @fileoverview restrict values that can be used as Promise rejection reasons 3 * @author Teddy Katz 4 */ 5"use strict"; 6 7const astUtils = require("./utils/ast-utils"); 8 9//------------------------------------------------------------------------------ 10// Rule Definition 11//------------------------------------------------------------------------------ 12 13module.exports = { 14 meta: { 15 type: "suggestion", 16 17 docs: { 18 description: "require using Error objects as Promise rejection reasons", 19 category: "Best Practices", 20 recommended: false, 21 url: "https://eslint.org/docs/rules/prefer-promise-reject-errors" 22 }, 23 24 fixable: null, 25 26 schema: [ 27 { 28 type: "object", 29 properties: { 30 allowEmptyReject: { type: "boolean", default: false } 31 }, 32 additionalProperties: false 33 } 34 ], 35 36 messages: { 37 rejectAnError: "Expected the Promise rejection reason to be an Error." 38 } 39 }, 40 41 create(context) { 42 43 const ALLOW_EMPTY_REJECT = context.options.length && context.options[0].allowEmptyReject; 44 45 //---------------------------------------------------------------------- 46 // Helpers 47 //---------------------------------------------------------------------- 48 49 /** 50 * Checks the argument of a reject() or Promise.reject() CallExpression, and reports it if it can't be an Error 51 * @param {ASTNode} callExpression A CallExpression node which is used to reject a Promise 52 * @returns {void} 53 */ 54 function checkRejectCall(callExpression) { 55 if (!callExpression.arguments.length && ALLOW_EMPTY_REJECT) { 56 return; 57 } 58 if ( 59 !callExpression.arguments.length || 60 !astUtils.couldBeError(callExpression.arguments[0]) || 61 callExpression.arguments[0].type === "Identifier" && callExpression.arguments[0].name === "undefined" 62 ) { 63 context.report({ 64 node: callExpression, 65 messageId: "rejectAnError" 66 }); 67 } 68 } 69 70 /** 71 * Determines whether a function call is a Promise.reject() call 72 * @param {ASTNode} node A CallExpression node 73 * @returns {boolean} `true` if the call is a Promise.reject() call 74 */ 75 function isPromiseRejectCall(node) { 76 return astUtils.isSpecificMemberAccess(node.callee, "Promise", "reject"); 77 } 78 79 //---------------------------------------------------------------------- 80 // Public 81 //---------------------------------------------------------------------- 82 83 return { 84 85 // Check `Promise.reject(value)` calls. 86 CallExpression(node) { 87 if (isPromiseRejectCall(node)) { 88 checkRejectCall(node); 89 } 90 }, 91 92 /* 93 * Check for `new Promise((resolve, reject) => {})`, and check for reject() calls. 94 * This function is run on "NewExpression:exit" instead of "NewExpression" to ensure that 95 * the nodes in the expression already have the `parent` property. 96 */ 97 "NewExpression:exit"(node) { 98 if ( 99 node.callee.type === "Identifier" && node.callee.name === "Promise" && 100 node.arguments.length && astUtils.isFunction(node.arguments[0]) && 101 node.arguments[0].params.length > 1 && node.arguments[0].params[1].type === "Identifier" 102 ) { 103 context.getDeclaredVariables(node.arguments[0]) 104 105 /* 106 * Find the first variable that matches the second parameter's name. 107 * If the first parameter has the same name as the second parameter, then the variable will actually 108 * be "declared" when the first parameter is evaluated, but then it will be immediately overwritten 109 * by the second parameter. It's not possible for an expression with the variable to be evaluated before 110 * the variable is overwritten, because functions with duplicate parameters cannot have destructuring or 111 * default assignments in their parameter lists. Therefore, it's not necessary to explicitly account for 112 * this case. 113 */ 114 .find(variable => variable.name === node.arguments[0].params[1].name) 115 116 // Get the references to that variable. 117 .references 118 119 // Only check the references that read the parameter's value. 120 .filter(ref => ref.isRead()) 121 122 // Only check the references that are used as the callee in a function call, e.g. `reject(foo)`. 123 .filter(ref => ref.identifier.parent.type === "CallExpression" && ref.identifier === ref.identifier.parent.callee) 124 125 // Check the argument of the function call to determine whether it's an Error. 126 .forEach(ref => checkRejectCall(ref.identifier.parent)); 127 } 128 } 129 }; 130 } 131}; 132