Skip to content

Commit 246155b

Browse files
committed
feat(plugins/hooks-extra): add 'no-use-in-try-catch' rule
1 parent 0ef739b commit 246155b

File tree

10 files changed

+189
-0
lines changed

10 files changed

+189
-0
lines changed

apps/website/content/docs/rules/meta.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
"hooks-extra-no-direct-set-state-in-use-layout-effect",
7878
"hooks-extra-no-unnecessary-use-callback",
7979
"hooks-extra-no-unnecessary-use-memo",
80+
"hooks-extra-no-use-in-try-catch",
8081
"hooks-extra-no-useless-custom-hooks",
8182
"hooks-extra-prefer-use-state-lazy-initialization",
8283
"---Naming Convention Rules---",

apps/website/content/docs/rules/overview.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ full: true
105105
| [`no-direct-set-state-in-use-layout-effect`](./hooks-extra-no-direct-set-state-in-use-layout-effect) | 0️⃣ | `🔍` | Disallow direct calls to the `set` function of `useState` in `useLayoutEffect`. |
106106
| [`no-unnecessary-use-callback`](./hooks-extra-no-unnecessary-use-callback) | 0️⃣ | `🔍` | Disallow unnecessary usage of `useCallback`. |
107107
| [`no-unnecessary-use-memo`](./hooks-extra-no-unnecessary-use-memo) | 0️⃣ | `🔍` | Disallow unnecessary usage of `useMemo`. |
108+
| [`no-use-in-try-catch`](./hooks-extra-no-use-in-try-catch) | 1️⃣ | `🔍` | Disallow `use` in try-catch block. |
108109
| [`no-useless-custom-hooks`](./hooks-extra-no-useless-custom-hooks) | 1️⃣ | `🔍` | Enforces custom Hooks to use at least one other Hook inside. |
109110
| [`prefer-use-state-lazy-initialization`](./hooks-extra-prefer-use-state-lazy-initialization) | 1️⃣ | `🔍` | Enforces function calls made inside `useState` to be wrapped in an `initializer function`. |
110111

packages/core/src/hook/is.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ export function isUseEffectCallLoose(node: TSESTree.Node | _) {
112112
}
113113
}
114114

115+
export const isUseCall = flip(isReactHookCallWithName)("use");
115116
export const isUseCallbackCall = flip(isReactHookCallWithName)("useCallback");
116117
export const isUseContextCall = flip(isReactHookCallWithName)("useContext");
117118
export const isUseDebugValueCall = flip(isReactHookCallWithName)("useDebugValue");

packages/plugins/eslint-plugin-react-hooks-extra/src/configs/recommended.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export const name = "react-hooks-extra/recommended";
44

55
export const rules = {
66
"react-hooks-extra/no-direct-set-state-in-use-effect": "warn",
7+
"react-hooks-extra/no-use-in-try-catch": "error",
78
"react-hooks-extra/no-useless-custom-hooks": "warn",
89
"react-hooks-extra/prefer-use-state-lazy-initialization": "warn",
910
} as const satisfies RulePreset;

packages/plugins/eslint-plugin-react-hooks-extra/src/plugin.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import noDirectSetStateInUseEffect from "./rules/no-direct-set-state-in-use-effe
33
import noDirectSetStateInUseLayoutEffect from "./rules/no-direct-set-state-in-use-layout-effect";
44
import noUnnecessaryUseCallback from "./rules/no-unnecessary-use-callback";
55
import noUnnecessaryUseMemo from "./rules/no-unnecessary-use-memo";
6+
import noUseInTryCatch from "./rules/no-use-in-try-catch";
67
import noUselessCustomHooks from "./rules/no-useless-custom-hooks";
78
import preferUseStateLazyInitialization from "./rules/prefer-use-state-lazy-initialization";
89

@@ -16,6 +17,7 @@ export const plugin = {
1617
"no-direct-set-state-in-use-layout-effect": noDirectSetStateInUseLayoutEffect,
1718
"no-unnecessary-use-callback": noUnnecessaryUseCallback,
1819
"no-unnecessary-use-memo": noUnnecessaryUseMemo,
20+
"no-use-in-try-catch": noUseInTryCatch,
1921
"no-useless-custom-hooks": noUselessCustomHooks,
2022
"prefer-use-state-lazy-initialization": preferUseStateLazyInitialization,
2123

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
---
2+
title: no-use-in-try-catch
3+
---
4+
5+
**Full Name in `eslint-plugin-react-hooks-extra`**
6+
7+
```plain copy
8+
react-hooks-extra/no-use-in-try-catch
9+
```
10+
11+
**Full Name in `@eslint-react/eslint-plugin`**
12+
13+
```plain copy
14+
@eslint-react/hooks-extra/no-use-in-try-catch
15+
```
16+
17+
**Features**
18+
19+
`🔍`
20+
21+
**Presets**
22+
23+
- `recommended`
24+
- `recommended-typescript`
25+
- `recommended-type-checked`
26+
27+
## What it does
28+
29+
This rule disallows the use of `use` in a try-catch block.
30+
31+
`use` cannot be called in a try-catch block. Instead of a try-catch block [wrap your component in an Error Boundary](https://react.dev/reference/react/use#displaying-an-error-to-users-with-error-boundary), or [provide an alternative value to use with the Promise’s `.catch` method](https://react.dev/reference/react/use#providing-an-alternative-value-with-promise-catch).
32+
33+
## Examples
34+
35+
### Failing
36+
37+
```tsx
38+
function Message({ messagePromise }) {
39+
try {
40+
const content = use(messagePromise);
41+
// ^^^
42+
// - 'use' cannot be called in a try-catch block. Instead of a try-catch block wrap your component in an Error Boundary, or provide an alternative value to use with the Promise’s .catch method.
43+
return <p>Here is the message: {content}</p>;
44+
} catch (error) {
45+
return <p>⚠️Something went wrong</p>;
46+
}
47+
}
48+
```
49+
50+
## Implementation
51+
52+
- [Rule source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-use-in-try-catch.ts)
53+
- [Test source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-use-in-try-catch.spec.ts)
54+
55+
## Further Reading
56+
57+
- [React: use](https://react.dev/reference/react/use)
58+
- [React: use#dealing-with-rejected-promises](https://react.dev/reference/react/use#dealing-with-rejected-promises)
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { allValid, ruleTester } from "../../../../../test";
2+
import rule, { RULE_NAME } from "./no-use-in-try-catch";
3+
4+
ruleTester.run(RULE_NAME, rule, {
5+
invalid: [
6+
{
7+
code: /* tsx */ `
8+
function Message({ messagePromise }) {
9+
try {
10+
const content = use(messagePromise);
11+
// ^^^
12+
// - 'use' cannot be called in a try-catch block. Instead of a try-catch block wrap your component in an Error Boundary, or provide an alternative value to use with the Promise’s .catch method.
13+
return <p>Here is the message: {content}</p>;
14+
} catch (error) {
15+
return <p>⚠️Something went wrong</p>;
16+
}
17+
}
18+
`,
19+
errors: [{ messageId: "noUseInTryCatch" }],
20+
},
21+
{
22+
code: /* tsx */ `
23+
function Message({ messagePromise }) {
24+
try {
25+
const content = React.use(messagePromise);
26+
// ^^^
27+
// - 'use' cannot be called in a try-catch block. Instead of a try-catch block wrap your component in an Error Boundary, or provide an alternative value to use with the Promise’s .catch method.
28+
return <p>Here is the message: {content}</p>;
29+
} catch (error) {
30+
return <p>⚠️Something went wrong</p>;
31+
}
32+
}
33+
`,
34+
errors: [{ messageId: "noUseInTryCatch" }],
35+
},
36+
{
37+
code: /* tsx */ `
38+
import { use } from "react";
39+
40+
function Message({ messagePromise }) {
41+
try {
42+
const content = use(messagePromise);
43+
// ^^^
44+
// - 'use' cannot be called in a try-catch block. Instead of a try-catch block wrap your component in an Error Boundary, or provide an alternative value to use with the Promise’s .catch method.
45+
return <p>Here is the message: {content}</p>;
46+
} catch (error) {
47+
return <p>⚠️Something went wrong</p>;
48+
}
49+
}
50+
`,
51+
errors: [{ messageId: "noUseInTryCatch" }],
52+
},
53+
{
54+
code: /* tsx */ `
55+
import * as React from "react";
56+
57+
function Message({ messagePromise }) {
58+
try {
59+
const content = React.use(messagePromise);
60+
// ^^^
61+
// - 'use' cannot be called in a try-catch block. Instead of a try-catch block wrap your component in an Error Boundary, or provide an alternative value to use with the Promise’s .catch method.
62+
return <p>Here is the message: {content}</p>;
63+
} catch (error) {
64+
return <p>⚠️Something went wrong</p>;
65+
}
66+
}
67+
`,
68+
errors: [{ messageId: "noUseInTryCatch" }],
69+
},
70+
],
71+
valid: [
72+
...allValid,
73+
],
74+
});
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import * as AST from "@eslint-react/ast";
2+
import { isUseCall } from "@eslint-react/core";
3+
import type { RuleFeature } from "@eslint-react/shared";
4+
import { AST_NODE_TYPES as T } from "@typescript-eslint/types";
5+
import type { CamelCase } from "string-ts";
6+
7+
import { createRule } from "../utils";
8+
9+
export const RULE_NAME = "no-use-in-try-catch";
10+
11+
export const RULE_FEATURES = [
12+
"CHK",
13+
] as const satisfies RuleFeature[];
14+
15+
export type MessageID = CamelCase<typeof RULE_NAME>;
16+
17+
export default createRule<[], MessageID>({
18+
meta: {
19+
type: "problem",
20+
docs: {
21+
description: "disallow 'use' in try-catch block",
22+
[Symbol.for("rule_features")]: RULE_FEATURES,
23+
},
24+
messages: {
25+
noUseInTryCatch:
26+
"'use' cannot be called in a try-catch block. Instead of a try-catch block wrap your component in an Error Boundary, or provide an alternative value to use with the Promise’s '.catch' method.",
27+
},
28+
schema: [],
29+
},
30+
name: RULE_NAME,
31+
create(context) {
32+
if (!context.sourceCode.text.includes("try")) return {};
33+
return {
34+
CallExpression(node) {
35+
if (!isUseCall(node, context)) return;
36+
const tryCatchOrFunction = AST.findParentNode(node, (n) => {
37+
return n.type === T.TryStatement || AST.isFunction(n);
38+
});
39+
if (tryCatchOrFunction?.type === T.TryStatement) {
40+
context.report({
41+
messageId: "noUseInTryCatch",
42+
node,
43+
});
44+
}
45+
},
46+
};
47+
},
48+
defaultOptions: [],
49+
});

packages/plugins/eslint-plugin/src/configs/all.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ export const rules = {
8383
"@eslint-react/hooks-extra/no-direct-set-state-in-use-layout-effect": "warn",
8484
"@eslint-react/hooks-extra/no-unnecessary-use-callback": "warn",
8585
"@eslint-react/hooks-extra/no-unnecessary-use-memo": "warn",
86+
"@eslint-react/hooks-extra/no-use-in-try-catch": "error",
8687
"@eslint-react/hooks-extra/no-useless-custom-hooks": "warn",
8788
"@eslint-react/hooks-extra/prefer-use-state-lazy-initialization": "warn",
8889

packages/plugins/eslint-plugin/src/configs/recommended.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export const rules = {
1414
...dom.rules,
1515
...webApi.rules,
1616
"@eslint-react/hooks-extra/no-direct-set-state-in-use-effect": "warn",
17+
"@eslint-react/hooks-extra/no-use-in-try-catch": "error",
1718
"@eslint-react/hooks-extra/no-useless-custom-hooks": "warn",
1819
"@eslint-react/hooks-extra/prefer-use-state-lazy-initialization": "warn",
1920
} as const satisfies RulePreset;

0 commit comments

Comments
 (0)