no-useless-promise-resolve-reject.js 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. 'use strict';
  2. const {matches, methodCallSelector} = require('./selectors/index.js');
  3. const {getParenthesizedRange} = require('./utils/parentheses.js');
  4. const MESSAGE_ID_RESOLVE = 'resolve';
  5. const MESSAGE_ID_REJECT = 'reject';
  6. const messages = {
  7. [MESSAGE_ID_RESOLVE]: 'Prefer `{{type}} value` over `{{type}} Promise.resolve(value)`.',
  8. [MESSAGE_ID_REJECT]: 'Prefer `throw error` over `{{type}} Promise.reject(error)`.',
  9. };
  10. const selector = [
  11. methodCallSelector({
  12. object: 'Promise',
  13. methods: ['resolve', 'reject'],
  14. }),
  15. matches([
  16. 'ArrowFunctionExpression > .body',
  17. 'ReturnStatement > .argument',
  18. 'YieldExpression[delegate!=true] > .argument',
  19. ]),
  20. ].join('');
  21. const functionTypes = new Set([
  22. 'ArrowFunctionExpression',
  23. 'FunctionDeclaration',
  24. 'FunctionExpression',
  25. ]);
  26. function getFunctionNode(node) {
  27. let isInTryStatement = false;
  28. let functionNode;
  29. for (; node; node = node.parent) {
  30. if (functionTypes.has(node.type)) {
  31. functionNode = node;
  32. break;
  33. }
  34. if (node.type === 'TryStatement') {
  35. isInTryStatement = true;
  36. }
  37. }
  38. return {
  39. functionNode,
  40. isInTryStatement,
  41. };
  42. }
  43. function isPromiseCallback(node) {
  44. if (
  45. node.parent.type === 'CallExpression'
  46. && node.parent.callee.type === 'MemberExpression'
  47. && !node.parent.callee.computed
  48. && node.parent.callee.property.type === 'Identifier'
  49. ) {
  50. const {callee: {property}, arguments: arguments_} = node.parent;
  51. if (
  52. arguments_.length === 1
  53. && (
  54. property.name === 'then'
  55. || property.name === 'catch'
  56. || property.name === 'finally'
  57. )
  58. && arguments_[0] === node
  59. ) {
  60. return true;
  61. }
  62. if (
  63. arguments_.length === 2
  64. && property.name === 'then'
  65. && (
  66. arguments_[0] === node
  67. || (arguments_[0].type !== 'SpreadElement' && arguments_[1] === node)
  68. )
  69. ) {
  70. return true;
  71. }
  72. }
  73. return false;
  74. }
  75. function createProblem(callExpression, fix) {
  76. const {callee, parent} = callExpression;
  77. const method = callee.property.name;
  78. const type = parent.type === 'YieldExpression' ? 'yield' : 'return';
  79. return {
  80. node: callee,
  81. messageId: method,
  82. data: {type},
  83. fix,
  84. };
  85. }
  86. function fix(callExpression, isInTryStatement, sourceCode) {
  87. if (callExpression.arguments.length > 1) {
  88. return;
  89. }
  90. const {callee, parent, arguments: [errorOrValue]} = callExpression;
  91. if (errorOrValue?.type === 'SpreadElement') {
  92. return;
  93. }
  94. const isReject = callee.property.name === 'reject';
  95. const isYieldExpression = parent.type === 'YieldExpression';
  96. if (
  97. isReject
  98. && (
  99. isInTryStatement
  100. || (isYieldExpression && parent.parent.type !== 'ExpressionStatement')
  101. )
  102. ) {
  103. return;
  104. }
  105. return function (fixer) {
  106. const isArrowFunctionBody = parent.type === 'ArrowFunctionExpression';
  107. let text = errorOrValue ? sourceCode.getText(errorOrValue) : '';
  108. if (errorOrValue?.type === 'SequenceExpression') {
  109. text = `(${text})`;
  110. }
  111. if (isReject) {
  112. // `return Promise.reject()` -> `throw undefined`
  113. text = text || 'undefined';
  114. text = `throw ${text}`;
  115. if (isYieldExpression) {
  116. return fixer.replaceTextRange(
  117. getParenthesizedRange(parent, sourceCode),
  118. text,
  119. );
  120. }
  121. text += ';';
  122. // `=> Promise.reject(error)` -> `=> { throw error; }`
  123. if (isArrowFunctionBody) {
  124. text = `{ ${text} }`;
  125. return fixer.replaceTextRange(
  126. getParenthesizedRange(callExpression, sourceCode),
  127. text,
  128. );
  129. }
  130. } else {
  131. // eslint-disable-next-line no-lonely-if
  132. if (isYieldExpression) {
  133. text = `yield${text ? ' ' : ''}${text}`;
  134. } else if (parent.type === 'ReturnStatement') {
  135. text = `return${text ? ' ' : ''}${text};`;
  136. } else {
  137. if (errorOrValue?.type === 'ObjectExpression') {
  138. text = `(${text})`;
  139. }
  140. // `=> Promise.resolve()` -> `=> {}`
  141. text = text || '{}';
  142. }
  143. }
  144. return fixer.replaceText(
  145. isArrowFunctionBody ? callExpression : parent,
  146. text,
  147. );
  148. };
  149. }
  150. /** @param {import('eslint').Rule.RuleContext} context */
  151. const create = context => {
  152. const sourceCode = context.getSourceCode();
  153. return {
  154. [selector](callExpression) {
  155. const {functionNode, isInTryStatement} = getFunctionNode(callExpression);
  156. if (!functionNode || !(functionNode.async || isPromiseCallback(functionNode))) {
  157. return;
  158. }
  159. return createProblem(
  160. callExpression,
  161. fix(callExpression, isInTryStatement, sourceCode),
  162. );
  163. },
  164. };
  165. };
  166. /** @type {import('eslint').Rule.RuleModule} */
  167. module.exports = {
  168. create,
  169. meta: {
  170. type: 'suggestion',
  171. docs: {
  172. description: 'Disallow returning/yielding `Promise.resolve/reject()` in async functions or promise callbacks',
  173. },
  174. fixable: 'code',
  175. messages,
  176. },
  177. };