diff --git a/.adrs/0000-choice-between-useless-and-unnecessary-in-rule-naming.md b/.adrs/0000-choice-between-useless-and-unnecessary-in-rule-naming.md new file mode 100644 index 0000000000..05cb3930e1 --- /dev/null +++ b/.adrs/0000-choice-between-useless-and-unnecessary-in-rule-naming.md @@ -0,0 +1,38 @@ +# ADR 0000: Choice between "useless" and "unnecessary" in rule naming + +## Status + +Accepted + +## Context + +When naming rules related to identifying redundant or non-functional code (e.g., in linting or static analysis), the terms "useless" and "unnecessary" both convey a sense of superfluity. However, their nuanced semantic differences could lead to ambiguity if misapplied. For example, a `forwardRef`-wrapped component that is never passed a `ref` is entirely without purpose, but other scenarios might involve optional code that is only redundant in specific contexts. A consistent naming convention is needed to ensure clarity and accuracy in rule documentation and error messaging. + +## Decision + +Use "useless" to describe code that has no functional purpose under any circumstances (e.g., a `forwardRef` with no `ref` usage). +Use "unnecessary" to describe code that is contextually redundant but not strictly non-functional (e.g., an `useEffect` that runs event-specific logic). + +## Consequences + +- Improved clarity: Developers can better discern whether code is strictly non-functional ("useless") or situationally redundant ("unnecessary"). + +- Consistency: Rules and error messages will align with precise semantic definitions. + +- Potential learning curve: Teams may need documentation to understand the distinction initially. + +## Alternatives Considered + +1. Using only "unnecessary" for all cases: + - Rejected because it conflates fundamentally distinct scenarios (strictly non-functional vs. contextually redundant), reducing diagnostic precision. + +2. Using only "useless" for all cases: + - Rejected because it could mislabel code that is optional but valid in other contexts, leading to confusion or dismissal of valid feedback. + +## Related ADRs + +N/A + +## Links + +N/A diff --git a/.adrs/0001-rename-ensure-forward-ref-using-ref-to-no-useless-forward-ref.md b/.adrs/0001-rename-ensure-forward-ref-using-ref-to-no-useless-forward-ref.md new file mode 100644 index 0000000000..8216de07c7 --- /dev/null +++ b/.adrs/0001-rename-ensure-forward-ref-using-ref-to-no-useless-forward-ref.md @@ -0,0 +1,36 @@ +# ADR 0001: Rename ensure-forward-ref-using-ref to no-useless-forward-ref + +## Status + +Accepted + +## Context + +The rule `ensure-forward-ref-using-ref` detects React `forwardRef`-wrapped components that never receive a `ref` prop. This matches the "useless" criteria defined in ADR 0000, as such components serve no functional purpose. The current name lacks alignment with our established `no--` naming convention and semantic categorization. + +## Decision + +Rename the rule to **`no-useless-forward-ref`** to: + +1. Adhere to the `no--` pattern used in similar rules (e.g., `no-useless-state`). +2. Reflect the strict "useless" classification per ADR 0000, since unreferenced `forwardRef` components are functionally inert. + +## Consequences + +- **Consistency**: Aligns with existing rule taxonomy and terminology. +- **Clarity**: Clearly signals the rule's focus on non-functional code. +- **Documentation updates**: Requires migration guides and rule metadata changes. + +## Alternatives Considered + +Using `no-unnecessary-forward-ref`: + +- Rejected because "unnecessary" implies optional redundancy, whereas unreferenced `forwardRef` has zero runtime utility. + +## Related ADRs + +- [ADR 0000: Choice between "useless" and "unnecessary" in rule naming](./0000-choice-between-useless-and-unnecessary-in-rule-naming.md) + +## Links + +N/A diff --git a/.vscode/settings.json b/.vscode/settings.json index 3e71dc9745..4da9cf51d7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,7 +3,7 @@ "editor.defaultFormatter": "dprint.dprint" }, "[jsonc]": { - "editor.defaultFormatter": "dprint.dprint" + "editor.defaultFormatter": "vscode.json-language-features" }, "[typescript]": { "editor.defaultFormatter": "dprint.dprint" diff --git a/CHANGELOG.md b/CHANGELOG.md index 27e4886bd1..1036a27903 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## v1.33.0 (Draft) + +### 🪄 Improvements + +- refactor(plugins/x): rename `ensure-forward-ref-using-ref` to `no-useless-forward-ref` + +### 📝 Changes you should be aware of + +The following rules have been renamed: + ## v1.32.1 (2025-03-13) ### 🐞 Fixes diff --git a/apps/website/content/docs/changelog.md b/apps/website/content/docs/changelog.md index d491288450..f56f3da080 100644 --- a/apps/website/content/docs/changelog.md +++ b/apps/website/content/docs/changelog.md @@ -2,6 +2,16 @@ title: Changelog --- +## v1.33.0 (Draft) + +### 🪄 Improvements + +- refactor(plugins/x): rename `ensure-forward-ref-using-ref` to `no-useless-forward-ref` + +### 📝 Changes you should be aware of + +The following rules have been renamed: + ## v1.32.1 (2025-03-13) ### 🐞 Fixes diff --git a/apps/website/content/docs/deprecated.md b/apps/website/content/docs/deprecated.md index 14f27aa789..32c119f26e 100644 --- a/apps/website/content/docs/deprecated.md +++ b/apps/website/content/docs/deprecated.md @@ -8,14 +8,15 @@ full: true | Rule | Replaced by | Deprecated in | | :--------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------- | :------------ | -| [`jsx-uses-vars`](/docs/rules/jsx-uses-vars) | [`use-jsx-vars`](/docs/rules/use-jsx-vars) | 1.22.0 | | [`jsx-no-duplicate-props`](/docs/rules/jsx-no-duplicate-props) | [`no-duplicate-jsx-props`](/docs/rules/no-duplicate-jsx-props) | 1.22.0 | -| [`no-complicated-conditional-rendering`](/docs/rules/no-complicated-conditional-rendering) | [`no-complex-conditional-rendering`](/docs/rules/no-complex-conditional-rendering) | 1.6.0 | -| [`no-children-in-void-dom-elements`](/docs/rules/dom-no-children-in-void-dom-elements) | [`no-void-elements-with-children`](/docs/rules/dom-no-void-elements-with-children) | 1.22.0 | -| [`no-redundant-custom-hook`](/docs/rules/hooks-extra-no-useless-custom-hooks) | [`no-useless-custom-hooks`](/docs/rules/hooks-extra-no-useless-custom-hooks) | 1.21.0 | +| [`jsx-uses-vars`](/docs/rules/jsx-uses-vars) | [`use-jsx-vars`](/docs/rules/use-jsx-vars) | 1.22.0 | | [`ensure-custom-hooks-using-other-hooks`](/docs/rules/hooks-extra-no-useless-custom-hooks) | [`no-useless-custom-hooks`](/docs/rules/hooks-extra-no-useless-custom-hooks) | 1.13.0 | -| [`ensure-use-memo-has-non-empty-deps`](/docs/rules/hooks-extra-ensure-use-memo-has-non-empty-deps) | [`no-unnecessary-use-memo`](/docs/rules/hooks-extra-no-unnecessary-use-memo) | 1.13.0 | +| [`ensure-forward-ref-using-ref`](/docs/rules/ensure-forward-ref-using-ref) | [`no-useless-forward-ref`](/docs/rules/no-useless-forward-ref) | 1.33.0 | | [`ensure-use-callback-has-non-empty-deps`](/docs/rules/hooks-extra-ensure-use-callback-has-non-empty-deps) | [`no-unnecessary-use-callback`](/docs/rules/hooks-extra-no-unnecessary-use-callback) | 1.13.0 | +| [`ensure-use-memo-has-non-empty-deps`](/docs/rules/hooks-extra-ensure-use-memo-has-non-empty-deps) | [`no-unnecessary-use-memo`](/docs/rules/hooks-extra-no-unnecessary-use-memo) | 1.13.0 | +| [`no-children-in-void-dom-elements`](/docs/rules/dom-no-children-in-void-dom-elements) | [`no-void-elements-with-children`](/docs/rules/dom-no-void-elements-with-children) | 1.22.0 | +| [`no-complicated-conditional-rendering`](/docs/rules/no-complicated-conditional-rendering) | [`no-complex-conditional-rendering`](/docs/rules/no-complex-conditional-rendering) | 1.6.0 | +| [`no-redundant-custom-hook`](/docs/rules/hooks-extra-no-useless-custom-hooks) | [`no-useless-custom-hooks`](/docs/rules/hooks-extra-no-useless-custom-hooks) | 1.21.0 | ## Presets diff --git a/apps/website/content/docs/roadmap.md b/apps/website/content/docs/roadmap.md index 01dffe5d5e..3f274ec9ad 100644 --- a/apps/website/content/docs/roadmap.md +++ b/apps/website/content/docs/roadmap.md @@ -32,7 +32,7 @@ title: Roadmap ### Add suggestion-fix feature to rules that can be fixed interactively -- [ ] `ensure-forward-ref-using-ref` +- [ ] `no-useless-forward-ref` - [ ] `no-leaked-conditional-rendering` - [ ] `no-redundant-should-component-update` - [ ] `no-unused-class-component-members` diff --git a/apps/website/content/docs/rules/meta.json b/apps/website/content/docs/rules/meta.json index db32b8301b..0b4246c218 100644 --- a/apps/website/content/docs/rules/meta.json +++ b/apps/website/content/docs/rules/meta.json @@ -4,7 +4,7 @@ "---Core Rules---", "avoid-shorthand-boolean", "avoid-shorthand-fragment", - "ensure-forward-ref-using-ref", + "no-useless-forward-ref", "no-access-state-in-setstate", "no-array-index-key", "no-children-count", diff --git a/apps/website/content/docs/rules/overview.md b/apps/website/content/docs/rules/overview.md index 7fd0dea4c4..c76ef36d54 100644 --- a/apps/website/content/docs/rules/overview.md +++ b/apps/website/content/docs/rules/overview.md @@ -20,7 +20,7 @@ full: true | :----------------------------------------------------------------------------------- | :- | :------------ | :---------------------------------------------------------------------------------------------------- | :------: | | [`avoid-shorthand-boolean`](./avoid-shorthand-boolean) | 0️⃣ | `🔍` `🔧` | Enforces the use of explicit boolean values for boolean attributes. | | | [`avoid-shorthand-fragment`](./avoid-shorthand-fragment) | 0️⃣ | `🔍` | Enforces the use of explicit `` components instead of the shorthand `<>` or `` syntax. | | -| [`ensure-forward-ref-using-ref`](./ensure-forward-ref-using-ref) | 1️⃣ | `🔍` | Requires that components wrapped with `forwardRef` must have a `ref` parameter. | | +| [`no-useless-forward-ref`](./no-useless-forward-ref) | 1️⃣ | `🔍` | Requires that components wrapped with `forwardRef` must have a `ref` parameter. | | | [`no-access-state-in-setstate`](./no-access-state-in-setstate) | 2️⃣ | `🔍` | Prevents accessing `this.state` inside `setState` calls. | | | [`no-array-index-key`](./no-array-index-key) | 1️⃣ | `🔍` | Prevents using an item's index in the array as its key | | | [`no-children-count`](./no-children-count) | 1️⃣ | `🔍` | Prevents using `Children.count`. | | diff --git a/apps/website/next.config.mjs b/apps/website/next.config.mjs index cc91609364..04f78f8773 100644 --- a/apps/website/next.config.mjs +++ b/apps/website/next.config.mjs @@ -89,6 +89,11 @@ const config = { destination: "/docs/rules/no-complex-conditional-rendering", permanent: true, }, + { + source: "/docs/rules/ensure-forward-ref-using-ref", + destination: "/docs/rules/no-useless-forward-ref", + permanent: true, + }, { source: "/docs/rules/dom-no-children-in-void-dom-elements", destination: "/docs/rules/dom-no-void-elements-with-children", @@ -114,11 +119,6 @@ const config = { destination: "/docs/rules/hooks-extra-no-useless-custom-hooks", permanent: true, }, - { - source: "/docs/rules/debug-react-hooks", - destination: "/docs/rules/debug-hook", - permanent: true, - }, ]; }, }; diff --git a/apps/website/package.json b/apps/website/package.json index 6dbe737135..86dced384d 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -35,7 +35,7 @@ "@local/configs": "workspace:*", "@mdx-js/mdx": "^3.1.0", "@next/eslint-plugin-next": "^15.2.2", - "@tailwindcss/postcss": "^4.0.13", + "@tailwindcss/postcss": "^4.0.14", "@tsconfig/next": "^2.0.3", "@tsconfig/node22": "^22.0.0", "@tsconfig/strictest": "^2.0.5", @@ -58,7 +58,7 @@ "eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-unicorn": "^57.0.0", "postcss": "^8.5.3", - "tailwindcss": "^4.0.13", + "tailwindcss": "^4.0.14", "tailwindcss-animated": "^2.0.0", "typescript": "^5.8.2", "typescript-eslint": "^8.26.1" 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 0ea5303d97..c94ae3535e 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,6 @@ import { DEFAULT_ESLINT_REACT_SETTINGS } from "@eslint-react/shared"; export const name = "react-x/recommended"; export const rules = { - "react-x/ensure-forward-ref-using-ref": "warn", "react-x/no-access-state-in-setstate": "error", "react-x/no-array-index-key": "warn", "react-x/no-children-count": "warn", @@ -41,6 +40,7 @@ export const rules = { "react-x/no-unused-class-component-members": "warn", "react-x/no-unused-state": "warn", "react-x/no-use-context": "warn", + "react-x/no-useless-forward-ref": "warn", "react-x/use-jsx-vars": "warn", } as const satisfies RulePreset; diff --git a/packages/plugins/eslint-plugin-react-x/src/plugin.ts b/packages/plugins/eslint-plugin-react-x/src/plugin.ts index 713ea63663..291cff6565 100644 --- a/packages/plugins/eslint-plugin-react-x/src/plugin.ts +++ b/packages/plugins/eslint-plugin-react-x/src/plugin.ts @@ -1,7 +1,6 @@ import { name, version } from "../package.json"; import avoidShorthandBoolean from "./rules/avoid-shorthand-boolean"; import avoidShorthandFragment from "./rules/avoid-shorthand-fragment"; -import forwardRefUsingRef from "./rules/ensure-forward-ref-using-ref"; import noAccessStateInSetstate from "./rules/no-access-state-in-setstate"; import noArrayIndexKey from "./rules/no-array-index-key"; import noChildrenCount from "./rules/no-children-count"; @@ -44,6 +43,7 @@ import noUnstableDefaultProps from "./rules/no-unstable-default-props"; import noUnusedClassComponentMembers from "./rules/no-unused-class-component-members"; 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 preferReactNamespaceImport from "./rules/prefer-react-namespace-import"; @@ -60,7 +60,6 @@ export const plugin = { rules: { "avoid-shorthand-boolean": avoidShorthandBoolean, "avoid-shorthand-fragment": avoidShorthandFragment, - "ensure-forward-ref-using-ref": forwardRefUsingRef, "no-access-state-in-setstate": noAccessStateInSetstate, "no-array-index-key": noArrayIndexKey, "no-children-count": noChildrenCount, @@ -103,6 +102,7 @@ export const plugin = { "no-unused-class-component-members": noUnusedClassComponentMembers, "no-unused-state": noUnusedState, "no-use-context": noUseContext, + "no-useless-forward-ref": noUselessForwardRef, "no-useless-fragment": noUselessFragment, "prefer-destructuring-assignment": preferDestructuringAssignment, "prefer-react-namespace-import": preferReactNamespaceImport, @@ -112,6 +112,8 @@ export const plugin = { "use-jsx-vars": useJsxVars, // Part: deprecated rules + /** @deprecated Use `no-useless-forward-ref` instead */ + "ensure-forward-ref-using-ref": noUselessForwardRef, /** @deprecated Use `no-duplicate-jsx-props` instead */ "jsx-no-duplicate-props": noDuplicateJsxProps, /** @deprecated Use `use-jsx-vars` instead */ diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-forward-ref.md b/packages/plugins/eslint-plugin-react-x/src/rules/no-forward-ref.md index 5eb88575a2..1333538b41 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-forward-ref.md +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-forward-ref.md @@ -98,6 +98,8 @@ function MyInput({ ref, value, onChange }: MyInputProps & { ref: React.RefObject ## See Also +- [`no-useless-forward-ref`](./no-useless-forward-ref)\ + Enforces that `forwardRef` is only used when a `ref` parameter is declared. - [`no-context-provider`](./no-context-provider)\ Replaces usages of `` with ``. - [`no-use-context`](./no-use-context)\ diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/ensure-forward-ref-using-ref.md b/packages/plugins/eslint-plugin-react-x/src/rules/no-useless-forward-ref.md similarity index 54% rename from packages/plugins/eslint-plugin-react-x/src/rules/ensure-forward-ref-using-ref.md rename to packages/plugins/eslint-plugin-react-x/src/rules/no-useless-forward-ref.md index 5444a4cbfc..f268ee411d 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/ensure-forward-ref-using-ref.md +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-useless-forward-ref.md @@ -1,17 +1,17 @@ --- -title: ensure-forward-ref-using-ref +title: no-useless-forward-ref --- **Full Name in `eslint-plugin-react-x`** ```plain copy -react-x/ensure-forward-ref-using-ref +react-x/no-useless-forward-ref ``` **Full Name in `@eslint-react/eslint-plugin`** ```plain copy -@eslint-react/ensure-forward-ref-using-ref +@eslint-react/no-useless-forward-ref ``` **Features** @@ -27,11 +27,12 @@ react-x/ensure-forward-ref-using-ref ## What it does -Requires that components wrapped with `forwardRef` must have a `ref` parameter. +Enforces that `forwardRef` is only used when a `ref` parameter is declared. -This rule checks all React components using `forwardRef` and verifies that there is a second parameter. +This rule enforces that: -Omitting the `ref` argument is usually a bug, and components not using `ref` don't need to be wrapped by `forwardRef`. +1. Components using `forwardRef` must declare a `ref` parameter +2. Components not using `ref` should not be wrapped with `forwardRef` ## Examples @@ -42,7 +43,7 @@ import React from "react"; const MyComponent = React.forwardRef((props) => { // ^^^^^ - // - 'forwardRef' is used with this component but no 'ref' parameter is set. + // - 'forwardRef' wrapper is useless without 'ref' parameter. return