Skip to content

Commit e1a8b59

Browse files
feat(no-throw-statements)!: replace option allowInAsyncFunctions with allowToRejectPromises
1 parent 38486c0 commit e1a8b59

File tree

4 files changed

+63
-14
lines changed

4 files changed

+63
-14
lines changed

docs/rules/no-throw-statements.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,15 +54,15 @@ This rule accepts an options object of the following type:
5454

5555
```ts
5656
type Options = {
57-
allowInAsyncFunctions: boolean;
57+
allowToRejectPromises: boolean;
5858
};
5959
```
6060

6161
### Default Options
6262

6363
```ts
6464
const defaults = {
65-
allowInAsyncFunctions: false,
65+
allowToRejectPromises: false,
6666
};
6767
```
6868

@@ -72,19 +72,19 @@ const defaults = {
7272

7373
```ts
7474
const recommendedAndLiteOptions = {
75-
allowInAsyncFunctions: true,
75+
allowToRejectPromises: true,
7676
};
7777
```
7878

79-
### `allowInAsyncFunctions`
79+
### `allowToRejectPromises`
8080

81-
If true, throw statements will be allowed within async functions.\
81+
If true, throw statements will be allowed when they are used to reject a promise, such when in an async function.\
8282
This essentially allows throw statements to be used as return statements for errors.
8383

8484
#### ✅ Correct
8585

8686
```js
87-
/* eslint functional/no-throw-statements: ["error", { "allowInAsyncFunctions": true }] */
87+
/* eslint functional/no-throw-statements: ["error", { "allowToRejectPromises": true }] */
8888

8989
async function divide(x, y) {
9090
const [xv, yv] = await Promise.all([x, y]);

src/configs/recommended.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ const overrides = {
6666
[noThrowStatements.fullName]: [
6767
"error",
6868
{
69-
allowInAsyncFunctions: true,
69+
allowToRejectPromises: true,
7070
},
7171
],
7272
[noTryStatements.fullName]: "off",

src/rules/no-throw-statements.ts

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ import {
88
type RuleResult,
99
createRule,
1010
} from "#/utils/rule";
11-
import { isInFunctionBody } from "#/utils/tree";
11+
import {
12+
getEnclosingFunction,
13+
getEnclosingTryStatement,
14+
isInPromiseHandlerFunction,
15+
} from "#/utils/tree";
1216

1317
/**
1418
* The name of this rule.
@@ -25,7 +29,7 @@ export const fullName = `${ruleNameScope}/${name}`;
2529
*/
2630
type Options = [
2731
{
28-
allowInAsyncFunctions: boolean;
32+
allowToRejectPromises: boolean;
2933
},
3034
];
3135

@@ -36,7 +40,7 @@ const schema: JSONSchema4[] = [
3640
{
3741
type: "object",
3842
properties: {
39-
allowInAsyncFunctions: {
43+
allowToRejectPromises: {
4044
type: "boolean",
4145
},
4246
},
@@ -49,7 +53,7 @@ const schema: JSONSchema4[] = [
4953
*/
5054
const defaultOptions: Options = [
5155
{
52-
allowInAsyncFunctions: false,
56+
allowToRejectPromises: false,
5357
},
5458
];
5559

@@ -84,9 +88,29 @@ function checkThrowStatement(
8488
context: Readonly<RuleContext<keyof typeof errorMessages, Options>>,
8589
options: Readonly<Options>,
8690
): RuleResult<keyof typeof errorMessages, Options> {
87-
const [{ allowInAsyncFunctions }] = options;
91+
const [{ allowToRejectPromises }] = options;
92+
93+
if (!allowToRejectPromises) {
94+
return { context, descriptors: [{ node, messageId: "generic" }] };
95+
}
96+
97+
if (isInPromiseHandlerFunction(node, context)) {
98+
return { context, descriptors: [] };
99+
}
100+
101+
const enclosingFunction = getEnclosingFunction(node);
102+
if (enclosingFunction?.async !== true) {
103+
return { context, descriptors: [{ node, messageId: "generic" }] };
104+
}
88105

89-
if (!allowInAsyncFunctions || !isInFunctionBody(node, true)) {
106+
const enclosingTryStatement = getEnclosingTryStatement(node);
107+
if (
108+
!(
109+
enclosingTryStatement === null ||
110+
getEnclosingFunction(enclosingTryStatement) !== enclosingFunction ||
111+
enclosingTryStatement.handler === null
112+
)
113+
) {
90114
return { context, descriptors: [{ node, messageId: "generic" }] };
91115
}
92116

src/utils/tree.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { type RuleContext } from "@typescript-eslint/utils/ts-eslint";
44

55
import typescript from "#/conditional-imports/typescript";
66

7-
import { type BaseOptions } from "./rule";
7+
import { type BaseOptions, getTypeOfNode } from "./rule";
88
import {
99
isBlockStatement,
1010
isCallExpression,
@@ -18,6 +18,7 @@ import {
1818
isMethodDefinition,
1919
isObjectExpression,
2020
isProgram,
21+
isPromiseType,
2122
isProperty,
2223
isTSInterfaceBody,
2324
isTSInterfaceHeritage,
@@ -117,6 +118,30 @@ export function isInReadonly(node: TSESTree.Node): boolean {
117118
return getReadonly(node) !== null;
118119
}
119120

121+
/**
122+
* Test if the given node is in a handler function callback of a promise.
123+
*/
124+
export function isInPromiseHandlerFunction<
125+
Context extends RuleContext<string, BaseOptions>,
126+
>(node: TSESTree.Node, context: Context): boolean {
127+
const functionNode = getAncestorOfType(
128+
(n, c): n is TSESTree.FunctionLike => isFunctionLike(n) && n.body === c,
129+
node,
130+
);
131+
132+
if (
133+
functionNode === null ||
134+
!isCallExpression(functionNode.parent) ||
135+
!isMemberExpression(functionNode.parent.callee) ||
136+
!isIdentifier(functionNode.parent.callee.property)
137+
) {
138+
return false;
139+
}
140+
141+
const objectType = getTypeOfNode(functionNode.parent.callee.object, context);
142+
return isPromiseType(objectType);
143+
}
144+
120145
/**
121146
* Test if the given node is shallowly inside a `Readonly<{...}>`.
122147
*/

0 commit comments

Comments
 (0)