Skip to content

Commit 444d6af

Browse files
feat(no-throw-statements)!: replace option allowInAsyncFunctions with allowToRejectPromises
1 parent 7a86c94 commit 444d6af

File tree

10 files changed

+227
-16
lines changed

10 files changed

+227
-16
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: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
type RuleResult,
99
createRule,
1010
} from "#/utils/rule";
11-
import { isInFunctionBody } from "#/utils/tree";
11+
import { isInFunctionBody, isInPromiseCatchFunction } from "#/utils/tree";
1212

1313
/**
1414
* The name of this rule.
@@ -25,7 +25,7 @@ export const fullName = `${ruleNameScope}/${name}`;
2525
*/
2626
type Options = [
2727
{
28-
allowInAsyncFunctions: boolean;
28+
allowToRejectPromises: boolean;
2929
},
3030
];
3131

@@ -36,7 +36,7 @@ const schema: JSONSchema4[] = [
3636
{
3737
type: "object",
3838
properties: {
39-
allowInAsyncFunctions: {
39+
allowToRejectPromises: {
4040
type: "boolean",
4141
},
4242
},
@@ -49,7 +49,7 @@ const schema: JSONSchema4[] = [
4949
*/
5050
const defaultOptions: Options = [
5151
{
52-
allowInAsyncFunctions: false,
52+
allowToRejectPromises: false,
5353
},
5454
];
5555

@@ -84,9 +84,12 @@ function checkThrowStatement(
8484
context: Readonly<RuleContext<keyof typeof errorMessages, Options>>,
8585
options: Readonly<Options>,
8686
): RuleResult<keyof typeof errorMessages, Options> {
87-
const [{ allowInAsyncFunctions }] = options;
87+
const [{ allowToRejectPromises }] = options;
8888

89-
if (!allowInAsyncFunctions || !isInFunctionBody(node, true)) {
89+
if (
90+
!allowToRejectPromises ||
91+
!(isInFunctionBody(node, true) || isInPromiseCatchFunction(node, context))
92+
) {
9093
return { context, descriptors: [{ node, messageId: "generic" }] };
9194
}
9295

src/utils/tree.ts

Lines changed: 47 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,
@@ -104,6 +105,51 @@ export function isInReadonly(node: TSESTree.Node): boolean {
104105
return getReadonly(node) !== null;
105106
}
106107

108+
/**
109+
* Test if the given node is in a catch function callback of a promise.
110+
*/
111+
export function isInPromiseCatchFunction<
112+
Context extends RuleContext<string, BaseOptions>,
113+
>(node: TSESTree.Node, context: Context): boolean {
114+
const functionNode = getAncestorOfType(
115+
(n, c): n is TSESTree.FunctionLike => isFunctionLike(n) && n.body === c,
116+
node,
117+
);
118+
119+
if (
120+
functionNode === null ||
121+
!isCallExpression(functionNode.parent) ||
122+
!isMemberExpression(functionNode.parent.callee) ||
123+
!isIdentifier(functionNode.parent.callee.property)
124+
) {
125+
return false;
126+
}
127+
128+
const { object, property } = functionNode.parent.callee;
129+
switch (property.name) {
130+
case "then": {
131+
if (functionNode.parent.arguments[1] !== functionNode) {
132+
return false;
133+
}
134+
break;
135+
}
136+
137+
case "catch": {
138+
if (functionNode.parent.arguments[0] !== functionNode) {
139+
return false;
140+
}
141+
break;
142+
}
143+
144+
default: {
145+
return false;
146+
}
147+
}
148+
149+
const objectType = getTypeOfNode(object, context);
150+
return isPromiseType(objectType);
151+
}
152+
107153
/**
108154
* Test if the given node is shallowly inside a `Readonly<{...}>`.
109155
*/

src/utils/type-guards.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,3 +434,12 @@ export function isObjectConstructorType(type: Type | null): boolean {
434434
export function isFunctionLikeType(type: Type | null): boolean {
435435
return type !== null && type.getCallSignatures().length > 0;
436436
}
437+
438+
export function isPromiseType(type: Type | null): boolean {
439+
return (
440+
type !== null &&
441+
(((type.symbol as unknown) !== undefined &&
442+
type.symbol.name === "Promise") ||
443+
(isUnionType(type) && type.types.some(isPromiseType)))
444+
);
445+
}

tests/rules/no-throw-statement/es2016/invalid.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const tests: Array<
2020
optionsSet: [
2121
[
2222
{
23-
allowInAsyncFunctions: false,
23+
allowToRejectPromises: false,
2424
},
2525
],
2626
],

tests/rules/no-throw-statement/es2016/valid.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const tests: Array<ValidTestCaseSet<OptionsOf<typeof rule>>> = [
1313
optionsSet: [
1414
[
1515
{
16-
allowInAsyncFunctions: true,
16+
allowToRejectPromises: true,
1717
},
1818
],
1919
],
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { name, rule } from "#/rules/no-throw-statements";
2+
import { testRule } from "#/tests/helpers/testers";
3+
4+
import es2016Invalid from "../es2016/invalid";
5+
import es2016Valid from "../es2016/valid";
6+
import es3Invalid from "../es3/invalid";
7+
import es3Valid from "../es3/valid";
8+
9+
import invalid from "./invalid";
10+
import valid from "./valid";
11+
12+
const tests = {
13+
valid: [...es3Valid, ...es2016Valid, ...valid],
14+
invalid: [...es3Invalid, ...es2016Invalid, ...invalid],
15+
};
16+
17+
const tester = testRule(name, rule);
18+
19+
tester.typescript(tests);
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { AST_NODE_TYPES } from "@typescript-eslint/utils";
2+
import dedent from "dedent";
3+
4+
import { type rule } from "#/rules/no-throw-statements";
5+
import {
6+
type InvalidTestCaseSet,
7+
type MessagesOf,
8+
type OptionsOf,
9+
} from "#/tests/helpers/util";
10+
11+
const tests: Array<
12+
InvalidTestCaseSet<MessagesOf<typeof rule>, OptionsOf<typeof rule>>
13+
> = [
14+
{
15+
code: dedent`
16+
const foo = Promise.reject();
17+
foo.then(() => {
18+
throw new Error();
19+
});
20+
`,
21+
optionsSet: [
22+
[
23+
{
24+
allowToRejectPromises: true,
25+
},
26+
],
27+
],
28+
errors: [
29+
{
30+
messageId: "generic",
31+
type: AST_NODE_TYPES.ThrowStatement,
32+
line: 3,
33+
column: 3,
34+
},
35+
],
36+
},
37+
{
38+
code: dedent`
39+
const foo = Promise.reject();
40+
foo.then(
41+
() => {
42+
throw new Error();
43+
},
44+
() => {}
45+
);
46+
`,
47+
optionsSet: [
48+
[
49+
{
50+
allowToRejectPromises: true,
51+
},
52+
],
53+
],
54+
errors: [
55+
{
56+
messageId: "generic",
57+
type: AST_NODE_TYPES.ThrowStatement,
58+
line: 4,
59+
column: 7,
60+
},
61+
],
62+
},
63+
{
64+
code: dedent`
65+
const foo = {
66+
catch(cb) {
67+
cb();
68+
},
69+
};
70+
foo.catch(() => {
71+
throw new Error();
72+
});
73+
`,
74+
optionsSet: [
75+
[
76+
{
77+
allowToRejectPromises: true,
78+
},
79+
],
80+
],
81+
errors: [
82+
{
83+
messageId: "generic",
84+
type: AST_NODE_TYPES.ThrowStatement,
85+
line: 7,
86+
column: 3,
87+
},
88+
],
89+
},
90+
];
91+
92+
export default tests;
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import dedent from "dedent";
2+
3+
import { type rule } from "#/rules/no-throw-statements";
4+
import { type OptionsOf, type ValidTestCaseSet } from "#/tests/helpers/util";
5+
6+
const tests: Array<ValidTestCaseSet<OptionsOf<typeof rule>>> = [
7+
{
8+
code: dedent`
9+
const foo = Promise.reject();
10+
foo.catch(() => {
11+
throw new Error();
12+
});
13+
`,
14+
optionsSet: [
15+
[
16+
{
17+
allowToRejectPromises: true,
18+
},
19+
],
20+
],
21+
},
22+
{
23+
code: dedent`
24+
const foo = Promise.reject();
25+
foo.then(
26+
() => {},
27+
() => {
28+
throw new Error();
29+
}
30+
);
31+
`,
32+
optionsSet: [
33+
[
34+
{
35+
allowToRejectPromises: true,
36+
},
37+
],
38+
],
39+
},
40+
];
41+
42+
export default tests;

0 commit comments

Comments
 (0)