Skip to content

Commit

Permalink
feat(no-throw-statements)!: replace option allowInAsyncFunctions wi…
Browse files Browse the repository at this point in the history
…th `allowToRejectPromises`
  • Loading branch information
RebeccaStevens committed Jul 11, 2024
1 parent 38486c0 commit a9d36e9
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 14 deletions.
12 changes: 6 additions & 6 deletions docs/rules/no-throw-statements.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,15 @@ This rule accepts an options object of the following type:

```ts
type Options = {
allowInAsyncFunctions: boolean;
allowToRejectPromises: boolean;
};
```

### Default Options

```ts
const defaults = {
allowInAsyncFunctions: false,
allowToRejectPromises: false,
};
```

Expand All @@ -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]);
Expand Down
2 changes: 1 addition & 1 deletion src/configs/recommended.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ const overrides = {
[noThrowStatements.fullName]: [
"error",
{
allowInAsyncFunctions: true,
allowToRejectPromises: true,
},
],
[noTryStatements.fullName]: "off",
Expand Down
36 changes: 30 additions & 6 deletions src/rules/no-throw-statements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -25,7 +29,7 @@ export const fullName = `${ruleNameScope}/${name}`;
*/
type Options = [
{
allowInAsyncFunctions: boolean;
allowToRejectPromises: boolean;
},
];

Expand All @@ -36,7 +40,7 @@ const schema: JSONSchema4[] = [
{
type: "object",
properties: {
allowInAsyncFunctions: {
allowToRejectPromises: {
type: "boolean",
},
},
Expand All @@ -49,7 +53,7 @@ const schema: JSONSchema4[] = [
*/
const defaultOptions: Options = [
{
allowInAsyncFunctions: false,
allowToRejectPromises: false,
},
];

Expand Down Expand Up @@ -84,9 +88,29 @@ function checkThrowStatement(
context: Readonly<RuleContext<keyof typeof errorMessages, Options>>,
options: Readonly<Options>,
): RuleResult<keyof typeof errorMessages, Options> {
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" }] };
}

Expand Down
27 changes: 26 additions & 1 deletion src/utils/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -18,6 +18,7 @@ import {
isMethodDefinition,
isObjectExpression,
isProgram,
isPromiseType,
isProperty,
isTSInterfaceBody,
isTSInterfaceHeritage,
Expand Down Expand Up @@ -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<string, BaseOptions>,
>(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<{...}>`.
*/
Expand Down
53 changes: 53 additions & 0 deletions tests/rules/no-throw-statements.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,4 +173,57 @@ describe(name, () => {
});
});
});

describe("typescript", () => {

Check failure on line 177 in tests/rules/no-throw-statements.test.ts

View workflow job for this annotation

GitHub Actions / lint_js

Describe is used multiple times in the same describe(suite) block
const { valid, invalid } = createRuleTester({
name,
rule,
configs: typescriptConfig,
});

describe("options", () => {
describe("allowToRejectPromises", () => {
it("doesn't report throw statements in promise then handlers", () => {
valid({
code: dedent`
function foo() {
Promise.resolve().then(() => {
throw new Error();
});
}
`,
options: [{ allowToRejectPromises: true }],
});
});

it("doesn't report throw statements in promise catch handlers", () => {
valid({
code: dedent`
function foo() {
Promise.resolve().catch(() => {
throw new Error();
});
}
`,
options: [{ allowToRejectPromises: true }],
});
});

it("doesn't report throw statements in promise handlers", () => {
valid({
code: dedent`
function foo() {
Promise.resolve().then(() => {
throw new Error();
}, () => {
throw new Error();
});
}
`,
options: [{ allowToRejectPromises: true }],
});
});
});
});
});
});

0 comments on commit a9d36e9

Please sign in to comment.