prefer-promise-reject-errors.js 4.5 KB

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