diff --git a/docs/rules/no-promise-reject.md b/docs/rules/no-promise-reject.md index 7d1bac258..9501d95c7 100644 --- a/docs/rules/no-promise-reject.md +++ b/docs/rules/no-promise-reject.md @@ -2,15 +2,13 @@ -This rule disallows use of `Promise.reject()`. +This rule disallows rejecting promises. ## Rule Details It is useful when using an `Option` type (something like `{ value: T } | { error: Error }`) for handling errors. In this case a promise should always resolve with an `Option` and never reject. -This rule should be used in conjunction with [`no-throw-statements`](./no-throw-statements.md). - ### ❌ Incorrect diff --git a/src/rules/no-promise-reject.ts b/src/rules/no-promise-reject.ts index 7300fe032..cda4e0c4b 100644 --- a/src/rules/no-promise-reject.ts +++ b/src/rules/no-promise-reject.ts @@ -8,7 +8,12 @@ import { type NamedCreateRuleCustomMeta, type RuleResult, } from "#/utils/rule"; -import { isIdentifier, isMemberExpression } from "#/utils/type-guards"; +import { getEnclosingFunction, getEnclosingTryStatement } from "#/utils/tree"; +import { + isFunctionLike, + isIdentifier, + isMemberExpression, +} from "#/utils/type-guards"; /** * The name of this rule. @@ -39,7 +44,7 @@ const defaultOptions: Options = [{}]; * The possible error messages. */ const errorMessages = { - generic: "Unexpected reject, return an error instead.", + generic: "Unexpected rejection, resolve an error instead.", } as const; /** @@ -67,6 +72,7 @@ function checkCallExpression( return { context, descriptors: + // TODO: Better Promise type detection. isMemberExpression(node.callee) && isIdentifier(node.callee.object) && isIdentifier(node.callee.property) && @@ -77,6 +83,57 @@ function checkCallExpression( }; } +/** + * Check if the given NewExpression is for a Promise and it has a callback that rejects. + */ +function checkNewExpression( + node: TSESTree.NewExpression, + context: Readonly>, +): RuleResult { + return { + context, + descriptors: + // TODO: Better Promise type detection. + isIdentifier(node.callee) && + node.callee.name === "Promise" && + node.arguments[0] !== undefined && + isFunctionLike(node.arguments[0]) && + node.arguments[0].params.length === 2 + ? [{ node: node.arguments[0].params[1]!, messageId: "generic" }] + : [], + }; +} + +/** + * Check if the given ThrowStatement violates this rule. + */ +function checkThrowStatement( + node: TSESTree.ThrowStatement, + context: Readonly>, +): RuleResult { + const enclosingFunction = getEnclosingFunction(node); + if (enclosingFunction?.async !== true) { + return { context, descriptors: [] }; + } + + const enclosingTryStatement = getEnclosingTryStatement(node); + if ( + enclosingTryStatement === null || + getEnclosingFunction(enclosingTryStatement) !== enclosingFunction || + enclosingTryStatement.handler === null + ) { + return { + context, + descriptors: [{ node, messageId: "generic" }], + }; + } + + return { + context, + descriptors: [], + }; +} + // Create the rule. export const rule = createRule( name, @@ -84,5 +141,7 @@ export const rule = createRule( defaultOptions, { CallExpression: checkCallExpression, + NewExpression: checkNewExpression, + ThrowStatement: checkThrowStatement, }, ); diff --git a/src/utils/tree.ts b/src/utils/tree.ts index 7ad1b2c14..d15e31665 100644 --- a/src/utils/tree.ts +++ b/src/utils/tree.ts @@ -24,6 +24,7 @@ import { isTSTypeAnnotation, isTSTypeLiteral, isTSTypeReference, + isTryStatement, isVariableDeclaration, } from "./type-guards"; @@ -52,7 +53,21 @@ export function isInFunctionBody( node: TSESTree.Node, async?: boolean, ): boolean { - const functionNode = getAncestorOfType( + const functionNode = getEnclosingFunction(node); + + return ( + functionNode !== null && + (async === undefined || functionNode.async === async) + ); +} + +/** + * Get the function the given node is in. + * + * Will return null if not in a function. + */ +export function getEnclosingFunction(node: TSESTree.Node) { + return getAncestorOfType( ( n, c, @@ -62,10 +77,17 @@ export function isInFunctionBody( | TSESTree.FunctionExpression => isFunctionLike(n) && n.body === c, node, ); +} - return ( - functionNode !== null && - (async === undefined || functionNode.async === async) +/** + * Get the function the given node is in. + * + * Will return null if not in a function. + */ +export function getEnclosingTryStatement(node: TSESTree.Node) { + return getAncestorOfType( + (n, c): n is TSESTree.TryStatement => isTryStatement(n) && n.block === c, + node, ); } diff --git a/src/utils/type-guards.ts b/src/utils/type-guards.ts index 87af3b85e..a2b0b646e 100644 --- a/src/utils/type-guards.ts +++ b/src/utils/type-guards.ts @@ -237,6 +237,12 @@ export function isThrowStatement( return node.type === AST_NODE_TYPES.ThrowStatement; } +export function isTryStatement( + node: TSESTree.Node, +): node is TSESTree.TryStatement { + return node.type === AST_NODE_TYPES.TryStatement; +} + export function isTSArrayType( node: TSESTree.Node, ): node is TSESTree.TSArrayType { @@ -440,3 +446,12 @@ export function isObjectConstructorType(type: Type | null): boolean { export function isFunctionLikeType(type: Type | null): boolean { return type !== null && type.getCallSignatures().length > 0; } + +export function isPromiseType(type: Type | null): boolean { + return ( + type !== null && + (((type.symbol as unknown) !== undefined && + type.symbol.name === "Promise") || + (isUnionType(type) && type.types.some(isPromiseType))) + ); +}