diff --git a/apps/website/content/docs/rules/meta.json b/apps/website/content/docs/rules/meta.json index db32b8301b..d8227d1fe2 100644 --- a/apps/website/content/docs/rules/meta.json +++ b/apps/website/content/docs/rules/meta.json @@ -78,6 +78,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 84dd670ecd..636e61b20d 100644 --- a/apps/website/content/docs/rules/overview.md +++ b/apps/website/content/docs/rules/overview.md @@ -106,6 +106,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/docs/README.md b/packages/core/docs/README.md index 443c0e19b5..971e867ccb 100644 --- a/packages/core/docs/README.md +++ b/packages/core/docs/README.md @@ -101,6 +101,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/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` diff --git a/packages/core/src/hook/is.ts b/packages/core/src/hook/is.ts index b13bae6f96..49f6bdad7f 100644 --- a/packages/core/src/hook/is.ts +++ b/packages/core/src/hook/is.ts @@ -109,6 +109,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. + returnHere 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. + returnHere 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. + returnHere 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. + returnHere 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..66ef2920da --- /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