From 5b35f50871e2431422a19827cc7c157c3708dd00 Mon Sep 17 00:00:00 2001 From: Rel1cx Date: Sun, 23 Feb 2025 08:38:31 +0800 Subject: [PATCH] feat(plugins/dom): add 'no-flush-sync' rule --- apps/website/content/docs/rules/meta.json | 1 + apps/website/content/docs/rules/overview.md | 1 + .../eslint-plugin-react-dom/src/plugin.ts | 2 + .../src/rules/no-find-dom-node.md | 2 +- .../src/rules/no-flush-sync.md | 48 ++++++++++++++++++ .../src/rules/no-flush-sync.spec.ts | 34 +++++++++++++ .../src/rules/no-flush-sync.ts | 49 +++++++++++++++++++ 7 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 packages/plugins/eslint-plugin-react-dom/src/rules/no-flush-sync.md create mode 100644 packages/plugins/eslint-plugin-react-dom/src/rules/no-flush-sync.spec.ts create mode 100644 packages/plugins/eslint-plugin-react-dom/src/rules/no-flush-sync.ts diff --git a/apps/website/content/docs/rules/meta.json b/apps/website/content/docs/rules/meta.json index 6d940ce016..85c5003f06 100644 --- a/apps/website/content/docs/rules/meta.json +++ b/apps/website/content/docs/rules/meta.json @@ -58,6 +58,7 @@ "dom-no-dangerously-set-innerhtml", "dom-no-dangerously-set-innerhtml-with-children", "dom-no-find-dom-node", + "dom-no-flush-sync", "dom-no-missing-button-type", "dom-no-missing-iframe-sandbox", "dom-no-namespace", diff --git a/apps/website/content/docs/rules/overview.md b/apps/website/content/docs/rules/overview.md index afd0c00178..6dd52b829d 100644 --- a/apps/website/content/docs/rules/overview.md +++ b/apps/website/content/docs/rules/overview.md @@ -79,6 +79,7 @@ full: true | [`no-dangerously-set-innerhtml-with-children`](./dom-no-dangerously-set-innerhtml-with-children) | 2️⃣ | `🔍` | Prevents DOM elements using `dangerouslySetInnerHTML` and `children` at the same time. | | [`no-dangerously-set-innerhtml`](./dom-no-dangerously-set-innerhtml) | 1️⃣ | `🔍` | Prevents DOM elements using `dangerouslySetInnerHTML`. | | [`no-find-dom-node`](./dom-no-find-dom-node) | 2️⃣ | `🔍` | Prevents using `findDOMNode`. | +| [`no-flush-sync`](./dom-no-flush-sync) | 1️⃣ | `🔍` | Prevents using `flushSync`. | | [`no-missing-button-type`](./dom-no-missing-button-type) | 1️⃣ | `🔍` | Enforces explicit `type` attribute for `button` elements. | | [`no-missing-iframe-sandbox`](./dom-no-missing-iframe-sandbox) | 1️⃣ | `🔍` | Enforces explicit `sandbox` attribute for `iframe` elements. | | [`no-namespace`](./dom-no-namespace) | 2️⃣ | `🔍` | Enforces the absence of a `namespace` in React elements. | diff --git a/packages/plugins/eslint-plugin-react-dom/src/plugin.ts b/packages/plugins/eslint-plugin-react-dom/src/plugin.ts index 41fbbca857..696807fbb0 100644 --- a/packages/plugins/eslint-plugin-react-dom/src/plugin.ts +++ b/packages/plugins/eslint-plugin-react-dom/src/plugin.ts @@ -2,6 +2,7 @@ import { name, version } from "../package.json"; import noDangerouslySetInnerHTML from "./rules/no-dangerously-set-innerhtml"; import noDangerouslySetInnerHTMLWithChildren from "./rules/no-dangerously-set-innerhtml-with-children"; import noFindDomNode from "./rules/no-find-dom-node"; +import noFlushSync from "./rules/no-flush-sync"; import noMissingButtonType from "./rules/no-missing-button-type"; import noMissingIframeSandbox from "./rules/no-missing-iframe-sandbox"; import noNamespace from "./rules/no-namespace"; @@ -21,6 +22,7 @@ export const plugin = { "no-dangerously-set-innerhtml": noDangerouslySetInnerHTML, "no-dangerously-set-innerhtml-with-children": noDangerouslySetInnerHTMLWithChildren, "no-find-dom-node": noFindDomNode, + "no-flush-sync": noFlushSync, "no-missing-button-type": noMissingButtonType, "no-missing-iframe-sandbox": noMissingIframeSandbox, "no-namespace": noNamespace, diff --git a/packages/plugins/eslint-plugin-react-dom/src/rules/no-find-dom-node.md b/packages/plugins/eslint-plugin-react-dom/src/rules/no-find-dom-node.md index 8e66d688f2..78a7db1b0e 100644 --- a/packages/plugins/eslint-plugin-react-dom/src/rules/no-find-dom-node.md +++ b/packages/plugins/eslint-plugin-react-dom/src/rules/no-find-dom-node.md @@ -80,4 +80,4 @@ class AutoSelectingInput extends Component { ## Further Reading -- [React: APIs findDOMNode](https://react.dev/reference/react-dom/findDOMNode) +- [React DOM: APIs findDOMNode](https://react.dev/reference/react-dom/findDOMNode) diff --git a/packages/plugins/eslint-plugin-react-dom/src/rules/no-flush-sync.md b/packages/plugins/eslint-plugin-react-dom/src/rules/no-flush-sync.md new file mode 100644 index 0000000000..3044da23a6 --- /dev/null +++ b/packages/plugins/eslint-plugin-react-dom/src/rules/no-flush-sync.md @@ -0,0 +1,48 @@ +--- +title: no-flush-sync +--- + +**Full Name in `eslint-plugin-react-dom`** + +```plain copy +react-dom/no-flush-sync +``` + +**Full Name in `@eslint-react/eslint-plugin`** + +```plain copy +@eslint-react/dom/no-flush-sync +``` + +**Features** + +`🔍` + +## What it does + +This rule reports usages of `flushSync`. + +`flushSync` can significantly hurt performance, and may unexpectedly force pending Suspense boundaries to show their fallback state. + +Most of the time, `flushSync` can be avoided, so use `flushSync` as a last resort. + +## Examples + +### Failing + +```tsx +import { flushSync } from "react-dom"; + +flushSync(() => { + setSomething(123); +}); +``` + +## Implementation + +- [Rule source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom/src/rules/no-flush-sync.ts) +- [Test source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom/src/rules/no-flush-sync.spec.ts) + +## Further Reading + +- [React DOM: APIs flushSync](https://react.dev/reference/react-dom/flushSync) diff --git a/packages/plugins/eslint-plugin-react-dom/src/rules/no-flush-sync.spec.ts b/packages/plugins/eslint-plugin-react-dom/src/rules/no-flush-sync.spec.ts new file mode 100644 index 0000000000..c22271d146 --- /dev/null +++ b/packages/plugins/eslint-plugin-react-dom/src/rules/no-flush-sync.spec.ts @@ -0,0 +1,34 @@ +import { allValid, ruleTester } from "../../../../../test"; +import rule, { RULE_NAME } from "./no-flush-sync"; + +ruleTester.run(RULE_NAME, rule, { + invalid: [ + { + code: /* tsx */ ` + import { flushSync } from 'react-dom'; + + flushSync(() => { + setSomething(123); + }); + `, + errors: [ + { messageId: "noFlushSync" }, + ], + }, + { + code: /* tsx */ ` + import reactDom from 'react-dom'; + + reactDom.flushSync(() => { + setSomething(123); + }); + `, + errors: [ + { messageId: "noFlushSync" }, + ], + }, + ], + valid: [ + ...allValid, + ], +}); diff --git a/packages/plugins/eslint-plugin-react-dom/src/rules/no-flush-sync.ts b/packages/plugins/eslint-plugin-react-dom/src/rules/no-flush-sync.ts new file mode 100644 index 0000000000..c5f07bf3ea --- /dev/null +++ b/packages/plugins/eslint-plugin-react-dom/src/rules/no-flush-sync.ts @@ -0,0 +1,49 @@ +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-flush-sync"; + +export const RULE_FEATURES = [ + "CHK", +] as const satisfies RuleFeature[]; + +export type MessageID = CamelCase; + +export default createRule<[], MessageID>({ + meta: { + type: "problem", + docs: { + description: "warns against using `flushSync`", + [Symbol.for("rule_features")]: RULE_FEATURES, + }, + messages: { + noFlushSync: "Using 'flushSync' is uncommon and can hurt the performance of your app.", + }, + schema: [], + }, + name: RULE_NAME, + create(context) { + if (!context.sourceCode.text.includes("flushSync")) return {}; + return { + CallExpression(node) { + const { callee } = node; + switch (callee.type) { + case T.Identifier: + if (callee.name === "flushSync") { + context.report({ messageId: "noFlushSync", node }); + } + return; + case T.MemberExpression: + if (callee.property.type === T.Identifier && callee.property.name === "flushSync") { + context.report({ messageId: "noFlushSync", node }); + } + return; + } + }, + }; + }, + defaultOptions: [], +});