'use strict'; const {matches, methodCallSelector} = require('./selectors/index.js'); const {getParenthesizedRange} = require('./utils/parentheses.js'); const MESSAGE_ID_RESOLVE = 'resolve'; const MESSAGE_ID_REJECT = 'reject'; const messages = { [MESSAGE_ID_RESOLVE]: 'Prefer `{{type}} value` over `{{type}} Promise.resolve(value)`.', [MESSAGE_ID_REJECT]: 'Prefer `throw error` over `{{type}} Promise.reject(error)`.', }; const selector = [ methodCallSelector({ object: 'Promise', methods: ['resolve', 'reject'], }), matches([ 'ArrowFunctionExpression > .body', 'ReturnStatement > .argument', 'YieldExpression[delegate!=true] > .argument', ]), ].join(''); const functionTypes = new Set([ 'ArrowFunctionExpression', 'FunctionDeclaration', 'FunctionExpression', ]); function getFunctionNode(node) { let isInTryStatement = false; let functionNode; for (; node; node = node.parent) { if (functionTypes.has(node.type)) { functionNode = node; break; } if (node.type === 'TryStatement') { isInTryStatement = true; } } return { functionNode, isInTryStatement, }; } function isPromiseCallback(node) { if ( node.parent.type === 'CallExpression' && node.parent.callee.type === 'MemberExpression' && !node.parent.callee.computed && node.parent.callee.property.type === 'Identifier' ) { const {callee: {property}, arguments: arguments_} = node.parent; if ( arguments_.length === 1 && ( property.name === 'then' || property.name === 'catch' || property.name === 'finally' ) && arguments_[0] === node ) { return true; } if ( arguments_.length === 2 && property.name === 'then' && ( arguments_[0] === node || (arguments_[0].type !== 'SpreadElement' && arguments_[1] === node) ) ) { return true; } } return false; } function createProblem(callExpression, fix) { const {callee, parent} = callExpression; const method = callee.property.name; const type = parent.type === 'YieldExpression' ? 'yield' : 'return'; return { node: callee, messageId: method, data: {type}, fix, }; } function fix(callExpression, isInTryStatement, sourceCode) { if (callExpression.arguments.length > 1) { return; } const {callee, parent, arguments: [errorOrValue]} = callExpression; if (errorOrValue?.type === 'SpreadElement') { return; } const isReject = callee.property.name === 'reject'; const isYieldExpression = parent.type === 'YieldExpression'; if ( isReject && ( isInTryStatement || (isYieldExpression && parent.parent.type !== 'ExpressionStatement') ) ) { return; } return function (fixer) { const isArrowFunctionBody = parent.type === 'ArrowFunctionExpression'; let text = errorOrValue ? sourceCode.getText(errorOrValue) : ''; if (errorOrValue?.type === 'SequenceExpression') { text = `(${text})`; } if (isReject) { // `return Promise.reject()` -> `throw undefined` text = text || 'undefined'; text = `throw ${text}`; if (isYieldExpression) { return fixer.replaceTextRange( getParenthesizedRange(parent, sourceCode), text, ); } text += ';'; // `=> Promise.reject(error)` -> `=> { throw error; }` if (isArrowFunctionBody) { text = `{ ${text} }`; return fixer.replaceTextRange( getParenthesizedRange(callExpression, sourceCode), text, ); } } else { // eslint-disable-next-line no-lonely-if if (isYieldExpression) { text = `yield${text ? ' ' : ''}${text}`; } else if (parent.type === 'ReturnStatement') { text = `return${text ? ' ' : ''}${text};`; } else { if (errorOrValue?.type === 'ObjectExpression') { text = `(${text})`; } // `=> Promise.resolve()` -> `=> {}` text = text || '{}'; } } return fixer.replaceText( isArrowFunctionBody ? callExpression : parent, text, ); }; } /** @param {import('eslint').Rule.RuleContext} context */ const create = context => { const sourceCode = context.getSourceCode(); return { [selector](callExpression) { const {functionNode, isInTryStatement} = getFunctionNode(callExpression); if (!functionNode || !(functionNode.async || isPromiseCallback(functionNode))) { return; } return createProblem( callExpression, fix(callExpression, isInTryStatement, sourceCode), ); }, }; }; /** @type {import('eslint').Rule.RuleModule} */ module.exports = { create, meta: { type: 'suggestion', docs: { description: 'Disallow returning/yielding `Promise.resolve/reject()` in async functions or promise callbacks', }, fixable: 'code', messages, }, };