From 69ac59493a3015c66757d562782f1c7ace423490 Mon Sep 17 00:00:00 2001 From: Rel1cx Date: Fri, 18 Apr 2025 11:15:28 +0800 Subject: [PATCH 01/78] docs: minor improvements --- apps/website/content/docs/rules/overview.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/website/content/docs/rules/overview.mdx b/apps/website/content/docs/rules/overview.mdx index 09445b298e..363bcea6e2 100644 --- a/apps/website/content/docs/rules/overview.mdx +++ b/apps/website/content/docs/rules/overview.mdx @@ -25,7 +25,7 @@ Linter rules can have false positives, false negatives, and some rules are depen -Rules prefixed with `jsx-` check for issues exclusive to JSX syntax, which are absent from standard JavaScript code (like handwritten `createElement()` calls). +The `jsx-*` rules check for issues exclusive to JSX syntax, which are absent from standard JavaScript (like handwritten `createElement()` calls). From e9e44ccb5e94534038e6019c40f0c6b0c03d20ce Mon Sep 17 00:00:00 2001 From: Rel1cx Date: Fri, 18 Apr 2025 12:16:47 +0800 Subject: [PATCH 02/78] refactor!: restructure rules --- VERSION | 2 +- apps/website/content/docs/roadmap.md | 6 +- apps/website/content/docs/rules/meta.json | 11 +- apps/website/content/docs/rules/overview.mdx | 10 +- apps/website/package.json | 4 +- examples/next-app/package.json | 4 +- package.json | 6 +- packages/core/package.json | 2 +- .../eslint-plugin-react-debug/package.json | 2 +- .../eslint-plugin-react-debug/src/plugin.ts | 4 - .../eslint-plugin-react-dom/package.json | 2 +- .../eslint-plugin-react-dom/src/plugin.ts | 4 - .../src/rules/prefer-namespace-import.md | 45 ++++ .../src/rules/prefer-namespace-import.spec.ts | 77 +++++++ .../src/rules/prefer-namespace-import.ts | 78 +++++++ .../package.json | 2 +- .../src/plugin.ts | 12 -- .../package.json | 2 +- .../eslint-plugin-react-web-api/package.json | 2 +- .../eslint-plugin-react-x/package.json | 2 +- .../src/configs/recommended.ts | 3 +- .../eslint-plugin-react-x/src/plugin.ts | 42 ++-- .../src/rules/avoid-shorthand-boolean.md | 59 ------ .../src/rules/avoid-shorthand-boolean.spec.ts | 47 ----- .../src/rules/avoid-shorthand-boolean.ts | 48 ----- .../src/rules/avoid-shorthand-fragment.md | 67 ------ .../rules/avoid-shorthand-fragment.spec.ts | 94 --------- .../src/rules/avoid-shorthand-fragment.ts | 49 ----- ...xtnodes.md => jsx-no-comment-textnodes.md} | 10 +- ...ec.ts => jsx-no-comment-textnodes.spec.ts} | 16 +- ...xtnodes.ts => jsx-no-comment-textnodes.ts} | 6 +- .../src/rules/jsx-shorthand-boolean.md | 59 ++++++ ....spec.ts => jsx-shorthand-boolean.spec.ts} | 8 +- ...nd-boolean.ts => jsx-shorthand-boolean.ts} | 6 +- .../src/rules/jsx-shorthand-fragment.md | 67 ++++++ ...spec.ts => jsx-shorthand-fragment.spec.ts} | 8 +- ...-fragment.ts => jsx-shorthand-fragment.ts} | 8 +- ...e-import.md => prefer-namespace-import.md} | 10 +- ...pec.ts => prefer-namespace-import.spec.ts} | 36 ++-- ...e-import.ts => prefer-namespace-import.ts} | 6 +- .../src/rules/prefer-shorthand-boolean.md | 63 ------ .../src/rules/prefer-shorthand-fragment.md | 71 ------- packages/plugins/eslint-plugin/package.json | 2 +- .../plugins/eslint-plugin/src/configs/all.ts | 6 +- packages/plugins/eslint-plugin/src/index.ts | 9 - packages/shared/package.json | 2 +- packages/utilities/ast/package.json | 2 +- packages/utilities/eff/package.json | 2 +- packages/utilities/kit/package.json | 2 +- packages/utilities/var/package.json | 2 +- pnpm-lock.yaml | 194 +++++++++++------- 51 files changed, 548 insertions(+), 733 deletions(-) create mode 100644 packages/plugins/eslint-plugin-react-dom/src/rules/prefer-namespace-import.md create mode 100644 packages/plugins/eslint-plugin-react-dom/src/rules/prefer-namespace-import.spec.ts create mode 100644 packages/plugins/eslint-plugin-react-dom/src/rules/prefer-namespace-import.ts delete mode 100644 packages/plugins/eslint-plugin-react-x/src/rules/avoid-shorthand-boolean.md delete mode 100644 packages/plugins/eslint-plugin-react-x/src/rules/avoid-shorthand-boolean.spec.ts delete mode 100644 packages/plugins/eslint-plugin-react-x/src/rules/avoid-shorthand-boolean.ts delete mode 100644 packages/plugins/eslint-plugin-react-x/src/rules/avoid-shorthand-fragment.md delete mode 100644 packages/plugins/eslint-plugin-react-x/src/rules/avoid-shorthand-fragment.spec.ts delete mode 100644 packages/plugins/eslint-plugin-react-x/src/rules/avoid-shorthand-fragment.ts rename packages/plugins/eslint-plugin-react-x/src/rules/{no-comment-textnodes.md => jsx-no-comment-textnodes.md} (86%) rename packages/plugins/eslint-plugin-react-x/src/rules/{no-comment-textnodes.spec.ts => jsx-no-comment-textnodes.spec.ts} (66%) rename packages/plugins/eslint-plugin-react-x/src/rules/{no-comment-textnodes.ts => jsx-no-comment-textnodes.ts} (93%) create mode 100644 packages/plugins/eslint-plugin-react-x/src/rules/jsx-shorthand-boolean.md rename packages/plugins/eslint-plugin-react-x/src/rules/{prefer-shorthand-boolean.spec.ts => jsx-shorthand-boolean.spec.ts} (79%) rename packages/plugins/eslint-plugin-react-x/src/rules/{prefer-shorthand-boolean.ts => jsx-shorthand-boolean.ts} (89%) create mode 100644 packages/plugins/eslint-plugin-react-x/src/rules/jsx-shorthand-fragment.md rename packages/plugins/eslint-plugin-react-x/src/rules/{prefer-shorthand-fragment.spec.ts => jsx-shorthand-fragment.spec.ts} (84%) rename packages/plugins/eslint-plugin-react-x/src/rules/{prefer-shorthand-fragment.ts => jsx-shorthand-fragment.ts} (83%) rename packages/plugins/eslint-plugin-react-x/src/rules/{prefer-react-namespace-import.md => prefer-namespace-import.md} (71%) rename packages/plugins/eslint-plugin-react-x/src/rules/{prefer-react-namespace-import.spec.ts => prefer-namespace-import.spec.ts} (80%) rename packages/plugins/eslint-plugin-react-x/src/rules/{prefer-react-namespace-import.ts => prefer-namespace-import.ts} (92%) delete mode 100644 packages/plugins/eslint-plugin-react-x/src/rules/prefer-shorthand-boolean.md delete mode 100644 packages/plugins/eslint-plugin-react-x/src/rules/prefer-shorthand-fragment.md diff --git a/VERSION b/VERSION index 41c8962f6f..4a73b25648 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.48.3 \ No newline at end of file +2.0.0-next.0 \ No newline at end of file diff --git a/apps/website/content/docs/roadmap.md b/apps/website/content/docs/roadmap.md index e806a04fbf..a59b7c81cb 100644 --- a/apps/website/content/docs/roadmap.md +++ b/apps/website/content/docs/roadmap.md @@ -40,9 +40,9 @@ Minimum supported versions: - [ ] `function-component-definition` - [x] `no-useless-fragment` -- [x] `prefer-shorthand-fragment` -- [x] `prefer-react-namespace-import` -- [x] `prefer-shorthand-boolean` +- [x] `jsx-shorthand-fragment` +- [x] `prefer-namespace-import` +- [x] `jsx-shorthand-boolean` ### Add suggestion-fix feature to rules that can be fixed interactively diff --git a/apps/website/content/docs/rules/meta.json b/apps/website/content/docs/rules/meta.json index 3fe23e91c5..6caee20618 100644 --- a/apps/website/content/docs/rules/meta.json +++ b/apps/website/content/docs/rules/meta.json @@ -2,8 +2,11 @@ "pages": [ "overview", "---X Rules---", + "jsx-no-comment-textnodes", "jsx-no-duplicate-props", "jsx-no-undef", + "jsx-shorthand-boolean", + "jsx-shorthand-fragment", "jsx-uses-react", "jsx-uses-vars", "no-access-state-in-setstate", @@ -16,8 +19,6 @@ "no-children-to-array", "no-class-component", "no-clone-element", - "no-comment-textnodes", - "no-complex-conditional-rendering", "no-component-will-mount", "no-component-will-receive-props", "no-component-will-update", @@ -52,12 +53,8 @@ "no-useless-forward-ref", "no-useless-fragment", "prefer-destructuring-assignment", - "prefer-react-namespace-import", + "prefer-namespace-import", "prefer-read-only-props", - "prefer-shorthand-boolean", - "prefer-shorthand-fragment", - "avoid-shorthand-boolean", - "avoid-shorthand-fragment", "---DOM Rules---", "dom-no-dangerously-set-innerhtml", "dom-no-dangerously-set-innerhtml-with-children", diff --git a/apps/website/content/docs/rules/overview.mdx b/apps/website/content/docs/rules/overview.mdx index 363bcea6e2..0db2a8470c 100644 --- a/apps/website/content/docs/rules/overview.mdx +++ b/apps/website/content/docs/rules/overview.mdx @@ -31,8 +31,11 @@ The `jsx-*` rules check for issues exclusive to JSX syntax, which are absent fro | Rule | ✅ | 🌟 | Description | `react` | | :----------------------------------------------------------------------------------- | :-- | :-------: | :-------------------------------------------------------------------------------------------------- | :------: | +| [`jsx-no-comment-textnodes`](./jsx-no-comment-textnodes) | 1️⃣ | | Prevents comments from being inserted as text nodes | | | [`jsx-no-duplicate-props`](./jsx-no-duplicate-props) | 1️⃣ | | Disallow duplicate props in JSX elements | | | [`jsx-no-undef`](./jsx-no-undef) | 0️⃣ | | Disallow undefined variables in JSX elements | | +| [`jsx-shorthand-boolean`](./jsx-shorthand-boolean) | 0️⃣ | | Enforces the use of _shorthand_ or _explicit_ boolean attributes | | +| [`jsx-shorthand-fragment`](./jsx-shorthand-fragment) | 0️⃣ | | Enforces the use of shorthand `<>` or `` syntax or `` element | | | [`jsx-uses-react`](./jsx-uses-react) | 1️⃣ | | Marks React variables as used when JSX is used | | | [`jsx-uses-vars`](./jsx-uses-vars) | 1️⃣ | | Marks variables used in JSX elements as used | | | [`no-access-state-in-setstate`](./no-access-state-in-setstate) | 2️⃣ | | Disallow accessing `this.state` inside `setState` calls | | @@ -45,7 +48,6 @@ The `jsx-*` rules check for issues exclusive to JSX syntax, which are absent fro | [`no-children-to-array`](./no-children-to-array) | 1️⃣ | | Disallow `Children.toArray` | | | [`no-class-component`](./no-class-component) | 0️⃣ | | Disallow class components except for error boundaries | | | [`no-clone-element`](./no-clone-element) | 1️⃣ | | Disallow `cloneElement` | | -| [`no-comment-textnodes`](./no-comment-textnodes) | 1️⃣ | | Prevents comments from being inserted as text nodes | | | [`no-complex-conditional-rendering`](./no-complex-conditional-rendering) | 0️⃣ | `🧪` | Disallow complex conditional rendering in JSX expressions | | | [`no-component-will-mount`](./no-component-will-mount) | 2️⃣ | `🔄` | Replaces usages of `componentWillMount` with `UNSAFE_componentWillMount` | >=16.3.0 | | [`no-component-will-receive-props`](./no-component-will-receive-props) | 2️⃣ | `🔄` | Replaces usages of `componentWillReceiveProps` with `UNSAFE_componentWillReceiveProps` | >=16.3.0 | @@ -81,12 +83,8 @@ The `jsx-*` rules check for issues exclusive to JSX syntax, which are absent fro | [`no-useless-forward-ref`](./no-useless-forward-ref) | 1️⃣ | | Disallow useless `forwardRef` calls on components that don't use `ref`s | | | [`no-useless-fragment`](./no-useless-fragment) | 0️⃣ | `🔧` `⚙️` | Disallow useless fragment elements | | | [`prefer-destructuring-assignment`](./prefer-destructuring-assignment) | 0️⃣ | | Enforces destructuring assignment for component props and context | | -| [`prefer-react-namespace-import`](./prefer-react-namespace-import) | 0️⃣ | `🔧` | Enforces React is imported via a namespace import | | +| [`prefer-namespace-import`](./prefer-namespace-import) | 0️⃣ | `🔧` | Enforces React is imported via a namespace import | | | [`prefer-read-only-props`](./prefer-read-only-props) | 0️⃣ | `💭` `🧪` | Enforces read-only props in components | | -| [`prefer-shorthand-boolean`](./prefer-shorthand-boolean) | 0️⃣ | `🔧` | Enforces shorthand syntax for boolean attributes | | -| [`prefer-shorthand-fragment`](./prefer-shorthand-fragment) | 0️⃣ | `🔧` | Enforces shorthand syntax for fragments | | -| [`avoid-shorthand-boolean`](./avoid-shorthand-boolean) | 0️⃣ | `🔧` | Enforces explicit boolean values for boolean attributes | | -| [`avoid-shorthand-fragment`](./avoid-shorthand-fragment) | 0️⃣ | | Enforces explicit `` components instead of the shorthand `<>` or `` syntax | | ## DOM Rules diff --git a/apps/website/package.json b/apps/website/package.json index 42ad504fbe..798ab38c67 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -20,7 +20,7 @@ "fumadocs-typescript": "4.0.2", "fumadocs-ui": "15.2.7", "lucide-react": "^0.488.0", - "next": "^15.3.0", + "next": "^15.3.1", "next-view-transitions": "^0.3.4", "react": "^19.1.0", "react-dom": "^19.1.0", @@ -33,7 +33,7 @@ "@eslint-react/kit": "workspace:*", "@eslint-react/shared": "workspace:*", "@eslint/js": "^9.24.0", - "@eslint/markdown": "^6.3.0", + "@eslint/markdown": "^6.4.0", "@local/configs": "workspace:*", "@mdx-js/mdx": "^3.1.0", "@tailwindcss/postcss": "^4.1.4", diff --git a/examples/next-app/package.json b/examples/next-app/package.json index 1912e67dd2..d8d34e447c 100644 --- a/examples/next-app/package.json +++ b/examples/next-app/package.json @@ -9,7 +9,7 @@ "start": "next start" }, "dependencies": { - "next": "^15.3.0", + "next": "^15.3.1", "react": "^19.1.0", "react-dom": "^19.1.0" }, @@ -17,7 +17,7 @@ "@eslint-react/eslint-plugin": "workspace:*", "@eslint/config-inspector": "^1.0.2", "@eslint/js": "^9.24.0", - "@next/eslint-plugin-next": "^15.3.0", + "@next/eslint-plugin-next": "^15.3.1", "@tsconfig/next": "^2.0.3", "@tsconfig/node22": "^22.0.1", "@tsconfig/strictest": "^2.0.5", diff --git a/package.json b/package.json index 39add8c913..ff121dc421 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@eslint-react/monorepo", - "version": "1.48.3", + "version": "2.0.0-next.0", "private": true, "description": "Monorepo for eslint-plugin-react-[x, dom, web-api, hooks-extra, naming-convention].", "keywords": [ @@ -53,7 +53,7 @@ }, "devDependencies": { "@eslint/config-inspector": "^1.0.2", - "@eslint/markdown": "^6.3.0", + "@eslint/markdown": "^6.4.0", "@local/configs": "workspace:*", "@local/eslint-plugin-local": "workspace:*", "@swc/core": "^1.11.21", @@ -114,7 +114,7 @@ "cross-spawn": "^7.0.6", "esbuild": "^0.25.2", "lucide-react": "^0.488.0", - "next": "^15.3.0", + "next": "^15.3.1", "react": "^19.1.0", "react-dom": "^19.1.0", "ts-api-utils": "^2.1.0", diff --git a/packages/core/package.json b/packages/core/package.json index 52e80453d9..0d335a4634 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@eslint-react/core", - "version": "1.48.3", + "version": "2.0.0-next.0", "description": "ESLint React's ESLint utility module for static analysis of React core APIs and patterns.", "homepage": "https://github.com/Rel1cx/eslint-react", "bugs": { diff --git a/packages/plugins/eslint-plugin-react-debug/package.json b/packages/plugins/eslint-plugin-react-debug/package.json index 775bb1e897..5026f04e0f 100644 --- a/packages/plugins/eslint-plugin-react-debug/package.json +++ b/packages/plugins/eslint-plugin-react-debug/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-react-debug", - "version": "1.48.3", + "version": "2.0.0-next.0", "description": "ESLint React's ESLint plugin for debugging related rules.", "keywords": [ "react", diff --git a/packages/plugins/eslint-plugin-react-debug/src/plugin.ts b/packages/plugins/eslint-plugin-react-debug/src/plugin.ts index e0710a251e..bf345d6e8c 100644 --- a/packages/plugins/eslint-plugin-react-debug/src/plugin.ts +++ b/packages/plugins/eslint-plugin-react-debug/src/plugin.ts @@ -16,9 +16,5 @@ export const plugin = { ["hook"]: hook, ["is-from-react"]: isFromReact, ["jsx"]: jsx, - - // Part: deprecated rules - /** @deprecated Use `hook` instead */ - "react-hooks": hook, }, } as const; diff --git a/packages/plugins/eslint-plugin-react-dom/package.json b/packages/plugins/eslint-plugin-react-dom/package.json index aa0a8894be..2f23b6cef0 100644 --- a/packages/plugins/eslint-plugin-react-dom/package.json +++ b/packages/plugins/eslint-plugin-react-dom/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-react-dom", - "version": "1.48.3", + "version": "2.0.0-next.0", "description": "ESLint React's ESLint plugin for React DOM related rules.", "keywords": [ "react", diff --git a/packages/plugins/eslint-plugin-react-dom/src/plugin.ts b/packages/plugins/eslint-plugin-react-dom/src/plugin.ts index bb552c45f2..f0c0d232e1 100644 --- a/packages/plugins/eslint-plugin-react-dom/src/plugin.ts +++ b/packages/plugins/eslint-plugin-react-dom/src/plugin.ts @@ -38,9 +38,5 @@ export const plugin = { "no-unsafe-target-blank": noUnsafeTargetBlank, "no-use-form-state": noUseFormState, "no-void-elements-with-children": noVoidElementsWithChildren, - - // Part: deprecated rules - /** @deprecated Use `no-void-elements-with-children` instead */ - "no-children-in-void-dom-elements": noVoidElementsWithChildren, }, } as const; diff --git a/packages/plugins/eslint-plugin-react-dom/src/rules/prefer-namespace-import.md b/packages/plugins/eslint-plugin-react-dom/src/rules/prefer-namespace-import.md new file mode 100644 index 0000000000..2f96e2193d --- /dev/null +++ b/packages/plugins/eslint-plugin-react-dom/src/rules/prefer-namespace-import.md @@ -0,0 +1,45 @@ +--- +title: prefer-namespace-import +--- + +**Full Name in `eslint-plugin-react-dom`** + +```sh copy +react-dom/prefer-namespace-import +``` + +**Full Name in `@eslint-react/eslint-plugin`** + +```sh copy +@eslint-react/dom/prefer-namespace-import +``` + +**Features** + +`🔧` + +## Description + +Enforces React DOM is imported via a namespace import. + +## Examples + +### Failing + +```tsx +import ReactDOM from "react-dom/client"; + +import type ReactDOM from "react-dom/client"; +``` + +### Passing + +```tsx +import * as ReactDOM from "react-dom/client"; +import type * as ReactDOM from "react-dom/client"; +``` + +## Implementation + +- [Rule source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom/src/rules/prefer-namespace-import.ts) +- [Test source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom/src/rules/prefer-namespace-import.spec.ts) diff --git a/packages/plugins/eslint-plugin-react-dom/src/rules/prefer-namespace-import.spec.ts b/packages/plugins/eslint-plugin-react-dom/src/rules/prefer-namespace-import.spec.ts new file mode 100644 index 0000000000..4091cf8e80 --- /dev/null +++ b/packages/plugins/eslint-plugin-react-dom/src/rules/prefer-namespace-import.spec.ts @@ -0,0 +1,77 @@ +import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; + +import { ruleTester } from "../../../../../test"; +import rule, { RULE_NAME } from "./prefer-namespace-import"; + +ruleTester.run(RULE_NAME, rule, { + invalid: [ + { + code: `import ReactDOM from 'react-dom';`, + errors: [{ type: T.ImportDeclaration, messageId: "preferNamespaceImport" }], + output: `import * as ReactDOM from 'react-dom';`, + }, + { + code: `import ReactDom from 'react-dom';`, + errors: [{ type: T.ImportDeclaration, messageId: "preferNamespaceImport" }], + output: `import * as ReactDom from 'react-dom';`, + }, + { + code: `import REACTDOM from 'react-dom';`, + errors: [{ type: T.ImportDeclaration, messageId: "preferNamespaceImport" }], + output: `import * as REACTDOM from 'react-dom';`, + }, + { + code: `import ReactDOM from 'react-dom/client';`, + errors: [{ type: T.ImportDeclaration, messageId: "preferNamespaceImport" }], + output: `import * as ReactDOM from 'react-dom/client';`, + }, + { + code: `import ReactDom from 'react-dom/client';`, + errors: [{ type: T.ImportDeclaration, messageId: "preferNamespaceImport" }], + output: `import * as ReactDom from 'react-dom/client';`, + }, + { + code: `import REACTDOM from 'react-dom/client';`, + errors: [{ type: T.ImportDeclaration, messageId: "preferNamespaceImport" }], + output: `import * as REACTDOM from 'react-dom/client';`, + }, + ], + valid: [ + { + code: `import React from 'react';`, + }, + { + code: `import * as React from 'react';`, + }, + { + code: `import { createRoot } from 'react-dom/client';`, + }, + { + code: `import * as ReactDOM from 'react-dom';`, + }, + { + code: `import * as ReactDOM from 'react-dom/client';`, + }, + { + code: `import * as ReactDOM from 'react-dom/server';`, + }, + { + code: `import * as ReactDom from 'react-dom';`, + }, + { + code: `import * as ReactDom from 'react-dom/client';`, + }, + { + code: `import * as ReactDom from 'react-dom/server';`, + }, + { + code: `import * as REACTDOM from 'react-dom';`, + }, + { + code: `import * as REACTDOM from 'react-dom/client';`, + }, + { + code: `import * as REACTDOM from 'react-dom/server';`, + }, + ], +}); diff --git a/packages/plugins/eslint-plugin-react-dom/src/rules/prefer-namespace-import.ts b/packages/plugins/eslint-plugin-react-dom/src/rules/prefer-namespace-import.ts new file mode 100644 index 0000000000..93629f02c5 --- /dev/null +++ b/packages/plugins/eslint-plugin-react-dom/src/rules/prefer-namespace-import.ts @@ -0,0 +1,78 @@ +import type { TSESTree } from "@typescript-eslint/types"; +import type { RuleListener } from "@typescript-eslint/utils/ts-eslint"; +import type { CamelCase } from "string-ts"; +import { type RuleContext, type RuleFeature } from "@eslint-react/kit"; +import { getSettingsFromContext } from "@eslint-react/shared"; + +import { createRule } from "../utils"; + +export const RULE_NAME = "prefer-namespace-import"; + +export const RULE_FEATURES = [ + "FIX", +] as const satisfies RuleFeature[]; + +export type MessageID = CamelCase; + +export default createRule<[], MessageID>({ + meta: { + type: "problem", + docs: { + description: "Enforces React Dom is imported via a namespace import.", + [Symbol.for("rule_features")]: RULE_FEATURES, + }, + fixable: "code", + messages: { + preferNamespaceImport: "Prefer importing React DOM via a namespace import.", + }, + schema: [], + }, + name: RULE_NAME, + create, + defaultOptions: [], +}); + +const importSources = [ + "react-dom", + "react-dom/client", + "react-dom/server", +]; + +export function create(context: RuleContext): RuleListener { + return { + [`ImportDeclaration ImportDefaultSpecifier`](node: TSESTree.ImportDefaultSpecifier) { + const importSource = node.parent.source.value; + if (!importSources.includes(importSource)) return; + const hasOtherSpecifiers = node.parent.specifiers.length > 1; + context.report({ + messageId: "preferNamespaceImport", + node: hasOtherSpecifiers ? node : node.parent, + data: { importSource }, + fix(fixer) { + const importDeclarationText = context.sourceCode.getText(node.parent); + const semi = importDeclarationText.endsWith(";") ? ";" : ""; + const quote = node.parent.source.raw.at(0) ?? "'"; + const isTypeImport = node.parent.importKind === "type"; + const importStringPrefix = `import${isTypeImport ? " type" : ""}`; + const importSourceQuoted = `${quote}${importSource}${quote}`; + if (!hasOtherSpecifiers) { + return fixer.replaceText( + node.parent, + `${importStringPrefix} * as ${node.local.name} from ${importSourceQuoted}${semi}`, + ); + } + // dprint-ignore + // remove the default specifier and prepend the namespace import specifier + const specifiers = importDeclarationText.slice(importDeclarationText.indexOf("{"), importDeclarationText.indexOf("}") + 1); + return fixer.replaceText( + node.parent, + [ + `${importStringPrefix} * as ${node.local.name} from ${importSourceQuoted}${semi}`, + `${importStringPrefix} ${specifiers} from ${importSourceQuoted}${semi}`, + ].join("\n"), + ); + }, + }); + }, + }; +} diff --git a/packages/plugins/eslint-plugin-react-hooks-extra/package.json b/packages/plugins/eslint-plugin-react-hooks-extra/package.json index 6eadbd704b..c8ff3fc965 100644 --- a/packages/plugins/eslint-plugin-react-hooks-extra/package.json +++ b/packages/plugins/eslint-plugin-react-hooks-extra/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-react-hooks-extra", - "version": "1.48.3", + "version": "2.0.0-next.0", "description": "ESLint React's ESLint plugin for React Hooks related rules.", "keywords": [ "react", 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 5442fbbd13..ca4310ef68 100644 --- a/packages/plugins/eslint-plugin-react-hooks-extra/src/plugin.ts +++ b/packages/plugins/eslint-plugin-react-hooks-extra/src/plugin.ts @@ -18,17 +18,5 @@ export const plugin = { "no-unnecessary-use-memo": noUnnecessaryUseMemo, "no-unnecessary-use-prefix": noUnnecessaryUsePrefix, "prefer-use-state-lazy-initialization": preferUseStateLazyInitialization, - - // Part: deprecated rules - /** @deprecated Use `no-unnecessary-use-prefix` instead */ - "ensure-custom-hooks-using-other-hooks": noUnnecessaryUsePrefix, - /** @deprecated Use `no-unnecessary-use-callback` instead */ - "ensure-use-callback-has-non-empty-deps": noUnnecessaryUseCallback, - /** @deprecated Use `no-unnecessary-use-memo` instead */ - "ensure-use-memo-has-non-empty-deps": noUnnecessaryUseMemo, - /** @deprecated Use `no-unnecessary-use-prefix` instead */ - "no-redundant-custom-hook": noUnnecessaryUsePrefix, - /** @deprecated Use `no-unnecessary-use-prefix` instead */ - "no-useless-custom-hooks": noUnnecessaryUsePrefix, }, } as const; diff --git a/packages/plugins/eslint-plugin-react-naming-convention/package.json b/packages/plugins/eslint-plugin-react-naming-convention/package.json index ba3542564e..8966594a95 100644 --- a/packages/plugins/eslint-plugin-react-naming-convention/package.json +++ b/packages/plugins/eslint-plugin-react-naming-convention/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-react-naming-convention", - "version": "1.48.3", + "version": "2.0.0-next.0", "description": "ESLint React's ESLint plugin for naming convention related rules.", "keywords": [ "react", diff --git a/packages/plugins/eslint-plugin-react-web-api/package.json b/packages/plugins/eslint-plugin-react-web-api/package.json index 4c0a810f97..13d8ee3b25 100644 --- a/packages/plugins/eslint-plugin-react-web-api/package.json +++ b/packages/plugins/eslint-plugin-react-web-api/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-react-web-api", - "version": "1.48.3", + "version": "2.0.0-next.0", "description": "ESLint React's ESLint plugin for interacting with Web APIs", "keywords": [ "react", diff --git a/packages/plugins/eslint-plugin-react-x/package.json b/packages/plugins/eslint-plugin-react-x/package.json index ab5ef30d21..3bc5d590c6 100644 --- a/packages/plugins/eslint-plugin-react-x/package.json +++ b/packages/plugins/eslint-plugin-react-x/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-react-x", - "version": "1.48.3", + "version": "2.0.0-next.0", "description": "A set of composable ESLint rules for for libraries and frameworks that use React as a UI runtime.", "keywords": [ "react", diff --git a/packages/plugins/eslint-plugin-react-x/src/configs/recommended.ts b/packages/plugins/eslint-plugin-react-x/src/configs/recommended.ts index 33f9165372..4e1bfadda0 100644 --- a/packages/plugins/eslint-plugin-react-x/src/configs/recommended.ts +++ b/packages/plugins/eslint-plugin-react-x/src/configs/recommended.ts @@ -4,7 +4,9 @@ import { DEFAULT_ESLINT_REACT_SETTINGS } from "@eslint-react/shared"; export const name = "react-x/recommended"; export const rules = { + "react-x/jsx-no-comment-textnodes": "warn", "react-x/jsx-no-duplicate-props": "warn", + "react-x/jsx-no-undef": "error", "react-x/jsx-uses-react": "warn", "react-x/jsx-uses-vars": "warn", "react-x/no-access-state-in-setstate": "error", @@ -15,7 +17,6 @@ export const rules = { "react-x/no-children-only": "warn", "react-x/no-children-to-array": "warn", "react-x/no-clone-element": "warn", - "react-x/no-comment-textnodes": "warn", "react-x/no-component-will-mount": "error", "react-x/no-component-will-receive-props": "error", "react-x/no-component-will-update": "error", diff --git a/packages/plugins/eslint-plugin-react-x/src/plugin.ts b/packages/plugins/eslint-plugin-react-x/src/plugin.ts index 5d7bf3f09e..58fa180b29 100644 --- a/packages/plugins/eslint-plugin-react-x/src/plugin.ts +++ b/packages/plugins/eslint-plugin-react-x/src/plugin.ts @@ -1,8 +1,9 @@ import { name, version } from "../package.json"; -import avoidShorthandBoolean from "./rules/avoid-shorthand-boolean"; -import avoidShorthandFragment from "./rules/avoid-shorthand-fragment"; +import jsxNoCommentTextnodes from "./rules/jsx-no-comment-textnodes"; import jsxNoDuplicateProps from "./rules/jsx-no-duplicate-props"; import jsxNoUndef from "./rules/jsx-no-undef"; +import jsxShorthandBoolean from "./rules/jsx-shorthand-boolean"; +import jsxShorthandFragment from "./rules/jsx-shorthand-fragment"; import jsxUsesReact from "./rules/jsx-uses-react"; import jsxUsesVars from "./rules/jsx-uses-vars"; import noAccessStateInSetstate from "./rules/no-access-state-in-setstate"; @@ -15,7 +16,6 @@ import noChildrenProp from "./rules/no-children-prop"; import noChildrenToArray from "./rules/no-children-to-array"; import noClassComponent from "./rules/no-class-component"; import noCloneElement from "./rules/no-clone-element"; -import noCommentTextnodes from "./rules/no-comment-textnodes"; import noComplexConditionalRendering from "./rules/no-complex-conditional-rendering"; import noComponentWillMount from "./rules/no-component-will-mount"; import noComponentWillReceiveProps from "./rules/no-component-will-receive-props"; @@ -51,10 +51,8 @@ import noUseContext from "./rules/no-use-context"; import noUselessForwardRef from "./rules/no-useless-forward-ref"; import noUselessFragment from "./rules/no-useless-fragment"; import preferDestructuringAssignment from "./rules/prefer-destructuring-assignment"; -import preferReactNamespaceImport from "./rules/prefer-react-namespace-import"; +import preferNamespaceImport from "./rules/prefer-namespace-import"; import preferReadOnlyProps from "./rules/prefer-read-only-props"; -import preferShorthandBoolean from "./rules/prefer-shorthand-boolean"; -import preferShorthandFragment from "./rules/prefer-shorthand-fragment"; export const plugin = { meta: { @@ -62,8 +60,13 @@ export const plugin = { version, }, rules: { - "avoid-shorthand-boolean": avoidShorthandBoolean, - "avoid-shorthand-fragment": avoidShorthandFragment, + "jsx-no-comment-textnodes": jsxNoCommentTextnodes, + "jsx-no-duplicate-props": jsxNoDuplicateProps, + "jsx-no-undef": jsxNoUndef, + "jsx-shorthand-boolean": jsxShorthandBoolean, + "jsx-shorthand-fragment": jsxShorthandFragment, + "jsx-uses-react": jsxUsesReact, + "jsx-uses-vars": jsxUsesVars, "no-access-state-in-setstate": noAccessStateInSetstate, "no-array-index-key": noArrayIndexKey, "no-children-count": noChildrenCount, @@ -74,7 +77,6 @@ export const plugin = { "no-children-to-array": noChildrenToArray, "no-class-component": noClassComponent, "no-clone-element": noCloneElement, - "no-comment-textnodes": noCommentTextnodes, "no-complex-conditional-rendering": noComplexConditionalRendering, "no-component-will-mount": noComponentWillMount, "no-component-will-receive-props": noComponentWillReceiveProps, @@ -110,27 +112,7 @@ export const plugin = { "no-useless-forward-ref": noUselessForwardRef, "no-useless-fragment": noUselessFragment, "prefer-destructuring-assignment": preferDestructuringAssignment, - "prefer-react-namespace-import": preferReactNamespaceImport, + "prefer-namespace-import": preferNamespaceImport, "prefer-read-only-props": preferReadOnlyProps, - "prefer-shorthand-boolean": preferShorthandBoolean, - "prefer-shorthand-fragment": preferShorthandFragment, - - // Part: JSX only rules - "jsx-no-duplicate-props": jsxNoDuplicateProps, - "jsx-no-undef": jsxNoUndef, - "jsx-uses-react": jsxUsesReact, - "jsx-uses-vars": jsxUsesVars, - - // Part: deprecated rules - /** @deprecated Use `no-useless-forward-ref` instead */ - "ensure-forward-ref-using-ref": noUselessForwardRef, - /** @deprecated Use `no-complex-conditional-rendering` instead */ - "no-complicated-conditional-rendering": noComplexConditionalRendering, - /** @deprecated Use `jsx-no-duplicate-props` instead */ - "no-duplicate-jsx-props": jsxNoDuplicateProps, - /** @deprecated Use `no-nested-component-definitions` instead */ - "no-nested-components": noNestedComponentDefinitions, - /** @deprecated Use `jsx-uses-vars` instead */ - "use-jsx-vars": jsxUsesVars, }, } as const; diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/avoid-shorthand-boolean.md b/packages/plugins/eslint-plugin-react-x/src/rules/avoid-shorthand-boolean.md deleted file mode 100644 index b41612e929..0000000000 --- a/packages/plugins/eslint-plugin-react-x/src/rules/avoid-shorthand-boolean.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -title: avoid-shorthand-boolean ---- - -**Full Name in `eslint-plugin-react-x`** - -```sh copy -react-x/avoid-shorthand-boolean -``` - -**Full Name in `@eslint-react/eslint-plugin`** - -```sh copy -@eslint-react/avoid-shorthand-boolean -``` - -**Features** - -`🔧` - -## Description - -Enforces explicit boolean values for boolean attributes. - -## Examples - -### Failing - -```tsx -const Input = ; -// ^^^^^^^ -// - Expected `checked={true}` instead of `checked` -const button = + ); + } else { + return null; + } + } + `, ], }); From 34a400b19ce8e2143e2850bc59a6086b818e10cd Mon Sep 17 00:00:00 2001 From: Rel1cx Date: Sun, 15 Jun 2025 16:07:04 +0800 Subject: [PATCH 73/78] refactor: unexport 'no-unnecessary-use-effect' rule --- packages/plugins/eslint-plugin-react-x/src/plugin.ts | 2 -- .../src/rules/no-unnecessary-use-effect.ts | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/plugins/eslint-plugin-react-x/src/plugin.ts b/packages/plugins/eslint-plugin-react-x/src/plugin.ts index d0b79b1db3..f4412b1143 100644 --- a/packages/plugins/eslint-plugin-react-x/src/plugin.ts +++ b/packages/plugins/eslint-plugin-react-x/src/plugin.ts @@ -42,7 +42,6 @@ import noSetStateInComponentDidUpdate from "./rules/no-set-state-in-component-di import noSetStateInComponentWillUpdate from "./rules/no-set-state-in-component-will-update"; import noStringRefs from "./rules/no-string-refs"; import noUnnecessaryUseCallback from "./rules/no-unnecessary-use-callback"; -import noUnnecessaryUseEffect from "./rules/no-unnecessary-use-effect"; import noUnnecessaryUseMemo from "./rules/no-unnecessary-use-memo"; import noUnnecessaryUsePrefix from "./rules/no-unnecessary-use-prefix"; import noUnsafeComponentWillMount from "./rules/no-unsafe-component-will-mount"; @@ -108,7 +107,6 @@ export const plugin = { "no-set-state-in-component-will-update": noSetStateInComponentWillUpdate, "no-string-refs": noStringRefs, "no-unnecessary-use-callback": noUnnecessaryUseCallback, - "no-unnecessary-use-effect": noUnnecessaryUseEffect, "no-unnecessary-use-memo": noUnnecessaryUseMemo, "no-unnecessary-use-prefix": noUnnecessaryUsePrefix, "no-unsafe-component-will-mount": noUnsafeComponentWillMount, diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-unnecessary-use-effect.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-unnecessary-use-effect.ts index b2d706f0c0..61f17e3127 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-unnecessary-use-effect.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-unnecessary-use-effect.ts @@ -20,7 +20,7 @@ export default createRule<[], MessageID>({ [Symbol.for("rule_features")]: RULE_FEATURES, }, messages: { - // TODO: Align the error messages precisely with the 6 scenarios described in react.dev/learn/you-might-not-need-an-effect. + // TODO: Align the error messages with the scenarios described in react.dev/learn/you-might-not-need-an-effect. noUnnecessaryUseEffect: "You Might Not Need an Effect. Visit https://react.dev/learn/you-might-not-need-an-effect to learn how to remove unnecessary Effects.", }, From c93154d8f2bd38ae622e23c66265e02350eed4bc Mon Sep 17 00:00:00 2001 From: Rel1cx Date: Sun, 15 Jun 2025 16:18:10 +0800 Subject: [PATCH 74/78] fix: recover forward compatibility for shorthand rules --- .../src/plugin.ts | 8 +- .../eslint-plugin-react-x/src/plugin.ts | 14 +++ .../rules-removed/avoid-shorthand-boolean.md | 59 ++++++++++++ .../avoid-shorthand-boolean.spec.ts | 47 ++++++++++ .../rules-removed/avoid-shorthand-boolean.ts | 48 ++++++++++ .../rules-removed/avoid-shorthand-fragment.md | 67 +++++++++++++ .../avoid-shorthand-fragment.spec.ts | 94 +++++++++++++++++++ .../rules-removed/avoid-shorthand-fragment.ts | 49 ++++++++++ .../rules-removed/prefer-shorthand-boolean.md | 63 +++++++++++++ .../prefer-shorthand-boolean.spec.ts | 41 ++++++++ .../rules-removed/prefer-shorthand-boolean.ts | 59 ++++++++++++ .../prefer-shorthand-fragment.md | 71 ++++++++++++++ .../prefer-shorthand-fragment.spec.ts | 58 ++++++++++++ .../prefer-shorthand-fragment.ts | 59 ++++++++++++ 14 files changed, 734 insertions(+), 3 deletions(-) create mode 100644 packages/plugins/eslint-plugin-react-x/src/rules-removed/avoid-shorthand-boolean.md create mode 100644 packages/plugins/eslint-plugin-react-x/src/rules-removed/avoid-shorthand-boolean.spec.ts create mode 100644 packages/plugins/eslint-plugin-react-x/src/rules-removed/avoid-shorthand-boolean.ts create mode 100644 packages/plugins/eslint-plugin-react-x/src/rules-removed/avoid-shorthand-fragment.md create mode 100644 packages/plugins/eslint-plugin-react-x/src/rules-removed/avoid-shorthand-fragment.spec.ts create mode 100644 packages/plugins/eslint-plugin-react-x/src/rules-removed/avoid-shorthand-fragment.ts create mode 100644 packages/plugins/eslint-plugin-react-x/src/rules-removed/prefer-shorthand-boolean.md create mode 100644 packages/plugins/eslint-plugin-react-x/src/rules-removed/prefer-shorthand-boolean.spec.ts create mode 100644 packages/plugins/eslint-plugin-react-x/src/rules-removed/prefer-shorthand-boolean.ts create mode 100644 packages/plugins/eslint-plugin-react-x/src/rules-removed/prefer-shorthand-fragment.md create mode 100644 packages/plugins/eslint-plugin-react-x/src/rules-removed/prefer-shorthand-fragment.spec.ts create mode 100644 packages/plugins/eslint-plugin-react-x/src/rules-removed/prefer-shorthand-fragment.ts 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 ed0a909947..7a70f42274 100644 --- a/packages/plugins/eslint-plugin-react-hooks-extra/src/plugin.ts +++ b/packages/plugins/eslint-plugin-react-hooks-extra/src/plugin.ts @@ -1,12 +1,14 @@ import { name, version } from "../package.json"; +import noDirectSetStateInUseEffect from "./rules/no-direct-set-state-in-use-effect"; +import noDirectSetStateInUseLayoutEffect from "./rules/no-direct-set-state-in-use-layout-effect"; + +/* eslint-disable perfectionist/sort-imports */ import noUnnecessaryUseCallback from "./rules-removed/no-unnecessary-use-callback"; import noUnnecessaryUseMemo from "./rules-removed/no-unnecessary-use-memo"; import noUnnecessaryUsePrefix from "./rules-removed/no-unnecessary-use-prefix"; import preferUseStateLazyInitialization from "./rules-removed/prefer-use-state-lazy-initialization"; - -import noDirectSetStateInUseEffect from "./rules/no-direct-set-state-in-use-effect"; -import noDirectSetStateInUseLayoutEffect from "./rules/no-direct-set-state-in-use-layout-effect"; +/* eslint-enable perfectionist/sort-imports */ export const plugin = { meta: { diff --git a/packages/plugins/eslint-plugin-react-x/src/plugin.ts b/packages/plugins/eslint-plugin-react-x/src/plugin.ts index f4412b1143..4f4afeb21d 100644 --- a/packages/plugins/eslint-plugin-react-x/src/plugin.ts +++ b/packages/plugins/eslint-plugin-react-x/src/plugin.ts @@ -1,4 +1,5 @@ import { name, version } from "../package.json"; + import jsxKeyBeforeSpread from "./rules/jsx-key-before-spread"; import jsxNoCommentTextnodes from "./rules/jsx-no-comment-textnodes"; import jsxNoDuplicateProps from "./rules/jsx-no-duplicate-props"; @@ -58,6 +59,13 @@ import preferNamespaceImport from "./rules/prefer-namespace-import"; import preferReadOnlyProps from "./rules/prefer-read-only-props"; import preferUseStateLazyInitialization from "./rules/prefer-use-state-lazy-initialization"; +/* eslint-disable perfectionist/sort-imports */ +import avoidShorthandBoolean from "./rules-removed/avoid-shorthand-boolean"; +import avoidShorthandFragment from "./rules-removed/avoid-shorthand-fragment"; +import preferShorthandBoolean from "./rules-removed/prefer-shorthand-boolean"; +import preferShorthandFragment from "./rules-removed/prefer-shorthand-fragment"; +/* eslint-enable perfectionist/sort-imports */ + export const plugin = { meta: { name, @@ -122,5 +130,11 @@ export const plugin = { "prefer-namespace-import": preferNamespaceImport, "prefer-read-only-props": preferReadOnlyProps, "prefer-use-state-lazy-initialization": preferUseStateLazyInitialization, + + // Removed rules + "avoid-shorthand-boolean": avoidShorthandBoolean, + "avoid-shorthand-fragment": avoidShorthandFragment, + "prefer-shorthand-boolean": preferShorthandBoolean, + "prefer-shorthand-fragment": preferShorthandFragment, }, } as const; diff --git a/packages/plugins/eslint-plugin-react-x/src/rules-removed/avoid-shorthand-boolean.md b/packages/plugins/eslint-plugin-react-x/src/rules-removed/avoid-shorthand-boolean.md new file mode 100644 index 0000000000..b41612e929 --- /dev/null +++ b/packages/plugins/eslint-plugin-react-x/src/rules-removed/avoid-shorthand-boolean.md @@ -0,0 +1,59 @@ +--- +title: avoid-shorthand-boolean +--- + +**Full Name in `eslint-plugin-react-x`** + +```sh copy +react-x/avoid-shorthand-boolean +``` + +**Full Name in `@eslint-react/eslint-plugin`** + +```sh copy +@eslint-react/avoid-shorthand-boolean +``` + +**Features** + +`🔧` + +## Description + +Enforces explicit boolean values for boolean attributes. + +## Examples + +### Failing + +```tsx +const Input = ; +// ^^^^^^^ +// - Expected `checked={true}` instead of `checked` +const button = + + + ); +} + +export default App; diff --git a/examples/vite-react-dom-app-v1/src/assets/eslint-react.svg b/examples/vite-react-dom-app-v1/src/assets/eslint-react.svg new file mode 100644 index 0000000000..5573445ab8 --- /dev/null +++ b/examples/vite-react-dom-app-v1/src/assets/eslint-react.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/examples/vite-react-dom-app-v1/src/assets/react.svg b/examples/vite-react-dom-app-v1/src/assets/react.svg new file mode 100644 index 0000000000..bbcc554ee6 --- /dev/null +++ b/examples/vite-react-dom-app-v1/src/assets/react.svg @@ -0,0 +1,6 @@ + diff --git a/examples/vite-react-dom-app-v1/src/index.css b/examples/vite-react-dom-app-v1/src/index.css new file mode 100644 index 0000000000..f5ac7a3860 --- /dev/null +++ b/examples/vite-react-dom-app-v1/src/index.css @@ -0,0 +1,88 @@ +:root { + font-family: ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + Roboto, + 'Helvetica Neue', + Arial, + 'Noto Sans', + sans-serif, + 'Apple Color Emoji', + 'Segoe UI Emoji', + 'Segoe UI Symbol', + 'Noto Color Emoji'; + + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} + +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} + +button:hover { + border-color: #646cff; +} + +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + + a:hover { + color: #747bff; + } + + button { + background-color: #f9f9f9; + } +} diff --git a/examples/vite-react-dom-app-v1/src/main.ts b/examples/vite-react-dom-app-v1/src/main.ts new file mode 100644 index 0000000000..18c08d5d89 --- /dev/null +++ b/examples/vite-react-dom-app-v1/src/main.ts @@ -0,0 +1,7 @@ +import "./index.css"; + +import ReactDOM from "react-dom/client"; + +import { root } from "./root"; + +ReactDOM.createRoot(document.querySelector("#root")!).render(root); diff --git a/examples/vite-react-dom-app-v1/src/root.tsx b/examples/vite-react-dom-app-v1/src/root.tsx new file mode 100644 index 0000000000..df78caa030 --- /dev/null +++ b/examples/vite-react-dom-app-v1/src/root.tsx @@ -0,0 +1,9 @@ +import React from "react"; + +import App from "./App"; + +export const root = ( + + + +); diff --git a/examples/vite-react-dom-app-v1/tsconfig.app.json b/examples/vite-react-dom-app-v1/tsconfig.app.json new file mode 100644 index 0000000000..57ac7498dc --- /dev/null +++ b/examples/vite-react-dom-app-v1/tsconfig.app.json @@ -0,0 +1,22 @@ +{ + "extends": [ + "@tsconfig/strictest/tsconfig.json", + "@tsconfig/vite-react/tsconfig.json" + ], + "compilerOptions": { + "target": "ES2021", + "lib": [ + "ES2021", + "DOM", + "DOM.Iterable" + ], + "types": [ + "vite/client" + ], + "erasableSyntaxOnly": true + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx" + ] +} diff --git a/examples/vite-react-dom-app-v1/tsconfig.json b/examples/vite-react-dom-app-v1/tsconfig.json new file mode 100644 index 0000000000..1ffef600d9 --- /dev/null +++ b/examples/vite-react-dom-app-v1/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/examples/vite-react-dom-app-v1/tsconfig.node.json b/examples/vite-react-dom-app-v1/tsconfig.node.json new file mode 100644 index 0000000000..9614e693fc --- /dev/null +++ b/examples/vite-react-dom-app-v1/tsconfig.node.json @@ -0,0 +1,29 @@ +{ + "extends": [ + "@tsconfig/strictest/tsconfig.json", + "@tsconfig/node22/tsconfig.json" + ], + "compilerOptions": { + "incremental": false, + "skipLibCheck": true, + "module": "ESNext", + "moduleDetection": "force", + "moduleResolution": "bundler", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "allowJs": true, + "noEmit": true + }, + "include": [ + "*.ts", + "*.cts", + "*.mts", + "*.d.ts" + ], + "exclude": [ + "node_modules", + "dist", + "src", + "benchmark" + ] +} diff --git a/examples/vite-react-dom-app-v1/vite.config.ts b/examples/vite-react-dom-app-v1/vite.config.ts new file mode 100644 index 0000000000..a88da26ab2 --- /dev/null +++ b/examples/vite-react-dom-app-v1/vite.config.ts @@ -0,0 +1,9 @@ +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + react(), + ], +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 33280733f3..5ea500b089 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -573,6 +573,61 @@ importers: specifier: ^6.3.5 version: 6.3.5(@types/node@24.0.3)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.0)(terser@5.43.0)(tsx@4.20.3)(yaml@2.8.0) + examples/vite-react-dom-app-v1: + dependencies: + react: + specifier: ^19.1.0 + version: 19.1.0 + react-dom: + specifier: ^19.1.0 + version: 19.1.0(react@19.1.0) + devDependencies: + '@eslint-react/eslint-plugin': + specifier: workspace:* + version: link:../../packages/plugins/eslint-plugin + '@eslint/config-inspector': + specifier: ^1.1.0 + version: 1.1.0(eslint@9.29.0(jiti@2.4.2)) + '@eslint/js': + specifier: ^9.29.0 + version: 9.29.0 + '@tsconfig/node22': + specifier: ^22.0.2 + version: 22.0.2 + '@tsconfig/strictest': + specifier: ^2.0.5 + version: 2.0.5 + '@tsconfig/vite-react': + specifier: ^6.3.6 + version: 6.3.6 + '@types/react': + specifier: ^19.1.8 + version: 19.1.8 + '@types/react-dom': + specifier: ^19.1.6 + version: 19.1.6(@types/react@19.1.8) + '@vitejs/plugin-react': + specifier: ^4.5.2 + version: 4.5.2(vite@6.3.5(@types/node@24.0.3)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.0)(terser@5.43.0)(tsx@4.20.3)(yaml@2.8.0)) + eslint: + specifier: ^9.29.0 + version: 9.29.0(jiti@2.4.2) + eslint-plugin-react-hooks: + specifier: ^5.2.0 + version: 5.2.0(eslint@9.29.0(jiti@2.4.2)) + eslint-plugin-react-refresh: + specifier: ^0.4.20 + version: 0.4.20(eslint@9.29.0(jiti@2.4.2)) + typescript: + specifier: ^5.8.3 + version: 5.8.3 + typescript-eslint: + specifier: ^8.34.1 + version: 8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3) + vite: + specifier: ^6.3.5 + version: 6.3.5(@types/node@24.0.3)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.0)(terser@5.43.0)(tsx@4.20.3)(yaml@2.8.0) + examples/vite-react-dom-js-app: dependencies: react: From d533b0c20069b0cd5c1b5dd566055353c1b30a04 Mon Sep 17 00:00:00 2001 From: Rel1cx Date: Wed, 18 Jun 2025 15:02:40 +0800 Subject: [PATCH 76/78] fix: recover forward compatibility for missing rules --- .../vite-react-dom-app-v1/eslint.config.js | 6 +- .../eslint-plugin-react-x/src/plugin.ts | 8 + .../rules-removed/avoid-shorthand-boolean.ts | 4 + .../rules-removed/avoid-shorthand-fragment.ts | 4 + .../src/rules-removed/no-comment-textnodes.md | 76 +++ .../no-comment-textnodes.spec.ts | 60 ++ .../src/rules-removed/no-comment-textnodes.ts | 65 ++ .../no-complex-conditional-rendering.md | 0 .../no-complex-conditional-rendering.spec.ts | 557 ++++++++++++++++++ .../no-complex-conditional-rendering.ts | 61 ++ .../prefer-react-namespace-import.md | 54 ++ .../prefer-react-namespace-import.spec.ts | 132 +++++ .../prefer-react-namespace-import.ts | 77 +++ .../rules-removed/prefer-shorthand-boolean.ts | 4 + .../prefer-shorthand-fragment.ts | 4 + .../src/rules/no-useless-fragment.spec.ts | 384 ++++++++++++ .../src/rules/no-useless-fragment.ts | 222 +++++++ 17 files changed, 1715 insertions(+), 3 deletions(-) create mode 100644 packages/plugins/eslint-plugin-react-x/src/rules-removed/no-comment-textnodes.md create mode 100644 packages/plugins/eslint-plugin-react-x/src/rules-removed/no-comment-textnodes.spec.ts create mode 100644 packages/plugins/eslint-plugin-react-x/src/rules-removed/no-comment-textnodes.ts rename packages/plugins/eslint-plugin-react-x/src/{rules => rules-removed}/no-complex-conditional-rendering.md (100%) create mode 100644 packages/plugins/eslint-plugin-react-x/src/rules-removed/no-complex-conditional-rendering.spec.ts create mode 100644 packages/plugins/eslint-plugin-react-x/src/rules-removed/no-complex-conditional-rendering.ts create mode 100644 packages/plugins/eslint-plugin-react-x/src/rules-removed/prefer-react-namespace-import.md create mode 100644 packages/plugins/eslint-plugin-react-x/src/rules-removed/prefer-react-namespace-import.spec.ts create mode 100644 packages/plugins/eslint-plugin-react-x/src/rules-removed/prefer-react-namespace-import.ts create mode 100644 packages/plugins/eslint-plugin-react-x/src/rules/no-useless-fragment.spec.ts create mode 100644 packages/plugins/eslint-plugin-react-x/src/rules/no-useless-fragment.ts diff --git a/examples/vite-react-dom-app-v1/eslint.config.js b/examples/vite-react-dom-app-v1/eslint.config.js index 459ca8ef7c..212f3e4e27 100644 --- a/examples/vite-react-dom-app-v1/eslint.config.js +++ b/examples/vite-react-dom-app-v1/eslint.config.js @@ -55,6 +55,9 @@ export default tseslint.config( ], plugins: { "react-hooks": eslintPluginReactHooks, + }, + rules: { + ...eslintPluginReactHooks.configs.recommended.rules, // Place the v1 ruleset here to test the compatibility in the v2 branch "@eslint-react/avoid-shorthand-boolean": "warn", @@ -151,8 +154,5 @@ export default tseslint.config( "@eslint-react/naming-convention/filename-extension": "warn", "@eslint-react/naming-convention/use-state": "warn", }, - rules: { - ...eslintPluginReactHooks.configs.recommended.rules, - }, }, ); diff --git a/packages/plugins/eslint-plugin-react-x/src/plugin.ts b/packages/plugins/eslint-plugin-react-x/src/plugin.ts index 4f4afeb21d..29ceebec85 100644 --- a/packages/plugins/eslint-plugin-react-x/src/plugin.ts +++ b/packages/plugins/eslint-plugin-react-x/src/plugin.ts @@ -54,6 +54,7 @@ import noUnusedClassComponentMembers from "./rules/no-unused-class-component-mem import noUnusedState from "./rules/no-unused-state"; import noUseContext from "./rules/no-use-context"; import noUselessForwardRef from "./rules/no-useless-forward-ref"; +import noUselessFragment from "./rules/no-useless-fragment"; import preferDestructuringAssignment from "./rules/prefer-destructuring-assignment"; import preferNamespaceImport from "./rules/prefer-namespace-import"; import preferReadOnlyProps from "./rules/prefer-read-only-props"; @@ -64,6 +65,9 @@ import avoidShorthandBoolean from "./rules-removed/avoid-shorthand-boolean"; import avoidShorthandFragment from "./rules-removed/avoid-shorthand-fragment"; import preferShorthandBoolean from "./rules-removed/prefer-shorthand-boolean"; import preferShorthandFragment from "./rules-removed/prefer-shorthand-fragment"; +import preferReactNamespaceImport from "./rules-removed/prefer-react-namespace-import"; +import noCommentTextnodes from "./rules-removed/no-comment-textnodes"; +import noComplexConditionalRendering from "./rules-removed/no-complex-conditional-rendering"; /* eslint-enable perfectionist/sort-imports */ export const plugin = { @@ -126,6 +130,7 @@ export const plugin = { "no-unused-state": noUnusedState, "no-use-context": noUseContext, "no-useless-forward-ref": noUselessForwardRef, + "no-useless-fragment": noUselessFragment, "prefer-destructuring-assignment": preferDestructuringAssignment, "prefer-namespace-import": preferNamespaceImport, "prefer-read-only-props": preferReadOnlyProps, @@ -134,6 +139,9 @@ export const plugin = { // Removed rules "avoid-shorthand-boolean": avoidShorthandBoolean, "avoid-shorthand-fragment": avoidShorthandFragment, + "no-comment-textnodes": noCommentTextnodes, + "no-complex-conditional-rendering": noComplexConditionalRendering, + "prefer-react-namespace-import": preferReactNamespaceImport, "prefer-shorthand-boolean": preferShorthandBoolean, "prefer-shorthand-fragment": preferShorthandFragment, }, diff --git a/packages/plugins/eslint-plugin-react-x/src/rules-removed/avoid-shorthand-boolean.ts b/packages/plugins/eslint-plugin-react-x/src/rules-removed/avoid-shorthand-boolean.ts index 15bd7f4685..20412c1915 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules-removed/avoid-shorthand-boolean.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules-removed/avoid-shorthand-boolean.ts @@ -14,6 +14,7 @@ export type MessageID = CamelCase; export default createRule<[], MessageID>({ meta: { type: "problem", + deprecated: true, docs: { description: "Enforces explicit boolean values for boolean attributes.", [Symbol.for("rule_features")]: RULE_FEATURES, @@ -23,6 +24,9 @@ export default createRule<[], MessageID>({ avoidShorthandBoolean: "Avoid using shorthand boolean attribute '{{propName}}'. Use '{{propName}}={true}' instead.", }, + replacedBy: [ + "react-x/jsx-shorthand-boolean", + ], schema: [], }, name: RULE_NAME, diff --git a/packages/plugins/eslint-plugin-react-x/src/rules-removed/avoid-shorthand-fragment.ts b/packages/plugins/eslint-plugin-react-x/src/rules-removed/avoid-shorthand-fragment.ts index a18a72a05d..1f82d73926 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules-removed/avoid-shorthand-fragment.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules-removed/avoid-shorthand-fragment.ts @@ -13,6 +13,7 @@ export type MessageID = CamelCase; export default createRule<[], MessageID>({ meta: { type: "problem", + deprecated: true, docs: { description: "Enforces explicit `` components instead of the shorthand `<>` or `` syntax.", [Symbol.for("rule_features")]: RULE_FEATURES, @@ -20,6 +21,9 @@ export default createRule<[], MessageID>({ messages: { avoidShorthandFragment: "Avoid using shorthand fragment syntax. Use '{{jsxFragmentFactory}}' component instead.", }, + replacedBy: [ + "react-x/jsx-shorthand-fragment", + ], schema: [], }, name: RULE_NAME, diff --git a/packages/plugins/eslint-plugin-react-x/src/rules-removed/no-comment-textnodes.md b/packages/plugins/eslint-plugin-react-x/src/rules-removed/no-comment-textnodes.md new file mode 100644 index 0000000000..6bfcc9cf6b --- /dev/null +++ b/packages/plugins/eslint-plugin-react-x/src/rules-removed/no-comment-textnodes.md @@ -0,0 +1,76 @@ +--- +title: no-comment-textnodes +--- + +**Full Name in `eslint-plugin-react-x`** + +```sh copy +react-x/no-comment-textnodes +``` + +**Full Name in `@eslint-react/eslint-plugin`** + +```sh copy +@eslint-react/no-comment-textnodes +``` + +**Presets** + +- `x` +- `recommended` +- `recommended-typescript` +- `recommended-type-checked` + +## Description + +Prevents comment strings (e.g. beginning with `//` or `/*`) from being accidentally inserted into the JSX element's textnodes. + +This could be a mistake during code editing or it could be a misunderstanding of how JSX works. Either way, it's probably not what you intended. + +## Examples + +### Failing + +```tsx +import React from "react"; + +function MyComponent1() { + return
// empty div
; + // ^^^^^^^^^^^^ + // - Possible misused comment in text node. Comments inside children section of tag should be placed inside braces. +} + +function MyComponent2() { + return
/* empty div */
; + // ^^^^^^^^^^^^^^^ + // - Possible misused comment in text node. Comments inside children section of tag should be placed inside braces. +} +``` + +### Passing + +```tsx +import React from "react"; + +function MyComponent() { + return
{/* empty div */}
; +} +``` + +### Legitimate uses + +It's possible you may want to legitimately output comment start characters (`//` or `/*`) in a JSX text node. In which case, you can do the following: + +```tsx +import React from "react"; + +function MyComponent() { + // 🟢 Good: This is a legitimate use of comment strings in JSX textnodes + return
{"/* This will be output as a text node */"}
; +} +``` + +## Implementation + +- [Rule source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/no-comment-textnodes.ts) +- [Test source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/no-comment-textnodes.spec.ts) diff --git a/packages/plugins/eslint-plugin-react-x/src/rules-removed/no-comment-textnodes.spec.ts b/packages/plugins/eslint-plugin-react-x/src/rules-removed/no-comment-textnodes.spec.ts new file mode 100644 index 0000000000..a9929089a2 --- /dev/null +++ b/packages/plugins/eslint-plugin-react-x/src/rules-removed/no-comment-textnodes.spec.ts @@ -0,0 +1,60 @@ +import tsx from "dedent"; + +import { allValid, ruleTester } from "../../../../../test"; +import rule, { RULE_NAME } from "./no-comment-textnodes"; + +ruleTester.run(RULE_NAME, rule, { + invalid: [ + { + code: tsx`
// invalid
`, + errors: [{ messageId: "noCommentTextnodes" }], + }, + { + code: tsx`<>// invalid`, + errors: [{ messageId: "noCommentTextnodes" }], + }, + { + code: tsx`
/* invalid */
`, + errors: [{ messageId: "noCommentTextnodes" }], + }, + { + code: tsx` +
+ // invalid +
+ `, + errors: [{ messageId: "noCommentTextnodes" }], + }, + { + code: tsx` +
+ abcdef + /* invalid */ + foo +
+ `, + errors: [{ messageId: "noCommentTextnodes" }], + }, + { + code: tsx` +
+ {'abcdef'} + // invalid + {'foo'} +
+ `, + errors: [{ messageId: "noCommentTextnodes" }], + }, + { + code: "/*", + errors: [{ messageId: "noCommentTextnodes" }], + }, + ], + valid: [ + ...allValid, + "{/* valid */}", + " https://www.eslint-react.xyz/attachment/download/1", + "", + "", + ], +}); diff --git a/packages/plugins/eslint-plugin-react-x/src/rules-removed/no-comment-textnodes.ts b/packages/plugins/eslint-plugin-react-x/src/rules-removed/no-comment-textnodes.ts new file mode 100644 index 0000000000..a990375c23 --- /dev/null +++ b/packages/plugins/eslint-plugin-react-x/src/rules-removed/no-comment-textnodes.ts @@ -0,0 +1,65 @@ +import type { RuleContext, RuleFeature } from "@eslint-react/kit"; +import type { TSESTree } from "@typescript-eslint/types"; +import type { RuleListener } from "@typescript-eslint/utils/ts-eslint"; +import type { CamelCase } from "string-ts"; +import * as AST from "@eslint-react/ast"; +import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; + +import { createRule } from "../utils"; + +export const RULE_NAME = "no-comment-textnodes"; + +export const RULE_FEATURES = [] as const satisfies RuleFeature[]; + +export type MessageID = CamelCase; + +export default createRule<[], MessageID>({ + meta: { + type: "problem", + deprecated: true, + docs: { + description: "Prevents comments from being inserted as text nodes.", + [Symbol.for("rule_features")]: RULE_FEATURES, + }, + messages: { + noCommentTextnodes: + "Possible misused comment in text node. Comments inside children section of tag should be placed inside braces.", + }, + replacedBy: [ + "react-x/jsx-no-comment-textnodes", + ], + schema: [], + }, + name: RULE_NAME, + create, + defaultOptions: [], +}); + +export function create(context: RuleContext): RuleListener { + function hasCommentLike(node: TSESTree.JSXText | TSESTree.Literal) { + if (AST.isOneOf([T.JSXAttribute, T.JSXExpressionContainer])(node.parent)) { + return false; + } + const rawValue = context.sourceCode.getText(node); + return /^\s*\/(?:\/|\*)/mu.test(rawValue); + } + const visitorFunction = (node: TSESTree.JSXText | TSESTree.Literal): void => { + if (!AST.isOneOf([T.JSXElement, T.JSXFragment])(node.parent)) { + return; + } + if (!hasCommentLike(node)) { + return; + } + if (!node.parent.type.includes("JSX")) { + return; + } + context.report({ + messageId: "noCommentTextnodes", + node, + }); + }; + return { + JSXText: visitorFunction, + Literal: visitorFunction, + }; +} diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-complex-conditional-rendering.md b/packages/plugins/eslint-plugin-react-x/src/rules-removed/no-complex-conditional-rendering.md similarity index 100% rename from packages/plugins/eslint-plugin-react-x/src/rules/no-complex-conditional-rendering.md rename to packages/plugins/eslint-plugin-react-x/src/rules-removed/no-complex-conditional-rendering.md diff --git a/packages/plugins/eslint-plugin-react-x/src/rules-removed/no-complex-conditional-rendering.spec.ts b/packages/plugins/eslint-plugin-react-x/src/rules-removed/no-complex-conditional-rendering.spec.ts new file mode 100644 index 0000000000..2efa5aafbe --- /dev/null +++ b/packages/plugins/eslint-plugin-react-x/src/rules-removed/no-complex-conditional-rendering.spec.ts @@ -0,0 +1,557 @@ +import tsx from "dedent"; + +import { allValid, ruleTester } from "../../../../../test"; +import rule, { RULE_NAME } from "./no-complex-conditional-rendering"; + +ruleTester.run(RULE_NAME, rule, { + invalid: [ + { + code: tsx` + function Component({ hideShapes, debugSvg }) { + return
{hideShapes ? null : debugSvg ? : }
; + } + `, + errors: [ + { + messageId: "noComplexConditionalRendering", + }, + ], + }, + { + code: tsx` + type AppProps = { + items: string[]; + count: number; + } + + const App = ({ items, count }: AppProps) => { + return
{direction ? (direction === "down" ? "▼" : "▲") : ""}
+ } + `, + errors: [ + { + messageId: "noComplexConditionalRendering", + }, + ], + }, + { + code: tsx` + const someCondition = 0; + const SomeComponent = () =>
; + + const App = () => { + return ( + <> + {!!someCondition + ? ( + ) + : someCondition ? null :
+ } + + ) + } + `, + errors: [ + { + messageId: "noComplexConditionalRendering", + }, + ], + }, + { + code: tsx` + const someCondition = 0; + const SomeComponent = () =>
; + + const App = () => { + return ( + <> + {!!someCondition + ? ( + ) + : someCondition &&
+ } + + ) + } + `, + errors: [ + { + messageId: "noComplexConditionalRendering", + }, + ], + }, + { + code: tsx` + const someCondition = 0; + const SomeComponent = () =>
; + + const App = () => { + return ( + <> + {!!someCondition + ? ( + ) + : someCondition ? someCondition :
+ } + + ) + } + `, + errors: [ + { + messageId: "noComplexConditionalRendering", + }, + ], + }, + { + code: tsx` + const someCondition = 0; + const SomeComponent = () =>
; + + const App = () => { + return ( + <> + {!!someCondition + ? ( + ) + : someCondition ? "aaa" + : someCondition && someCondition + ?
+ : null + } + + ) + } + `, + errors: [ + { + messageId: "noComplexConditionalRendering", + }, + ], + }, + { + code: tsx` + const App = () => { + return ( + <> + {0 && 1 || } + {NaN || 0 && } + + ) + } + `, + errors: [ + { + messageId: "noComplexConditionalRendering", + }, + { + messageId: "noComplexConditionalRendering", + }, + ], + }, + { + code: tsx` + const App = () => { + return ( + <> + {0 && 1 && 2 || } + {NaN || 1 || 0 && } + + ) + } + `, + errors: [ + { + messageId: "noComplexConditionalRendering", + }, + { + messageId: "noComplexConditionalRendering", + }, + ], + }, + ], + valid: [ + ...allValid, + tsx` + function Component({ hideShapes, debugSvg }) { + // Early return if_to render + if (hideShapes) { + return null; + } + + return debugSvg ? : ; + } + `, + tsx` + const foo = Math.random() > 0.5; + const bar = "bar"; + + const App = () => { + return
{foo || bar}
+ } + `, + tsx` + type AppProps = { + foo: string; + } + + const App = ({ foo }: AppProps) => { + return
{foo}
+ } + `, + tsx` + type AppProps = { + items: string[]; + } + + const App = ({ items }: AppProps) => { + return
There are {items.length} elements
+ } + `, + tsx` + type AppProps = { + items: string[]; + count: number; + } + + const App = ({ items, count }: AppProps) => { + return
{!count && 'No results found'}
+ } + `, + tsx` + type ListProps = { + items: string[]; + } + + const List = ({ items }: ListProps) => { + return
{items.map(item =>
{item}
)}
+ } + + type AppProps = { + items: string[]; + } + + const App = ({ items }: AppProps) => { + return
{!!items.length && }
+ } + `, + tsx` + type ListProps = { + items: string[]; + } + + const List = ({ items }: ListProps) => { + return
{items.map(item =>
{item}
)}
+ } + + type AppProps = { + items: string[]; + } + + const App = ({ items }: AppProps) => { + return
{Boolean(items.length) && }
+ } + `, + tsx` + type ListProps = { + items: string[]; + } + + const List = ({ items }: ListProps) => { + return
{items.map(item =>
{item}
)}
+ } + + type AppProps = { + items: string[]; + } + + const App = ({ items }: AppProps) => { + return
{items.length > 0 && }
+ } + `, + tsx` + type ListProps = { + items: string[]; + } + + const List = ({ items }: ListProps) => { + return
{items.map(item =>
{item}
)}
+ } + + type AppProps = { + items: string[]; + } + + const App = ({ items }: AppProps) => { + return
{items.length ? : null}
+ } + `, + tsx` + type ListProps = { + items: string[]; + } + + const List = ({ items }: ListProps) => { + return
{items.map(item =>
{item}
)}
+ } + + type AppProps = { + items: string[]; + count: number; + } + + const App = ({ items, count }: AppProps) => { + return
{count ? : null}
+ } + `, + tsx` + type ListProps = { + items: string[]; + } + + const List = ({ items }: ListProps) => { + return
{items.map(item =>
{item}
)}
+ } + + type AppProps = { + items: string[]; + count: number; + } + + const App = ({ items, count }: AppProps) => { + return
{!!count && }
+ } + `, + tsx` + const App = () => { + return ( + <> + {0 ? : null} + {'' && } + {NaN ? : null} + + ) + } + `, + tsx` + const foo = Math.random() > 0.5; + const bar = 0; + function App() { + return ( + ;`, + }, + { + messageId: "addButtonType", + data: { type: "submit" }, + output: tsx`;`, + }, + { + messageId: "addButtonType", + data: { type: "reset" }, + output: tsx`;`, + }, + ], }, ], }, @@ -18,6 +35,23 @@ ruleTester.run(RULE_NAME, rule, { errors: [ { messageId: "noMissingButtonType", + suggestions: [ + { + messageId: "addButtonType", + data: { type: "button" }, + output: tsx`Click me;`, + }, + { + messageId: "addButtonType", + data: { type: "submit" }, + output: tsx`Click me;`, + }, + { + messageId: "addButtonType", + data: { type: "reset" }, + output: tsx`Click me;`, + }, + ], }, ], settings: { diff --git a/packages/plugins/eslint-plugin-react-dom/src/rules/no-missing-button-type.ts b/packages/plugins/eslint-plugin-react-dom/src/rules/no-missing-button-type.ts index 3aa7e959d6..c7fd983f7f 100644 --- a/packages/plugins/eslint-plugin-react-dom/src/rules/no-missing-button-type.ts +++ b/packages/plugins/eslint-plugin-react-dom/src/rules/no-missing-button-type.ts @@ -1,5 +1,5 @@ -import type { RuleContext, RuleFeature } from "@eslint-react/kit"; -import type { RuleListener } from "@typescript-eslint/utils/ts-eslint"; +import type { RuleContext, RuleFeature, RuleSuggest } from "@eslint-react/kit"; +import type { RuleFixer, RuleListener } from "@typescript-eslint/utils/ts-eslint"; import type { CamelCase } from "string-ts"; import * as ER from "@eslint-react/core"; import { createJsxElementResolver, createRule, findCustomComponentProp } from "../utils"; @@ -8,7 +8,11 @@ export const RULE_NAME = "no-missing-button-type"; export const RULE_FEATURES = [] as const satisfies RuleFeature[]; -export type MessageID = CamelCase; +export const BUTTON_TYPES = ["button", "submit", "reset"] as const; + +export type MessageID = + | CamelCase + | "addButtonType"; export default createRule<[], MessageID>({ meta: { @@ -17,7 +21,9 @@ export default createRule<[], MessageID>({ description: "Enforces explicit `type` attribute for `button` elements.", [Symbol.for("rule_features")]: RULE_FEATURES, }, + hasSuggestions: true, messages: { + addButtonType: "Add 'type' attribute with value '{{type}}'.", noMissingButtonType: "Add missing 'type' attribute on 'button' component.", }, schema: [], @@ -51,6 +57,9 @@ export function create(context: RuleContext): RuleListener { context.report({ messageId: "noMissingButtonType", node: attributeNode, + suggest: getSuggest((type) => (fixer: RuleFixer) => { + return fixer.replaceText(node, `${propNameOnJsx}="${type}"`); + }), }); } return; @@ -59,8 +68,21 @@ export function create(context: RuleContext): RuleListener { context.report({ messageId: "noMissingButtonType", node, + suggest: getSuggest((type) => (fixer: RuleFixer) => { + const lastToken = context.sourceCode.getLastToken(node.openingElement); + if (lastToken == null) return null; + return fixer.insertTextBefore(lastToken, ` type="${type}"`); + }), }); } }, }; } + +function getSuggest(getFix: (type: string) => RuleSuggest["fix"]): RuleSuggest[] { + return BUTTON_TYPES.map((type) => ({ + messageId: "addButtonType", + data: { type }, + fix: getFix(type), + })); +} diff --git a/packages/utilities/kit/src/types.ts b/packages/utilities/kit/src/types.ts index 9fa8cd6120..a5c584edad 100644 --- a/packages/utilities/kit/src/types.ts +++ b/packages/utilities/kit/src/types.ts @@ -44,3 +44,9 @@ export type RuleFeature = | "EXP"; // Experimental export type RulePolicy = number; + +export type RuleSuggest = { + messageId: MessageIds; + data?: Record; + fix: tseslint.ReportFixFunction; +};