From 246155ba27c38404be61a80b9d61949835a15e2f Mon Sep 17 00:00:00 2001 From: Rel1cx Date: Sun, 23 Feb 2025 09:39:28 +0800 Subject: [PATCH 1/3] feat(plugins/hooks-extra): add 'no-use-in-try-catch' rule --- apps/website/content/docs/rules/meta.json | 1 + apps/website/content/docs/rules/overview.md | 1 + packages/core/src/hook/is.ts | 1 + .../src/configs/recommended.ts | 1 + .../src/plugin.ts | 2 + .../src/rules/no-use-in-try-catch.md | 58 +++++++++++++++ .../src/rules/no-use-in-try-catch.spec.ts | 74 +++++++++++++++++++ .../src/rules/no-use-in-try-catch.ts | 49 ++++++++++++ .../plugins/eslint-plugin/src/configs/all.ts | 1 + .../eslint-plugin/src/configs/recommended.ts | 1 + 10 files changed, 189 insertions(+) create mode 100644 packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-use-in-try-catch.md create mode 100644 packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-use-in-try-catch.spec.ts create mode 100644 packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-use-in-try-catch.ts diff --git a/apps/website/content/docs/rules/meta.json b/apps/website/content/docs/rules/meta.json index 6d940ce016..da822d9b79 100644 --- a/apps/website/content/docs/rules/meta.json +++ b/apps/website/content/docs/rules/meta.json @@ -77,6 +77,7 @@ "hooks-extra-no-direct-set-state-in-use-layout-effect", "hooks-extra-no-unnecessary-use-callback", "hooks-extra-no-unnecessary-use-memo", + "hooks-extra-no-use-in-try-catch", "hooks-extra-no-useless-custom-hooks", "hooks-extra-prefer-use-state-lazy-initialization", "---Naming Convention Rules---", diff --git a/apps/website/content/docs/rules/overview.md b/apps/website/content/docs/rules/overview.md index afd0c00178..0eb5b9e867 100644 --- a/apps/website/content/docs/rules/overview.md +++ b/apps/website/content/docs/rules/overview.md @@ -105,6 +105,7 @@ full: true | [`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`. | | [`no-unnecessary-use-callback`](./hooks-extra-no-unnecessary-use-callback) | 0️⃣ | `🔍` | Disallow unnecessary usage of `useCallback`. | | [`no-unnecessary-use-memo`](./hooks-extra-no-unnecessary-use-memo) | 0️⃣ | `🔍` | Disallow unnecessary usage of `useMemo`. | +| [`no-use-in-try-catch`](./hooks-extra-no-use-in-try-catch) | 1️⃣ | `🔍` | Disallow `use` in try-catch block. | | [`no-useless-custom-hooks`](./hooks-extra-no-useless-custom-hooks) | 1️⃣ | `🔍` | Enforces custom Hooks to use at least one other Hook inside. | | [`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`. | diff --git a/packages/core/src/hook/is.ts b/packages/core/src/hook/is.ts index 39e3343382..56cc3d7805 100644 --- a/packages/core/src/hook/is.ts +++ b/packages/core/src/hook/is.ts @@ -112,6 +112,7 @@ export function isUseEffectCallLoose(node: TSESTree.Node | _) { } } +export const isUseCall = flip(isReactHookCallWithName)("use"); export const isUseCallbackCall = flip(isReactHookCallWithName)("useCallback"); export const isUseContextCall = flip(isReactHookCallWithName)("useContext"); export const isUseDebugValueCall = flip(isReactHookCallWithName)("useDebugValue"); diff --git a/packages/plugins/eslint-plugin-react-hooks-extra/src/configs/recommended.ts b/packages/plugins/eslint-plugin-react-hooks-extra/src/configs/recommended.ts index 3f09f320d4..33e542deef 100644 --- a/packages/plugins/eslint-plugin-react-hooks-extra/src/configs/recommended.ts +++ b/packages/plugins/eslint-plugin-react-hooks-extra/src/configs/recommended.ts @@ -4,6 +4,7 @@ export const name = "react-hooks-extra/recommended"; export const rules = { "react-hooks-extra/no-direct-set-state-in-use-effect": "warn", + "react-hooks-extra/no-use-in-try-catch": "error", "react-hooks-extra/no-useless-custom-hooks": "warn", "react-hooks-extra/prefer-use-state-lazy-initialization": "warn", } as const satisfies RulePreset; diff --git a/packages/plugins/eslint-plugin-react-hooks-extra/src/plugin.ts b/packages/plugins/eslint-plugin-react-hooks-extra/src/plugin.ts index 1bdb6ba829..43be4972ad 100644 --- a/packages/plugins/eslint-plugin-react-hooks-extra/src/plugin.ts +++ b/packages/plugins/eslint-plugin-react-hooks-extra/src/plugin.ts @@ -3,6 +3,7 @@ import noDirectSetStateInUseEffect from "./rules/no-direct-set-state-in-use-effe import noDirectSetStateInUseLayoutEffect from "./rules/no-direct-set-state-in-use-layout-effect"; import noUnnecessaryUseCallback from "./rules/no-unnecessary-use-callback"; import noUnnecessaryUseMemo from "./rules/no-unnecessary-use-memo"; +import noUseInTryCatch from "./rules/no-use-in-try-catch"; import noUselessCustomHooks from "./rules/no-useless-custom-hooks"; import preferUseStateLazyInitialization from "./rules/prefer-use-state-lazy-initialization"; @@ -16,6 +17,7 @@ export const plugin = { "no-direct-set-state-in-use-layout-effect": noDirectSetStateInUseLayoutEffect, "no-unnecessary-use-callback": noUnnecessaryUseCallback, "no-unnecessary-use-memo": noUnnecessaryUseMemo, + "no-use-in-try-catch": noUseInTryCatch, "no-useless-custom-hooks": noUselessCustomHooks, "prefer-use-state-lazy-initialization": preferUseStateLazyInitialization, diff --git a/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-use-in-try-catch.md b/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-use-in-try-catch.md new file mode 100644 index 0000000000..e763f6ad30 --- /dev/null +++ b/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-use-in-try-catch.md @@ -0,0 +1,58 @@ +--- +title: no-use-in-try-catch +--- + +**Full Name in `eslint-plugin-react-hooks-extra`** + +```plain copy +react-hooks-extra/no-use-in-try-catch +``` + +**Full Name in `@eslint-react/eslint-plugin`** + +```plain copy +@eslint-react/hooks-extra/no-use-in-try-catch +``` + +**Features** + +`🔍` + +**Presets** + +- `recommended` +- `recommended-typescript` +- `recommended-type-checked` + +## What it does + +This rule disallows the use of `use` in a try-catch block. + +`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). + +## Examples + +### Failing + +```tsx +function Message({ messagePromise }) { + try { + const content = use(messagePromise); + // ^^^ + // - '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. + return

Here is the message: {content}

; + } catch (error) { + return

⚠️Something went wrong

; + } +} +``` + +## Implementation + +- [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) +- [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) + +## Further Reading + +- [React: use](https://react.dev/reference/react/use) +- [React: use#dealing-with-rejected-promises](https://react.dev/reference/react/use#dealing-with-rejected-promises) diff --git a/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-use-in-try-catch.spec.ts b/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-use-in-try-catch.spec.ts new file mode 100644 index 0000000000..7d7e5ea409 --- /dev/null +++ b/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-use-in-try-catch.spec.ts @@ -0,0 +1,74 @@ +import { allValid, ruleTester } from "../../../../../test"; +import rule, { RULE_NAME } from "./no-use-in-try-catch"; + +ruleTester.run(RULE_NAME, rule, { + invalid: [ + { + code: /* tsx */ ` + function Message({ messagePromise }) { + try { + const content = use(messagePromise); + // ^^^ + // - '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. + return

Here is the message: {content}

; + } catch (error) { + return

⚠️Something went wrong

; + } + } + `, + errors: [{ messageId: "noUseInTryCatch" }], + }, + { + code: /* tsx */ ` + function Message({ messagePromise }) { + try { + const content = React.use(messagePromise); + // ^^^ + // - '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. + return

Here is the message: {content}

; + } catch (error) { + return

⚠️Something went wrong

; + } + } + `, + errors: [{ messageId: "noUseInTryCatch" }], + }, + { + code: /* tsx */ ` + import { use } from "react"; + + function Message({ messagePromise }) { + try { + const content = use(messagePromise); + // ^^^ + // - '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. + return

Here is the message: {content}

; + } catch (error) { + return

⚠️Something went wrong

; + } + } + `, + errors: [{ messageId: "noUseInTryCatch" }], + }, + { + code: /* tsx */ ` + import * as React from "react"; + + function Message({ messagePromise }) { + try { + const content = React.use(messagePromise); + // ^^^ + // - '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. + return

Here is the message: {content}

; + } catch (error) { + return

⚠️Something went wrong

; + } + } + `, + errors: [{ messageId: "noUseInTryCatch" }], + }, + ], + valid: [ + ...allValid, + ], +}); diff --git a/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-use-in-try-catch.ts b/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-use-in-try-catch.ts new file mode 100644 index 0000000000..4b98a7077c --- /dev/null +++ b/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-use-in-try-catch.ts @@ -0,0 +1,49 @@ +import * as AST from "@eslint-react/ast"; +import { isUseCall } from "@eslint-react/core"; +import type { RuleFeature } from "@eslint-react/shared"; +import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; +import type { CamelCase } from "string-ts"; + +import { createRule } from "../utils"; + +export const RULE_NAME = "no-use-in-try-catch"; + +export const RULE_FEATURES = [ + "CHK", +] as const satisfies RuleFeature[]; + +export type MessageID = CamelCase; + +export default createRule<[], MessageID>({ + meta: { + type: "problem", + docs: { + description: "disallow 'use' in try-catch block", + [Symbol.for("rule_features")]: RULE_FEATURES, + }, + messages: { + noUseInTryCatch: + "'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.", + }, + schema: [], + }, + name: RULE_NAME, + create(context) { + if (!context.sourceCode.text.includes("try")) return {}; + return { + CallExpression(node) { + if (!isUseCall(node, context)) return; + const tryCatchOrFunction = AST.findParentNode(node, (n) => { + return n.type === T.TryStatement || AST.isFunction(n); + }); + if (tryCatchOrFunction?.type === T.TryStatement) { + context.report({ + messageId: "noUseInTryCatch", + node, + }); + } + }, + }; + }, + defaultOptions: [], +}); diff --git a/packages/plugins/eslint-plugin/src/configs/all.ts b/packages/plugins/eslint-plugin/src/configs/all.ts index 40c83997f1..ff27020a01 100644 --- a/packages/plugins/eslint-plugin/src/configs/all.ts +++ b/packages/plugins/eslint-plugin/src/configs/all.ts @@ -83,6 +83,7 @@ export const rules = { "@eslint-react/hooks-extra/no-direct-set-state-in-use-layout-effect": "warn", "@eslint-react/hooks-extra/no-unnecessary-use-callback": "warn", "@eslint-react/hooks-extra/no-unnecessary-use-memo": "warn", + "@eslint-react/hooks-extra/no-use-in-try-catch": "error", "@eslint-react/hooks-extra/no-useless-custom-hooks": "warn", "@eslint-react/hooks-extra/prefer-use-state-lazy-initialization": "warn", diff --git a/packages/plugins/eslint-plugin/src/configs/recommended.ts b/packages/plugins/eslint-plugin/src/configs/recommended.ts index 74dcdaa719..c57bf77f3e 100644 --- a/packages/plugins/eslint-plugin/src/configs/recommended.ts +++ b/packages/plugins/eslint-plugin/src/configs/recommended.ts @@ -14,6 +14,7 @@ export const rules = { ...dom.rules, ...webApi.rules, "@eslint-react/hooks-extra/no-direct-set-state-in-use-effect": "warn", + "@eslint-react/hooks-extra/no-use-in-try-catch": "error", "@eslint-react/hooks-extra/no-useless-custom-hooks": "warn", "@eslint-react/hooks-extra/prefer-use-state-lazy-initialization": "warn", } as const satisfies RulePreset; From 560cc2d710ab8deab1af27e991df28bcc6c4c527 Mon Sep 17 00:00:00 2001 From: Rel1cx Date: Tue, 25 Feb 2025 22:01:26 +0800 Subject: [PATCH 2/3] fix: correct argument order in isUseCall function --- .../src/rules/no-use-in-try-catch.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-use-in-try-catch.ts b/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-use-in-try-catch.ts index 4b98a7077c..66ef2920da 100644 --- a/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-use-in-try-catch.ts +++ b/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-use-in-try-catch.ts @@ -32,7 +32,7 @@ export default createRule<[], MessageID>({ if (!context.sourceCode.text.includes("try")) return {}; return { CallExpression(node) { - if (!isUseCall(node, context)) return; + if (!isUseCall(context, node)) return; const tryCatchOrFunction = AST.findParentNode(node, (n) => { return n.type === T.TryStatement || AST.isFunction(n); }); From 0e2d85182483beb939d4165cc1383abff435cc53 Mon Sep 17 00:00:00 2001 From: Rel1cx Date: Tue, 25 Feb 2025 22:02:03 +0800 Subject: [PATCH 3/3] docs: add isUseCall function documentation and update related references --- packages/core/docs/README.md | 1 + .../docs/functions/isInitializedFromReact.md | 4 ++-- packages/core/docs/functions/isUseCall.md | 19 +++++++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 packages/core/docs/functions/isUseCall.md diff --git a/packages/core/docs/README.md b/packages/core/docs/README.md index 2d7bc0d0f0..74f22f137d 100644 --- a/packages/core/docs/README.md +++ b/packages/core/docs/README.md @@ -116,6 +116,7 @@ - [isRenderMethodLike](functions/isRenderMethodLike.md) - [isRenderPropLoose](functions/isRenderPropLoose.md) - [isThisSetState](functions/isThisSetState.md) +- [isUseCall](functions/isUseCall.md) - [isUseCallbackCall](functions/isUseCallbackCall.md) - [isUseContextCall](functions/isUseContextCall.md) - [isUseDebugValueCall](functions/isUseDebugValueCall.md) diff --git a/packages/core/docs/functions/isInitializedFromReact.md b/packages/core/docs/functions/isInitializedFromReact.md index c8f2bc847e..b2d85cbcc9 100644 --- a/packages/core/docs/functions/isInitializedFromReact.md +++ b/packages/core/docs/functions/isInitializedFromReact.md @@ -6,7 +6,7 @@ # Function: isInitializedFromReact() -> **isInitializedFromReact**(`name`, `source`, `initialScope`): `boolean` +> **isInitializedFromReact**(`name`, `importSource`, `initialScope`): `boolean` Check if an identifier is initialized from react @@ -18,7 +18,7 @@ Check if an identifier is initialized from react The top-level identifier's name -### source +### importSource `string` diff --git a/packages/core/docs/functions/isUseCall.md b/packages/core/docs/functions/isUseCall.md new file mode 100644 index 0000000000..c860d22987 --- /dev/null +++ b/packages/core/docs/functions/isUseCall.md @@ -0,0 +1,19 @@ +[**@eslint-react/core**](../README.md) + +*** + +[@eslint-react/core](../README.md) / isUseCall + +# Function: isUseCall() + +> **isUseCall**(...`a`): `boolean` + +## Parameters + +### a + +...\[[`Readonly`](../-internal-/type-aliases/Readonly.md)\<[`RuleContext`](../-internal-/interfaces/RuleContext.md)\<`string`, readonly `unknown`[]\>\>, [`CallExpression`](../-internal-/interfaces/CallExpression.md)\] + +## Returns + +`boolean`