diff --git a/apps/website/content/docs/migration.mdx b/apps/website/content/docs/migration.mdx index 0f8811000d..26b301075c 100644 --- a/apps/website/content/docs/migration.mdx +++ b/apps/website/content/docs/migration.mdx @@ -125,7 +125,7 @@ The following table compares all rules from `eslint-plugin-react` with their ESL | [`no-unsafe`](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/no-unsafe.md) | [`no-unsafe-component-will-mount`](/docs/rules/no-unsafe-component-will-mount) + [`no-unsafe-component-will-receive-props`](/docs/rules/no-unsafe-component-will-receive-props) + [`no-unsafe-component-will-update`](/docs/rules/no-unsafe-component-will-update) | ✅ | 🟡 | | [`no-unstable-nested-components`](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/no-unstable-nested-components.md) | [`no-nested-component-definitions`](/docs/rules/no-nested-component-definitions) | ✅ | ✅ | | [`no-unused-class-component-methods`](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/no-unused-class-component-methods.md) | [`no-unused-class-component-members`](/docs/rules/no-unused-class-component-members) | ✅ | 🚫 | -| [`no-unused-prop-types`](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/no-unused-prop-types.md) | [`no-prop-types`](/docs/rules/no-prop-types) | ✅ | 🚫 | +| [`no-unused-prop-types`](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/no-unused-prop-types.md) | [`no-unused-props`](/docs/rules/no-unused-props) | ✅ | 🚫 | | [`no-unused-state`](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/no-unused-state.md) | [`no-unused-state`](/docs/rules/no-unused-state) | ✅ | 🚫 | | [`no-will-update-set-state`](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/no-will-update-set-state.md) | [`no-set-state-in-component-will-update`](/docs/rules/no-set-state-in-component-will-update) | ✅ | ✅ | | [`prefer-es6-class`](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/prefer-es6-class.md) | [`no-prop-types`](/docs/rules/no-prop-types) | ✅ | 🚫 | diff --git a/apps/website/content/docs/rules/meta.json b/apps/website/content/docs/rules/meta.json index 4c284c2852..cbe0000694 100644 --- a/apps/website/content/docs/rules/meta.json +++ b/apps/website/content/docs/rules/meta.json @@ -55,6 +55,7 @@ "no-unstable-context-value", "no-unstable-default-props", "no-unused-class-component-members", + "no-unused-props", "no-unused-state", "no-use-context", "no-useless-forward-ref", diff --git a/apps/website/content/docs/rules/overview.mdx b/apps/website/content/docs/rules/overview.mdx index 0f8c8c7194..a708e0d235 100644 --- a/apps/website/content/docs/rules/overview.mdx +++ b/apps/website/content/docs/rules/overview.mdx @@ -82,6 +82,7 @@ The `jsx-*` rules check for issues exclusive to JSX syntax, which are absent fro | [`no-unstable-context-value`](./no-unstable-context-value) | 1️⃣ | | Prevents non-stable values (i.e. object literals) from being used as a value for `Context.Provider` | | | [`no-unstable-default-props`](./no-unstable-default-props) | 1️⃣ | | Prevents using referential-type values as default props in object destructuring | | | [`no-unused-class-component-members`](./no-unused-class-component-members) | 1️⃣ | | Warns unused class component methods and properties | | +| [`no-unused-props`](./no-unused-props) | 0️⃣ | | Warns about unused component prop declarations | | | [`no-unused-state`](./no-unused-state) | 1️⃣ | | Warns unused class component state | | | [`no-use-context`](./no-use-context) | 1️⃣ | `🔄` | Replaces usages of `useContext` with `use` | >=19.0.0 | | [`no-useless-forward-ref`](./no-useless-forward-ref) | 1️⃣ | | Disallow useless `forwardRef` calls on components that don't use `ref`s | | 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 cc4ba5ae27..7a3ad3db90 100644 --- a/packages/plugins/eslint-plugin-react-x/src/configs/recommended.ts +++ b/packages/plugins/eslint-plugin-react-x/src/configs/recommended.ts @@ -56,6 +56,7 @@ export const rules = { "react-x/no-unstable-context-value": "warn", "react-x/no-unstable-default-props": "warn", "react-x/no-unused-class-component-members": "warn", + // "react-x/no-unused-props": "warn", "react-x/no-unused-state": "warn", "react-x/no-use-context": "warn", "react-x/no-useless-forward-ref": "warn", diff --git a/packages/plugins/eslint-plugin-react-x/src/plugin.ts b/packages/plugins/eslint-plugin-react-x/src/plugin.ts index 29ceebec85..5df3b29b6a 100644 --- a/packages/plugins/eslint-plugin-react-x/src/plugin.ts +++ b/packages/plugins/eslint-plugin-react-x/src/plugin.ts @@ -51,6 +51,7 @@ import noUnsafeComponentWillUpdate from "./rules/no-unsafe-component-will-update import noUnstableContextValue from "./rules/no-unstable-context-value"; import noUnstableDefaultProps from "./rules/no-unstable-default-props"; import noUnusedClassComponentMembers from "./rules/no-unused-class-component-members"; +import noUnusedProps from "./rules/no-unused-props"; import noUnusedState from "./rules/no-unused-state"; import noUseContext from "./rules/no-use-context"; import noUselessForwardRef from "./rules/no-useless-forward-ref"; @@ -127,6 +128,7 @@ export const plugin = { "no-unstable-context-value": noUnstableContextValue, "no-unstable-default-props": noUnstableDefaultProps, "no-unused-class-component-members": noUnusedClassComponentMembers, + "no-unused-props": noUnusedProps, "no-unused-state": noUnusedState, "no-use-context": noUseContext, "no-useless-forward-ref": noUselessForwardRef, diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.md b/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.md new file mode 100644 index 0000000000..5fd8990c65 --- /dev/null +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.md @@ -0,0 +1,81 @@ +--- +title: no-unused-props +--- + +**Full Name in `eslint-plugin-react-x`** + +```sh copy +react-x/no-unused-props +``` + +**Full Name in `@eslint-react/eslint-plugin`** + +```sh copy +@eslint-react/no-unused-props +``` + +## Description + +Warns about unused component prop declarations. + +Unused props increase maintenance overhead and may mislead consumers of the component into thinking the prop is required or meaningful, even when it has no effect. + +This is the TypeScript-only version of [`eslint-plugin-react/no-unused-prop-types`](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/no-unused-prop-types.md). In contrast to the original rule, this rule + +- doesn't support the legacy propTypes syntax +- combines the used props of one type definition declared by multiple components + +## Examples + +### Failing + +```tsx +interface Props { + abc: string; // used + hello: string; // NOT used +} + +function Component(props: Props) { + const { abc } = props; // `hello` isn't accessed from `props` + return null; +} +``` + +### Passing + +```tsx +interface Props { + abc: string; // used + hello: string; // used +} + +function Component(props: Props) { + const { abc, hello } = props; + return null; +} +``` + +```tsx +interface Props { + abc: string; // used by Component1 + hello: string; // used by Component2 +} + +function Component1({ abc }: Props) { + return null; +} + +function Component2({ hello }: Props) { + return null; +} +``` + +## Implementation + +- [Rule source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.ts) +- [Test source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.spec.ts) + +## See Also + +- [`no-prop-types`](/docs/rules/no-prop-types)\ + Disallows `propTypes` diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.spec.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.spec.ts new file mode 100644 index 0000000000..726e6946e1 --- /dev/null +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.spec.ts @@ -0,0 +1,811 @@ +import tsx from "dedent"; + +import { allValid, ruleTesterWithTypes } from "../../../../../test"; +import rule, { RULE_NAME } from "./no-unused-props"; + +ruleTesterWithTypes.run(RULE_NAME, rule, { + invalid: [ + { + // interface type and later destructuring + code: tsx` + interface Props { + abc: string; + hello: string; + } + + function Component(props: Props) { + const { abc } = props; + return null; + } + `, + errors: [{ + messageId: "noUnusedProps", + column: 3, + data: { + name: "hello", + }, + endColumn: 8, + endLine: 3, + line: 3, + }], + }, + { + // interface type and direct destructuring + code: tsx` + interface Props { + abc: string; + hello: string; + } + + function Component({ abc }: Props) { + return null; + } + `, + errors: [{ + messageId: "noUnusedProps", + column: 3, + data: { + name: "hello", + }, + endColumn: 8, + endLine: 3, + line: 3, + }], + }, + { + // named type and later destructuring + code: tsx` + type Props = { + abc: string; + hello: string; + } + + function Component(props: Props) { + const { abc } = props; + return null; + } + `, + errors: [{ + messageId: "noUnusedProps", + column: 3, + data: { + name: "hello", + }, + endColumn: 8, + endLine: 3, + line: 3, + }], + }, + { + // interface type and direct destructuring + code: tsx` + type Props = { + abc: string; + hello: string; + } + + function Component({ abc }: Props) { + return null; + } + `, + errors: [{ + messageId: "noUnusedProps", + column: 3, + data: { + name: "hello", + }, + endColumn: 8, + endLine: 3, + line: 3, + }], + }, + { + // inline type and later destructuring + code: tsx` + function Component(props: { abc: string; hello: string; }) { + const { abc } = props; + return null; + } + `, + errors: [{ + messageId: "noUnusedProps", + column: 42, + data: { + name: "hello", + }, + endColumn: 47, + endLine: 1, + line: 1, + }], + }, + { + // inline type and direct destructuring + code: tsx` + function Component({ abc }: { abc: string; hello: string; }) { + return null; + } + `, + errors: [{ + messageId: "noUnusedProps", + column: 44, + data: { + name: "hello", + }, + endColumn: 49, + endLine: 1, + line: 1, + }], + }, + { + // multiple properties unused + code: tsx` + function Component({ }: { abc: string; hello: string; }) { + return null; + } + `, + errors: [{ + messageId: "noUnusedProps", + column: 27, + data: { + name: "abc", + }, + endColumn: 30, + endLine: 1, + line: 1, + }, { + messageId: "noUnusedProps", + column: 40, + data: { + name: "hello", + }, + endColumn: 45, + endLine: 1, + line: 1, + }], + }, + { + // interface augmentation + code: tsx` + interface Props { + used1: string; + abc: string; + } + + interface Props { + used2: string; + hello: string; + } + + function Component({ used1, used2 }: Props) { + return null; + } + `, + errors: [{ + messageId: "noUnusedProps", + column: 3, + data: { + name: "abc", + }, + endColumn: 6, + endLine: 3, + line: 3, + }, { + messageId: "noUnusedProps", + column: 3, + data: { + name: "hello", + }, + endColumn: 8, + endLine: 8, + line: 8, + }], + }, + { + // interface union + code: tsx` + interface Props1 { + used1: string; + abc: string; + } + + interface Props2 { + used2: string; + hello: string; + } + + function Component({ used1, used2 }: Props1 & Props2) { + return null; + } + `, + errors: [{ + messageId: "noUnusedProps", + column: 3, + data: { + name: "abc", + }, + endColumn: 6, + endLine: 3, + line: 3, + }, { + messageId: "noUnusedProps", + column: 3, + data: { + name: "hello", + }, + endColumn: 8, + endLine: 8, + line: 8, + }], + }, + { + // interface extends + code: tsx` + interface PropsBase { + used1: string; + abc: string; + } + + interface Props extends PropsBase { + used2: string; + hello: string; + } + + function Component({ used1, used2 }: Props) { + return null; + } + `, + errors: [{ + messageId: "noUnusedProps", + column: 3, + data: { + name: "abc", + }, + endColumn: 6, + endLine: 3, + line: 3, + }, { + messageId: "noUnusedProps", + column: 3, + data: { + name: "hello", + }, + endColumn: 8, + endLine: 8, + line: 8, + }], + }, + { + // track uses of properties on rest element + code: tsx` + function Component({ ...rest }: { abc: string; hello: string; }) { + return
{rest.abc}
; + } + `, + errors: [{ + messageId: "noUnusedProps", + column: 48, + data: { + name: "hello", + }, + endColumn: 53, + endLine: 1, + line: 1, + }], + }, + { + // track uses of properties on rest element + code: tsx` + function Component(props: { abc: string; hello: string; }) { + const { ...rest } = props; + return
{rest.abc}
; + } + `, + errors: [{ + messageId: "noUnusedProps", + column: 42, + data: { + name: "hello", + }, + endColumn: 47, + endLine: 1, + line: 1, + }], + }, + { + // track assignment + code: tsx` + function Component(props: { abc: string; hello: string; }) { + const abc = props.abc; + return
{abc}
; + } + `, + errors: [{ + messageId: "noUnusedProps", + column: 42, + data: { + name: "hello", + }, + endColumn: 47, + endLine: 1, + line: 1, + }], + }, + { + // track computed member access + code: tsx` + function Component(props: { abc: string; hello: string; }) { + return
{props["abc"]}
; + } + `, + errors: [{ + messageId: "noUnusedProps", + column: 42, + data: { + name: "hello", + }, + endColumn: 47, + endLine: 1, + line: 1, + }], + }, + { + // correct error span on complex prop type + code: tsx` + function Component({ abc }: { abc: string; hello: { abc: string; subHello: number | null }; }) { + return null; + } + `, + errors: [{ + messageId: "noUnusedProps", + column: 44, + data: { + name: "hello", + }, + endColumn: 49, + endLine: 1, + line: 1, + }], + }, + { + // access of sub property should mark property as used + code: tsx` + function Component({ hello: { subHello } }: { abc: string; hello: { abc: string; subHello: number | null }; }) { + return null; + } + `, + errors: [{ + messageId: "noUnusedProps", + column: 47, + data: { + name: "abc", + }, + endColumn: 50, + endLine: 1, + line: 1, + }], + }, + { + // expect no false negatives when using PropsWithChildren + code: tsx` + import { PropsWithChildren } from 'react'; + + type ButtonProps = { + backgroundColor : string; + onClick: () => void; + }; + + const Button = ({ backgroundColor }: PropsWithChildren) => { + return ( + + ); + }; + `, + // expect no false positives when using PropsWithChildren + tsx` + import { PropsWithChildren } from 'react'; + + type ButtonProps = PropsWithChildren<{ + backgroundColor : string; + onClick: () => void; + }>; + + const Button = ({ backgroundColor, onClick, children }: ButtonProps) => { + return ( + + ); + }; + `, + // TODO: Should we report unused children prop when using PropsWithChildren? currently we don't + tsx` + import { PropsWithChildren } from 'react'; + + type ButtonProps = { + backgroundColor : string; + onClick: () => void; + }; + + const Button = ({ backgroundColor, onClick }: PropsWithChildren) => { + return ( +