Skip to content

Commit c2c589c

Browse files
committedAug 5, 2024··
feat(no-throw-statements)!: replace option allowInAsyncFunctions with allowToRejectPromises (#839)
fix #838
1 parent c04e425 commit c2c589c

7 files changed

+186
-24
lines changed
 

‎docs/rules/no-throw-statements.md

+6-10
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,6 @@ throw new Error("Something went wrong.");
2828

2929
### ✅ Correct
3030

31-
<!-- eslint-skip -->
32-
3331
```js
3432
/* eslint functional/no-throw-statements: "error" */
3533

@@ -38,8 +36,6 @@ function divide(x, y) {
3836
}
3937
```
4038

41-
<!-- eslint-skip -->
42-
4339
```js
4440
/* eslint functional/no-throw-statements: "error" */
4541

@@ -58,15 +54,15 @@ This rule accepts an options object of the following type:
5854

5955
```ts
6056
type Options = {
61-
allowInAsyncFunctions: boolean;
57+
allowToRejectPromises: boolean;
6258
};
6359
```
6460

6561
### Default Options
6662

6763
```ts
6864
const defaults = {
69-
allowInAsyncFunctions: false,
65+
allowToRejectPromises: false,
7066
};
7167
```
7268

@@ -76,19 +72,19 @@ const defaults = {
7672

7773
```ts
7874
const recommendedAndLiteOptions = {
79-
allowInAsyncFunctions: true,
75+
allowToRejectPromises: true,
8076
};
8177
```
8278

83-
### `allowInAsyncFunctions`
79+
### `allowToRejectPromises`
8480

85-
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.\
8682
This essentially allows throw statements to be used as return statements for errors.
8783

8884
#### ✅ Correct
8985

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

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

‎docs/rules/prefer-immutable-types.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,7 @@ acceptsCallback((options: CallbackOptions) => {});
425425

426426
```ts
427427
export interface CallbackOptions {
428-
prop: string;
428+
readonly prop: string;
429429
}
430430
type Callback = (options: CallbackOptions) => void;
431431
type AcceptsCallback = (callback: Callback) => void;

‎eslint.config.js

+6-4
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,12 @@ export default rsEslint(
3030
},
3131
},
3232
formatters: true,
33-
functional: "lite",
33+
functional: {
34+
functionalEnforcement: "lite",
35+
overrides: {
36+
"functional/no-throw-statements": "off",
37+
},
38+
},
3439
jsonc: true,
3540
markdown: {
3641
enableTypeRequiredRules: true,
@@ -50,9 +55,6 @@ export default rsEslint(
5055
rules: {
5156
// Some types say they have nonnullable properties, but they don't always.
5257
"ts/no-unnecessary-condition": "off",
53-
54-
// Temp
55-
"functional/no-throw-statements": "off",
5658
},
5759
},
5860
{

‎src/configs/recommended.ts

+1-1
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

+30-6
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ import {
99
type RuleResult,
1010
createRule,
1111
} from "#/utils/rule";
12-
import { isInFunctionBody } from "#/utils/tree";
12+
import {
13+
getEnclosingFunction,
14+
getEnclosingTryStatement,
15+
isInPromiseHandlerFunction,
16+
} from "#/utils/tree";
1317

1418
/**
1519
* The name of this rule.
@@ -26,7 +30,7 @@ export const fullName: `${typeof ruleNameScope}/${typeof name}` = `${ruleNameSco
2630
*/
2731
type Options = [
2832
{
29-
allowInAsyncFunctions: boolean;
33+
allowToRejectPromises: boolean;
3034
},
3135
];
3236

@@ -37,7 +41,7 @@ const schema: JSONSchema4[] = [
3741
{
3842
type: "object",
3943
properties: {
40-
allowInAsyncFunctions: {
44+
allowToRejectPromises: {
4145
type: "boolean",
4246
},
4347
},
@@ -50,7 +54,7 @@ const schema: JSONSchema4[] = [
5054
*/
5155
const defaultOptions: Options = [
5256
{
53-
allowInAsyncFunctions: false,
57+
allowToRejectPromises: false,
5458
},
5559
];
5660

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

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

‎src/utils/tree.ts

+26-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type { RuleContext } from "@typescript-eslint/utils/ts-eslint";
66

77
import typescript from "#/conditional-imports/typescript";
88

9-
import type { BaseOptions } from "./rule";
9+
import { type BaseOptions, getTypeOfNode } from "./rule";
1010
import {
1111
isBlockStatement,
1212
isCallExpression,
@@ -20,6 +20,7 @@ import {
2020
isMethodDefinition,
2121
isObjectExpression,
2222
isProgram,
23+
isPromiseType,
2324
isProperty,
2425
isTSInterfaceBody,
2526
isTSInterfaceHeritage,
@@ -127,6 +128,30 @@ export function isInReadonly(node: TSESTree.Node): boolean {
127128
return getReadonly(node) !== null;
128129
}
129130

131+
/**
132+
* Test if the given node is in a handler function callback of a promise.
133+
*/
134+
export function isInPromiseHandlerFunction<
135+
Context extends RuleContext<string, BaseOptions>,
136+
>(node: TSESTree.Node, context: Context): boolean {
137+
const functionNode = getAncestorOfType(
138+
(n, c): n is TSESTree.FunctionLike => isFunctionLike(n) && n.body === c,
139+
node,
140+
);
141+
142+
if (
143+
functionNode === null ||
144+
!isCallExpression(functionNode.parent) ||
145+
!isMemberExpression(functionNode.parent.callee) ||
146+
!isIdentifier(functionNode.parent.callee.property)
147+
) {
148+
return false;
149+
}
150+
151+
const objectType = getTypeOfNode(functionNode.parent.callee.object, context);
152+
return isPromiseType(objectType);
153+
}
154+
130155
/**
131156
* Test if the given node is shallowly inside a `Readonly<{...}>`.
132157
*/

‎tests/rules/no-throw-statements.test.ts

+116-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { describe, expect, it } from "vitest";
44

55
import { name, rule } from "#/rules/no-throw-statements";
66

7-
import { esLatestConfig } from "../utils/configs";
7+
import { esLatestConfig, typescriptConfig } from "../utils/configs";
88

99
describe(name, () => {
1010
describe("javascript - es latest", () => {
@@ -57,5 +57,120 @@ describe(name, () => {
5757
});
5858
expect(invalidResult.messages).toMatchSnapshot();
5959
});
60+
61+
describe("options", () => {
62+
describe("allowToRejectPromises", () => {
63+
it("doesn't report throw statements in async functions", () => {
64+
valid({
65+
code: dedent`
66+
async function foo() {
67+
throw new Error();
68+
}
69+
`,
70+
options: [{ allowToRejectPromises: true }],
71+
});
72+
});
73+
74+
it("doesn't report throw statements in try without catch in async functions", () => {
75+
valid({
76+
code: dedent`
77+
async function foo() {
78+
try {
79+
throw new Error("hello");
80+
} finally {
81+
console.log("world");
82+
}
83+
}
84+
`,
85+
options: [{ allowToRejectPromises: true }],
86+
});
87+
});
88+
89+
it("reports throw statements in try with catch in async functions", () => {
90+
const invalidResult = invalid({
91+
code: dedent`
92+
async function foo() {
93+
try {
94+
throw new Error("hello world");
95+
} catch (e) {
96+
console.log(e);
97+
}
98+
}
99+
`,
100+
errors: ["generic"],
101+
options: [{ allowToRejectPromises: true }],
102+
});
103+
expect(invalidResult.messages).toMatchSnapshot();
104+
});
105+
106+
it("reports throw statements in functions nested in async functions", () => {
107+
const invalidResult = invalid({
108+
code: dedent`
109+
async function foo() {
110+
function bar() {
111+
throw new Error();
112+
}
113+
}
114+
`,
115+
errors: ["generic"],
116+
options: [{ allowToRejectPromises: true }],
117+
});
118+
expect(invalidResult.messages).toMatchSnapshot();
119+
});
120+
});
121+
});
122+
});
123+
124+
describe("typescript", () => {
125+
const { valid, invalid } = createRuleTester({
126+
name,
127+
rule,
128+
configs: typescriptConfig,
129+
});
130+
131+
describe("options", () => {
132+
describe("allowToRejectPromises", () => {
133+
it("doesn't report throw statements in promise then handlers", () => {
134+
valid({
135+
code: dedent`
136+
function foo() {
137+
Promise.resolve().then(() => {
138+
throw new Error();
139+
});
140+
}
141+
`,
142+
options: [{ allowToRejectPromises: true }],
143+
});
144+
});
145+
146+
it("doesn't report throw statements in promise catch handlers", () => {
147+
valid({
148+
code: dedent`
149+
function foo() {
150+
Promise.resolve().catch(() => {
151+
throw new Error();
152+
});
153+
}
154+
`,
155+
options: [{ allowToRejectPromises: true }],
156+
});
157+
});
158+
159+
it("doesn't report throw statements in promise handlers", () => {
160+
valid({
161+
code: dedent`
162+
function foo() {
163+
Promise.resolve().then(() => {
164+
throw new Error();
165+
}, () => {
166+
throw new Error();
167+
});
168+
}
169+
`,
170+
options: [{ allowToRejectPromises: true }],
171+
});
172+
});
173+
});
174+
});
60175
});
61176
});

0 commit comments

Comments
 (0)
Please sign in to comment.