From d880185f35eca4d66641c7b35cd4b94ad79df10b Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Mon, 24 Jun 2024 23:02:19 +1200 Subject: [PATCH] feat(no-throw-statements)!: replace option `allowInAsyncFunctions` with `allowToRejectPromises` --- docs/rules/no-throw-statements.md | 12 +++++------ src/configs/recommended.ts | 2 +- src/rules/no-throw-statements.ts | 36 +++++++++++++++++++++++++------ src/utils/tree.ts | 27 ++++++++++++++++++++++- 4 files changed, 63 insertions(+), 14 deletions(-) diff --git a/docs/rules/no-throw-statements.md b/docs/rules/no-throw-statements.md index 862b9c975..b4cbbdde0 100644 --- a/docs/rules/no-throw-statements.md +++ b/docs/rules/no-throw-statements.md @@ -54,7 +54,7 @@ This rule accepts an options object of the following type: ```ts type Options = { - allowInAsyncFunctions: boolean; + allowToRejectPromises: boolean; }; ``` @@ -62,7 +62,7 @@ type Options = { ```ts const defaults = { - allowInAsyncFunctions: false, + allowToRejectPromises: false, }; ``` @@ -72,19 +72,19 @@ const defaults = { ```ts const recommendedAndLiteOptions = { - allowInAsyncFunctions: true, + allowToRejectPromises: true, }; ``` -### `allowInAsyncFunctions` +### `allowToRejectPromises` -If true, throw statements will be allowed within async functions.\ +If true, throw statements will be allowed when they are used to reject a promise, such when in an async function.\ This essentially allows throw statements to be used as return statements for errors. #### ✅ Correct ```js -/* eslint functional/no-throw-statements: ["error", { "allowInAsyncFunctions": true }] */ +/* eslint functional/no-throw-statements: ["error", { "allowToRejectPromises": true }] */ async function divide(x, y) { const [xv, yv] = await Promise.all([x, y]); diff --git a/src/configs/recommended.ts b/src/configs/recommended.ts index d67d2ac7b..5a77b53ab 100644 --- a/src/configs/recommended.ts +++ b/src/configs/recommended.ts @@ -66,7 +66,7 @@ const overrides = { [noThrowStatements.fullName]: [ "error", { - allowInAsyncFunctions: true, + allowToRejectPromises: true, }, ], [noTryStatements.fullName]: "off", diff --git a/src/rules/no-throw-statements.ts b/src/rules/no-throw-statements.ts index 22c3840bd..4642b0253 100644 --- a/src/rules/no-throw-statements.ts +++ b/src/rules/no-throw-statements.ts @@ -8,7 +8,11 @@ import { type RuleResult, createRule, } from "#/utils/rule"; -import { isInFunctionBody } from "#/utils/tree"; +import { + getEnclosingFunction, + getEnclosingTryStatement, + isInPromiseHandlerFunction, +} from "#/utils/tree"; /** * The name of this rule. @@ -25,7 +29,7 @@ export const fullName = `${ruleNameScope}/${name}`; */ type Options = [ { - allowInAsyncFunctions: boolean; + allowToRejectPromises: boolean; }, ]; @@ -36,7 +40,7 @@ const schema: JSONSchema4[] = [ { type: "object", properties: { - allowInAsyncFunctions: { + allowToRejectPromises: { type: "boolean", }, }, @@ -49,7 +53,7 @@ const schema: JSONSchema4[] = [ */ const defaultOptions: Options = [ { - allowInAsyncFunctions: false, + allowToRejectPromises: false, }, ]; @@ -84,9 +88,29 @@ function checkThrowStatement( context: Readonly>, options: Readonly, ): RuleResult { - const [{ allowInAsyncFunctions }] = options; + const [{ allowToRejectPromises }] = options; + + if (!allowToRejectPromises) { + return { context, descriptors: [{ node, messageId: "generic" }] }; + } + + if (isInPromiseHandlerFunction(node, context)) { + return { context, descriptors: [] }; + } + + const enclosingFunction = getEnclosingFunction(node); + if (enclosingFunction?.async !== true) { + return { context, descriptors: [{ node, messageId: "generic" }] }; + } - if (!allowInAsyncFunctions || !isInFunctionBody(node, true)) { + const enclosingTryStatement = getEnclosingTryStatement(node); + if ( + !( + enclosingTryStatement === null || + getEnclosingFunction(enclosingTryStatement) !== enclosingFunction || + enclosingTryStatement.handler === null + ) + ) { return { context, descriptors: [{ node, messageId: "generic" }] }; } diff --git a/src/utils/tree.ts b/src/utils/tree.ts index 17b0d9fe1..28754f920 100644 --- a/src/utils/tree.ts +++ b/src/utils/tree.ts @@ -4,7 +4,7 @@ import { type RuleContext } from "@typescript-eslint/utils/ts-eslint"; import typescript from "#/conditional-imports/typescript"; -import { type BaseOptions } from "./rule"; +import { type BaseOptions, getTypeOfNode } from "./rule"; import { isBlockStatement, isCallExpression, @@ -18,6 +18,7 @@ import { isMethodDefinition, isObjectExpression, isProgram, + isPromiseType, isProperty, isTSInterfaceBody, isTSInterfaceHeritage, @@ -117,6 +118,30 @@ export function isInReadonly(node: TSESTree.Node): boolean { return getReadonly(node) !== null; } +/** + * Test if the given node is in a handler function callback of a promise. + */ +export function isInPromiseHandlerFunction< + Context extends RuleContext, +>(node: TSESTree.Node, context: Context): boolean { + const functionNode = getAncestorOfType( + (n, c): n is TSESTree.FunctionLike => isFunctionLike(n) && n.body === c, + node, + ); + + if ( + functionNode === null || + !isCallExpression(functionNode.parent) || + !isMemberExpression(functionNode.parent.callee) || + !isIdentifier(functionNode.parent.callee.property) + ) { + return false; + } + + const objectType = getTypeOfNode(functionNode.parent.callee.object, context); + return isPromiseType(objectType); +} + /** * Test if the given node is shallowly inside a `Readonly<{...}>`. */