eslint-plugin-unicorn
Version:
More than 300 powerful ESLint rules
172 lines (148 loc) • 4.89 kB
JavaScript
import {isCommentToken, isCommaToken} from '@eslint-community/eslint-utils';
import {isFunction, isMethodCall, isUndefined} from './ast/index.js';
import {getArgumentRemovalRange} from './fix/index.js';
import {
getParentheses,
getParenthesizedText,
getTypeSymbol,
isGlobalIdentifier,
isDefaultLibrarySymbol,
isUnknownType,
unwrapTypeScriptExpression,
wouldRemoveComments,
} from './utils/index.js';
const MESSAGE_ID = 'prefer-then-catch';
const SUGGESTION_ID = 'prefer-then-catch/suggestion';
const messages = {
[MESSAGE_ID]: 'Prefer `.then(…).catch(…)` over passing a rejection handler to `.then()`.',
[SUGGESTION_ID]: 'Move the rejection handler to `.catch()`.',
};
const isNativePromiseType = (type, program) => {
const symbol = getTypeSymbol(type);
return symbol?.getName() === 'Promise'
&& isDefaultLibrarySymbol(symbol, program);
};
function isNullish(node, context) {
node = unwrapTypeScriptExpression(node);
return (node.type === 'Literal' && node.value === null)
|| (node.type === 'UnaryExpression' && node.operator === 'void')
|| (isUndefined(node) && isGlobalIdentifier(node, context));
}
function hasCallableCatch(type, checker, location) {
if (type.isUnion()) {
return type.types.every(type => hasCallableCatch(type, checker, location));
}
const catchMethod = checker.getPropertyOfType(type, 'catch');
return Boolean(catchMethod && checker.getTypeOfSymbolAtLocation(catchMethod, location).getCallSignatures().length > 0);
}
function canThenResultCatch(callExpression, context) {
const {parserServices} = context.sourceCode;
if (!parserServices?.program) {
return true;
}
try {
const checker = parserServices.program.getTypeChecker();
const receiverType = checker.getNonNullableType(parserServices.getTypeAtLocation(callExpression.callee.object));
if (isUnknownType(receiverType)) {
return true;
}
if (!isNativePromiseType(receiverType, parserServices.program)) {
return false;
}
const resultType = parserServices.getTypeAtLocation(callExpression);
return hasCallableCatch(resultType, checker, callExpression);
} catch {
// TypeScript can throw while resolving incomplete projects; keep this rule best-effort.
return true;
}
}
function getRejectionHandlerRemovalRange(node, context) {
const range = getArgumentRemovalRange(node, context);
const lastToken = getParentheses(node, context).at(-1) ?? node;
const trailingComma = context.sourceCode.getTokenAfter(lastToken);
if (isCommaToken(trailingComma)) {
range[1] = context.sourceCode.getRange(trailingComma)[1];
}
return range;
}
function hasTrailingArgumentComment(node, context) {
const {sourceCode} = context;
const lastToken = getParentheses(node, context).at(-1) ?? node;
let tokenAfter = sourceCode.getTokenAfter(lastToken, {includeComments: true});
if (isCommentToken(tokenAfter)) {
return true;
}
if (isCommaToken(tokenAfter)) {
tokenAfter = sourceCode.getTokenAfter(tokenAfter, {includeComments: true});
}
return isCommentToken(tokenAfter);
}
function isRejectionHandlerSafeToMove(node) {
node = unwrapTypeScriptExpression(node);
return node.type === 'Identifier'
|| isFunction(node);
}
function getSuggestion(callExpression, rejectionHandler, context) {
const removalRange = getRejectionHandlerRemovalRange(rejectionHandler, context);
if (
!isRejectionHandlerSafeToMove(rejectionHandler)
|| wouldRemoveComments(context, removalRange, [rejectionHandler])
|| hasTrailingArgumentComment(rejectionHandler, context)
) {
return;
}
const rejectionHandlerText = getParenthesizedText(rejectionHandler, context);
return {
messageId: SUGGESTION_ID,
* fix(fixer) {
yield fixer.removeRange(removalRange);
yield fixer.insertTextAfter(callExpression, `.catch(${rejectionHandlerText})`);
},
};
}
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
context.on('CallExpression', callExpression => {
if (!isMethodCall(callExpression, {
method: 'then',
argumentsLength: 2,
optionalCall: false,
optionalMember: false,
computed: false,
})) {
return;
}
const [fulfillmentHandler, rejectionHandler] = callExpression.arguments;
if (
isNullish(fulfillmentHandler, context)
|| isNullish(rejectionHandler, context)
|| !canThenResultCatch(callExpression, context)
) {
return;
}
const suggestion = getSuggestion(callExpression, rejectionHandler, context);
return {
node: callExpression.callee.property,
messageId: MESSAGE_ID,
...(suggestion && {suggest: [suggestion]}),
};
});
};
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'problem',
docs: {
description: 'Prefer `.then().catch()` over `.then(…, …)` for error handling.',
recommended: true,
},
hasSuggestions: true,
schema: [],
messages,
languages: [
'js/js',
],
},
};
export default config;