From c02d42a0f336069f7cd5479b0ae3e00726c038f0 Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Mon, 21 Jul 2025 21:12:32 +0200 Subject: [PATCH 01/23] bootstrap new rule --- apps/website/content/docs/rules/overview.mdx | 1 + .../vite-react-dom-app-v1/eslint.config.js | 1 + .../src/configs/recommended.ts | 1 + .../eslint-plugin-react-x/src/plugin.ts | 2 + .../src/rules/no-unused-props.md | 30 +++++++++++++ .../src/rules/no-unused-props.spec.ts | 11 +++++ .../src/rules/no-unused-props.ts | 44 +++++++++++++++++++ .../plugins/eslint-plugin/src/configs/all.ts | 1 + .../plugins/eslint-plugin/src/configs/x.ts | 1 + 9 files changed, 92 insertions(+) create mode 100644 packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.md create mode 100644 packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.spec.ts create mode 100644 packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.ts diff --git a/apps/website/content/docs/rules/overview.mdx b/apps/website/content/docs/rules/overview.mdx index 0f8c8c7194..a999bf8d03 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 unused component props 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/examples/vite-react-dom-app-v1/eslint.config.js b/examples/vite-react-dom-app-v1/eslint.config.js index 212f3e4e27..d653788121 100644 --- a/examples/vite-react-dom-app-v1/eslint.config.js +++ b/examples/vite-react-dom-app-v1/eslint.config.js @@ -109,6 +109,7 @@ export default tseslint.config( "@eslint-react/no-unstable-context-value": "warn", "@eslint-react/no-unstable-default-props": "warn", "@eslint-react/no-unused-class-component-members": "warn", + "@eslint-react/no-unused-props": "warn", "@eslint-react/no-unused-state": "warn", "@eslint-react/no-use-context": "warn", "@eslint-react/no-useless-forward-ref": "warn", 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..7a6e36d898 --- /dev/null +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.md @@ -0,0 +1,30 @@ +--- +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 unused component props declarations. + +## Examples + +### Failing + +### Passing + +## 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) 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..4ab03d89b0 --- /dev/null +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.spec.ts @@ -0,0 +1,11 @@ +import tsx from "dedent"; + +import { allValid, ruleTester } from "../../../../../test"; +import rule, { RULE_NAME } from "./no-unused-props"; + +ruleTester.run(RULE_NAME, rule, { + invalid: [], + valid: [ + ...allValid, + ], +}); diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.ts new file mode 100644 index 0000000000..3e86148697 --- /dev/null +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.ts @@ -0,0 +1,44 @@ +import type { RuleContext, RuleFeature } from "@eslint-react/kit"; +import type { RuleListener } from "@typescript-eslint/utils/ts-eslint"; +import type { CamelCase } from "string-ts"; +import * as ER from "@eslint-react/core"; + +import { createRule } from "../utils"; + +export const RULE_NAME = "no-unused-props"; + +export const RULE_FEATURES = [] as const satisfies RuleFeature[]; + +export type MessageID = CamelCase; + +export default createRule<[], MessageID>({ + meta: { + type: "problem", + docs: { + description: "Disallow passing `children` as a prop.", + [Symbol.for("rule_features")]: RULE_FEATURES, + }, + messages: { + noUnusedProps: "Do not pass 'children' as props.", + }, + schema: [], + }, + name: RULE_NAME, + create, + defaultOptions: [], +}); + +export function create(context: RuleContext): RuleListener { + return { + JSXElement(node) { + const attributes = node.openingElement.attributes; + const childrenProp = ER.getAttribute(context, "children", attributes, context.sourceCode.getScope(node)); + if (childrenProp != null) { + context.report({ + messageId: "noUnusedProps", + node: childrenProp, + }); + } + }, + }; +} diff --git a/packages/plugins/eslint-plugin/src/configs/all.ts b/packages/plugins/eslint-plugin/src/configs/all.ts index a477b625ab..e71055c03f 100644 --- a/packages/plugins/eslint-plugin/src/configs/all.ts +++ b/packages/plugins/eslint-plugin/src/configs/all.ts @@ -63,6 +63,7 @@ export const rules = { "@eslint-react/no-unstable-context-value": "warn", "@eslint-react/no-unstable-default-props": "warn", "@eslint-react/no-unused-class-component-members": "warn", + "@eslint-react/no-unused-props": "warn", "@eslint-react/no-unused-state": "warn", "@eslint-react/no-use-context": "warn", "@eslint-react/no-useless-forward-ref": "warn", diff --git a/packages/plugins/eslint-plugin/src/configs/x.ts b/packages/plugins/eslint-plugin/src/configs/x.ts index 5b0fe57670..7ff5cf062e 100644 --- a/packages/plugins/eslint-plugin/src/configs/x.ts +++ b/packages/plugins/eslint-plugin/src/configs/x.ts @@ -57,6 +57,7 @@ export const rules = { "@eslint-react/no-unstable-context-value": "warn", "@eslint-react/no-unstable-default-props": "warn", "@eslint-react/no-unused-class-component-members": "warn", + "@eslint-react/no-unused-props": "warn", "@eslint-react/no-unused-state": "warn", "@eslint-react/no-use-context": "warn", "@eslint-react/no-useless-forward-ref": "warn", From ac323a54616d238377f364e3dfe4458cb6660f96 Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Tue, 22 Jul 2025 21:12:47 +0200 Subject: [PATCH 02/23] add first tests and make them pass --- apps/website/content/docs/rules/overview.mdx | 2 +- .../src/rules/no-unused-props.md | 2 +- .../src/rules/no-unused-props.spec.ts | 86 ++++++++++- .../src/rules/no-unused-props.ts | 141 ++++++++++++++++-- 4 files changed, 215 insertions(+), 16 deletions(-) diff --git a/apps/website/content/docs/rules/overview.mdx b/apps/website/content/docs/rules/overview.mdx index a999bf8d03..a708e0d235 100644 --- a/apps/website/content/docs/rules/overview.mdx +++ b/apps/website/content/docs/rules/overview.mdx @@ -82,7 +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 unused component props declarations | | +| [`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/rules/no-unused-props.md b/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.md index 7a6e36d898..4f945f13d4 100644 --- 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 @@ -16,7 +16,7 @@ react-x/no-unused-props ## Description -Warns unused component props declarations. +Warns about unused component prop declarations. ## Examples 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 index 4ab03d89b0..ebae8b8a70 100644 --- 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 @@ -1,10 +1,90 @@ import tsx from "dedent"; -import { allValid, ruleTester } from "../../../../../test"; +import { allValid, ruleTesterWithTypes } from "../../../../../test"; import rule, { RULE_NAME } from "./no-unused-props"; -ruleTester.run(RULE_NAME, rule, { - invalid: [], +ruleTesterWithTypes.run(RULE_NAME, rule, { + invalid: [{ + code: tsx` + interface Props { + abc: string; + hello: string; + } + + function Component(props: Props) { + const {abc} = props; + + return

{abc}

; + } + `, + errors: [{ + messageId: "noUnusedProps", + column: 3, + data: { + name: "hello", + }, + endColumn: 17, + endLine: 3, + line: 3, + }], + }, { + code: tsx` + type Props = { + abc: string; + hello: string; + } + + function Component(props: Props) { + const {abc} = props; + + return

{abc}

; + } + `, + errors: [{ + messageId: "noUnusedProps", + column: 3, + data: { + name: "hello", + }, + endColumn: 17, + endLine: 3, + line: 3, + }], + }, { + code: tsx` + function Component(props: { abc: string; hello: string; }) { + const {abc} = props; + + return

{abc}

; + } + `, + errors: [{ + messageId: "noUnusedProps", + column: 42, + data: { + name: "hello", + }, + endColumn: 56, + endLine: 1, + line: 1, + }], + }, { + code: tsx` + function Component({ abc }: { abc: string; hello: string; }) { + return

{abc}

; + } + `, + errors: [{ + messageId: "noUnusedProps", + column: 44, + data: { + name: "hello", + }, + endColumn: 58, + endLine: 1, + line: 1, + }], + }], valid: [ ...allValid, ], diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.ts index 3e86148697..6d929ef861 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.ts @@ -1,13 +1,17 @@ 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 type ts from "typescript"; import * as ER from "@eslint-react/core"; +import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; +import { ESLintUtils, type ParserServicesWithTypeInformation } from "@typescript-eslint/utils"; import { createRule } from "../utils"; export const RULE_NAME = "no-unused-props"; -export const RULE_FEATURES = [] as const satisfies RuleFeature[]; +export const RULE_FEATURES = ["TSC", "EXP"] as const satisfies RuleFeature[]; export type MessageID = CamelCase; @@ -15,11 +19,11 @@ export default createRule<[], MessageID>({ meta: { type: "problem", docs: { - description: "Disallow passing `children` as a prop.", + description: "Warns about unused component prop declarations.", [Symbol.for("rule_features")]: RULE_FEATURES, }, messages: { - noUnusedProps: "Do not pass 'children' as props.", + noUnusedProps: "Prop `{{name}}` is declared but never used", }, schema: [], }, @@ -29,16 +33,131 @@ export default createRule<[], MessageID>({ }); export function create(context: RuleContext): RuleListener { + const services = ESLintUtils.getParserServices(context, false); + const { ctx, listeners } = ER.useComponentCollector(context); + return { - JSXElement(node) { - const attributes = node.openingElement.attributes; - const childrenProp = ER.getAttribute(context, "children", attributes, context.sourceCode.getScope(node)); - if (childrenProp != null) { - context.report({ - messageId: "noUnusedProps", - node: childrenProp, - }); + ...listeners, + "Program:exit"(program) { + const checker = services.program.getTypeChecker(); + const components = ctx.getAllComponents(program); + + for (const [, component] of components) { + const [props] = component.node.params; + if (props == null) continue; + + const usedPropKeys = collectUsedPropKeys(context, props); + if (usedPropKeys == null) continue; + + const tsNode = services.esTreeNodeToTSNodeMap.get(props); + const declaredProps = checker.getTypeAtLocation(tsNode).getProperties(); + + for (const prop of declaredProps) { + if (!usedPropKeys.has(prop.name)) { + reportUnusedProp(context, services, prop); + } + } } }, }; } + +function collectUsedPropKeys(context: RuleContext, props: TSESTree.Parameter): Set | null { + switch (props.type) { + case T.Identifier: { + return collectUsedPropKeysOfIdentifier(context, props); + } + case T.ObjectPattern: { + return collectUsedPropKeysOfObjectPattern(context, props); + } + default: { + return null; + } + } +} + +function collectUsedPropKeysOfObjectPattern( + context: RuleContext, + props: TSESTree.ObjectPattern, +): Set | null { + const usedKeys = new Set(); + + for (const prop of props.properties) { + if (prop.type === T.Property) { + if (prop.key.type === T.Identifier) { + usedKeys.add(prop.key.name); + } else if (prop.key.type === T.Literal && typeof prop.key.value === "string") { + usedKeys.add(prop.key.value); + } + } else if (prop.argument.type === T.Identifier) { + // TODO: handle rest props destructuring here + } + } + + return usedKeys; +} + +function collectUsedPropKeysOfIdentifier( + context: RuleContext, + props: TSESTree.Identifier, +): Set | null { + const propsName = props.name; + const scope = context.sourceCode.getScope(props); + const variable = scope.variables.find((v) => v.name === propsName); + + if (variable == null) return null; + + const usedPropKeys = new Set(); + for (const ref of variable.references) { + const node = ref.identifier.parent; + + switch (node.type) { + case T.MemberExpression: { + if ( + node.object.type === T.Identifier + && node.object.name === propsName + && node.property.type === T.Identifier + ) { + usedPropKeys.add(node.property.name); + } + break; + } + case T.VariableDeclarator: { + if ( + node.id.type === T.ObjectPattern + && ref.identifier === node.init + ) { + for (const prop of node.id.properties) { + if ( + prop.type === T.Property + && prop.key.type === T.Identifier + ) { + usedPropKeys.add(prop.key.name); + } + } + } + break; + } + } + } + + return usedPropKeys; +} + +function reportUnusedProp( + context: RuleContext, + services: ParserServicesWithTypeInformation, + prop: ts.Symbol, +) { + const declarations = prop.getDeclarations(); + if (declarations != null) { + const decl = declarations[0]; + if (decl == null) return; + const esNode = services.tsNodeToESTreeNodeMap.get(decl); + context.report({ + messageId: "noUnusedProps", + node: esNode, + data: { name: prop.name }, + }); + } +} From d5c321f3a49239df2aa214b8ac6a2e25fdb4ec01 Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Tue, 22 Jul 2025 21:23:40 +0200 Subject: [PATCH 03/23] add more tests --- .../src/rules/no-unused-props.spec.ts | 58 +++++++++++++++---- 1 file changed, 48 insertions(+), 10 deletions(-) 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 index ebae8b8a70..9b024d3e2b 100644 --- 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 @@ -12,9 +12,8 @@ ruleTesterWithTypes.run(RULE_NAME, rule, { } function Component(props: Props) { - const {abc} = props; - - return

{abc}

; + const { abc } = props; + return null; } `, errors: [{ @@ -35,9 +34,8 @@ ruleTesterWithTypes.run(RULE_NAME, rule, { } function Component(props: Props) { - const {abc} = props; - - return

{abc}

; + const { abc } = props; + return null; } `, errors: [{ @@ -53,9 +51,8 @@ ruleTesterWithTypes.run(RULE_NAME, rule, { }, { code: tsx` function Component(props: { abc: string; hello: string; }) { - const {abc} = props; - - return

{abc}

; + const { abc } = props; + return null; } `, errors: [{ @@ -71,7 +68,7 @@ ruleTesterWithTypes.run(RULE_NAME, rule, { }, { code: tsx` function Component({ abc }: { abc: string; hello: string; }) { - return

{abc}

; + return null; } `, errors: [{ @@ -86,6 +83,47 @@ ruleTesterWithTypes.run(RULE_NAME, rule, { }], }], valid: [ + { + code: tsx` + interface Props { + abc: string; + hello: string; + } + + function Component(props: Props) { + const { abc, hello } = props; + return null; + } + `, + }, + { + code: tsx` + type Props = { + abc: string; + hello: string; + } + + function Component(props: Props) { + const { abc, hello } = props; + return null; + } + `, + }, + { + code: tsx` + function Component(props: { abc: string; hello: string; }) { + const { abc, hello } = props; + return null; + } + `, + }, + { + code: tsx` + function Component({ abc, hello }: { abc: string; hello: string; }) { + return null; + } + `, + }, ...allValid, ], }); From 961ba4e68473810d35c2bf7c402ba6a5125f6899 Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Tue, 22 Jul 2025 21:34:52 +0200 Subject: [PATCH 04/23] add basic examples to doc page --- .../src/rules/no-unused-props.md | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) 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 index 4f945f13d4..f064185faf 100644 --- 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 @@ -22,8 +22,32 @@ Warns about unused component prop declarations. ### 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; +} +``` + ## Implementation - [Rule source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.ts) From 8e31ac6b806beb47c843af8992504d7c1d7b060e Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Wed, 23 Jul 2025 08:06:20 +0200 Subject: [PATCH 05/23] remove rule from examples for now Signed-off-by: Ulrich Stark --- examples/vite-react-dom-app-v1/eslint.config.js | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/vite-react-dom-app-v1/eslint.config.js b/examples/vite-react-dom-app-v1/eslint.config.js index d653788121..212f3e4e27 100644 --- a/examples/vite-react-dom-app-v1/eslint.config.js +++ b/examples/vite-react-dom-app-v1/eslint.config.js @@ -109,7 +109,6 @@ export default tseslint.config( "@eslint-react/no-unstable-context-value": "warn", "@eslint-react/no-unstable-default-props": "warn", "@eslint-react/no-unused-class-component-members": "warn", - "@eslint-react/no-unused-props": "warn", "@eslint-react/no-unused-state": "warn", "@eslint-react/no-use-context": "warn", "@eslint-react/no-useless-forward-ref": "warn", From 64e2fb10e335dc3f80dbb4f5967e755a68fe7f13 Mon Sep 17 00:00:00 2001 From: REL1CX Date: Fri, 25 Jul 2025 11:13:00 +0800 Subject: [PATCH 06/23] comment out no-unused-props in x preset Signed-off-by: REL1CX --- packages/plugins/eslint-plugin/src/configs/x.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugins/eslint-plugin/src/configs/x.ts b/packages/plugins/eslint-plugin/src/configs/x.ts index 7ff5cf062e..46b95f688f 100644 --- a/packages/plugins/eslint-plugin/src/configs/x.ts +++ b/packages/plugins/eslint-plugin/src/configs/x.ts @@ -57,7 +57,7 @@ export const rules = { "@eslint-react/no-unstable-context-value": "warn", "@eslint-react/no-unstable-default-props": "warn", "@eslint-react/no-unused-class-component-members": "warn", - "@eslint-react/no-unused-props": "warn", + // "@eslint-react/no-unused-props": "warn", "@eslint-react/no-unused-state": "warn", "@eslint-react/no-use-context": "warn", "@eslint-react/no-useless-forward-ref": "warn", From d72e304ca971fd4daa471002e7669e0e93f7496a Mon Sep 17 00:00:00 2001 From: REL1CX Date: Fri, 25 Jul 2025 11:46:44 +0800 Subject: [PATCH 07/23] fix: comment out no-unused-props rule in 'all' config Signed-off-by: REL1CX --- packages/plugins/eslint-plugin/src/configs/all.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugins/eslint-plugin/src/configs/all.ts b/packages/plugins/eslint-plugin/src/configs/all.ts index e71055c03f..9167089203 100644 --- a/packages/plugins/eslint-plugin/src/configs/all.ts +++ b/packages/plugins/eslint-plugin/src/configs/all.ts @@ -63,7 +63,7 @@ export const rules = { "@eslint-react/no-unstable-context-value": "warn", "@eslint-react/no-unstable-default-props": "warn", "@eslint-react/no-unused-class-component-members": "warn", - "@eslint-react/no-unused-props": "warn", + // "@eslint-react/no-unused-props": "warn", "@eslint-react/no-unused-state": "warn", "@eslint-react/no-use-context": "warn", "@eslint-react/no-useless-forward-ref": "warn", From c7e4608e8d65e7d1d96204579b1b6931f3dba573 Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Fri, 25 Jul 2025 21:40:31 +0200 Subject: [PATCH 08/23] only report on property key and not the whole declaration --- .../src/rules/no-unused-props.spec.ts | 8 ++++---- .../src/rules/no-unused-props.ts | 11 +++++++++-- 2 files changed, 13 insertions(+), 6 deletions(-) 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 index 9b024d3e2b..23705797f0 100644 --- 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 @@ -22,7 +22,7 @@ ruleTesterWithTypes.run(RULE_NAME, rule, { data: { name: "hello", }, - endColumn: 17, + endColumn: 8, endLine: 3, line: 3, }], @@ -44,7 +44,7 @@ ruleTesterWithTypes.run(RULE_NAME, rule, { data: { name: "hello", }, - endColumn: 17, + endColumn: 8, endLine: 3, line: 3, }], @@ -61,7 +61,7 @@ ruleTesterWithTypes.run(RULE_NAME, rule, { data: { name: "hello", }, - endColumn: 56, + endColumn: 47, endLine: 1, line: 1, }], @@ -77,7 +77,7 @@ ruleTesterWithTypes.run(RULE_NAME, rule, { data: { name: "hello", }, - endColumn: 58, + endColumn: 49, endLine: 1, line: 1, }], diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.ts index 6d929ef861..2890e607e4 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.ts @@ -153,10 +153,17 @@ function reportUnusedProp( if (declarations != null) { const decl = declarations[0]; if (decl == null) return; - const esNode = services.tsNodeToESTreeNodeMap.get(decl); + const node = services.tsNodeToESTreeNodeMap.get(decl); + + const nodeKey = node.type === T.TSPropertySignature + || node.type === T.PropertyDefinition + || node.type === T.Property + ? node.key + : node; + context.report({ messageId: "noUnusedProps", - node: esNode, + node: nodeKey, data: { name: prop.name }, }); } From ba34768e8d6c2cd00c8ea20ff66fe4c753d5d007 Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Fri, 25 Jul 2025 22:20:03 +0200 Subject: [PATCH 09/23] add valid test and make it pass --- .../src/rules/no-unused-props.spec.ts | 21 +++++++++++++++++++ .../src/rules/no-unused-props.ts | 18 ++++++++++++---- 2 files changed, 35 insertions(+), 4 deletions(-) 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 index 23705797f0..b0afafbc54 100644 --- 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 @@ -84,6 +84,7 @@ ruleTesterWithTypes.run(RULE_NAME, rule, { }], valid: [ { + // valid, because all props are used code: tsx` interface Props { abc: string; @@ -97,6 +98,7 @@ ruleTesterWithTypes.run(RULE_NAME, rule, { `, }, { + // valid, because all props are used code: tsx` type Props = { abc: string; @@ -110,6 +112,7 @@ ruleTesterWithTypes.run(RULE_NAME, rule, { `, }, { + // valid, because all props are used code: tsx` function Component(props: { abc: string; hello: string; }) { const { abc, hello } = props; @@ -118,12 +121,30 @@ ruleTesterWithTypes.run(RULE_NAME, rule, { `, }, { + // valid, because all props are used code: tsx` function Component({ abc, hello }: { abc: string; hello: string; }) { return null; } `, }, + { + // valid, because props are used by two components each accessing one prop + code: tsx` + interface Props { + abc: string; + hello: string; + } + + function Component({ abc }: Props) { + return null; + } + + function Component2({ hello }: Props) { + return null; + } + `, + }, ...allValid, ], }); diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.ts index 2890e607e4..54d2417a12 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.ts @@ -42,6 +42,9 @@ export function create(context: RuleContext): RuleListener { const checker = services.program.getTypeChecker(); const components = ctx.getAllComponents(program); + const totalDeclaredProps = new Set(); + const totalUsedProps = new Set(); + for (const [, component] of components) { const [props] = component.node.params; if (props == null) continue; @@ -52,12 +55,19 @@ export function create(context: RuleContext): RuleListener { const tsNode = services.esTreeNodeToTSNodeMap.get(props); const declaredProps = checker.getTypeAtLocation(tsNode).getProperties(); - for (const prop of declaredProps) { - if (!usedPropKeys.has(prop.name)) { - reportUnusedProp(context, services, prop); + for (const declaredProp of declaredProps) { + totalDeclaredProps.add(declaredProp); + + if (usedPropKeys.has(declaredProp.name)) { + totalUsedProps.add(declaredProp); } } } + + const unusedProps = totalDeclaredProps.difference(totalUsedProps); + for (const unusedProp of unusedProps) { + reportUnusedProp(context, services, unusedProp); + } }, }; } @@ -79,7 +89,7 @@ function collectUsedPropKeys(context: RuleContext, props: TSESTre function collectUsedPropKeysOfObjectPattern( context: RuleContext, props: TSESTree.ObjectPattern, -): Set | null { +): Set { const usedKeys = new Set(); for (const prop of props.properties) { From f0b5e3bfb2dca496fede9a0349dfb8cf19dec185 Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Fri, 25 Jul 2025 22:53:47 +0200 Subject: [PATCH 10/23] add more tests to avoid false positives --- .../src/rules/no-unused-props.spec.ts | 49 +++++++++++++++++++ .../src/rules/no-unused-props.ts | 22 +++++---- 2 files changed, 62 insertions(+), 9 deletions(-) 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 index b0afafbc54..841d694a58 100644 --- 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 @@ -145,6 +145,55 @@ ruleTesterWithTypes.run(RULE_NAME, rule, { } `, }, + { + // valid, because we can't track what happens to the props object + code: tsx` + import { Component2 } from "./component2"; + + interface Props { + abc: string; + hello: string; + } + + function Component(props: Props) { + return ; + } + `, + }, + { + // valid, because we can't track what happens to the props object + code: tsx` + import { anyFunction } from "./anyFunction"; + + interface Props { + abc: string; + hello: string; + } + + function Component(props: Props) { + anyFunction(props); + + return null; + } + `, + }, + { + // valid, because we can't track what happens to the props object + code: tsx` + import { anyFunction } from "./anyFunction"; + + interface Props { + abc: string; + hello: string; + } + + function Component(props: Props) { + anyFunction({ props }); + + return null; + } + `, + }, ...allValid, ], }); diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.ts index 54d2417a12..6b3a063f5a 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.ts @@ -119,25 +119,25 @@ function collectUsedPropKeysOfIdentifier( const usedPropKeys = new Set(); for (const ref of variable.references) { - const node = ref.identifier.parent; + const { parent } = ref.identifier; - switch (node.type) { + switch (parent.type) { case T.MemberExpression: { if ( - node.object.type === T.Identifier - && node.object.name === propsName - && node.property.type === T.Identifier + parent.object.type === T.Identifier + && parent.object.name === propsName + && parent.property.type === T.Identifier ) { - usedPropKeys.add(node.property.name); + usedPropKeys.add(parent.property.name); } break; } case T.VariableDeclarator: { if ( - node.id.type === T.ObjectPattern - && ref.identifier === node.init + parent.id.type === T.ObjectPattern + && ref.identifier === parent.init ) { - for (const prop of node.id.properties) { + for (const prop of parent.id.properties) { if ( prop.type === T.Property && prop.key.type === T.Identifier @@ -148,6 +148,10 @@ function collectUsedPropKeysOfIdentifier( } break; } + default: { + // the whole props object is referenced in some way we probably can't track + return null; // return null to avoid false positives + } } } From 1393016a7f6c5a3cb90e086cd7b4c4fc2e657644 Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Fri, 25 Jul 2025 23:44:43 +0200 Subject: [PATCH 11/23] track properties of rest element, add more tests and comments --- .../src/rules/no-unused-props.spec.ts | 295 +++++++++++++++++- .../src/rules/no-unused-props.ts | 23 +- 2 files changed, 306 insertions(+), 12 deletions(-) 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 index 841d694a58..50b61494ee 100644 --- 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 @@ -5,6 +5,7 @@ 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; @@ -27,6 +28,29 @@ ruleTesterWithTypes.run(RULE_NAME, rule, { 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; @@ -49,6 +73,29 @@ ruleTesterWithTypes.run(RULE_NAME, rule, { 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; @@ -66,6 +113,7 @@ ruleTesterWithTypes.run(RULE_NAME, rule, { line: 1, }], }, { + // inline type and direct destructuring code: tsx` function Component({ abc }: { abc: string; hello: string; }) { return null; @@ -81,10 +129,161 @@ ruleTesterWithTypes.run(RULE_NAME, rule, { 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, + }], }], valid: [ { - // valid, because all props are used + // all props are used code: tsx` interface Props { abc: string; @@ -98,7 +297,20 @@ ruleTesterWithTypes.run(RULE_NAME, rule, { `, }, { - // valid, because all props are used + // all props are used + code: tsx` + interface Props { + abc: string; + hello: string; + } + + function Component({ abc, hello }: Props) { + return null; + } + `, + }, + { + // all props are used code: tsx` type Props = { abc: string; @@ -112,7 +324,20 @@ ruleTesterWithTypes.run(RULE_NAME, rule, { `, }, { - // valid, because all props are used + // all props are used + code: tsx` + type Props = { + abc: string; + hello: string; + } + + function Component({ abc, hello }: Props) { + return null; + } + `, + }, + { + // all props are used code: tsx` function Component(props: { abc: string; hello: string; }) { const { abc, hello } = props; @@ -121,7 +346,7 @@ ruleTesterWithTypes.run(RULE_NAME, rule, { `, }, { - // valid, because all props are used + // all props are used code: tsx` function Component({ abc, hello }: { abc: string; hello: string; }) { return null; @@ -129,7 +354,15 @@ ruleTesterWithTypes.run(RULE_NAME, rule, { `, }, { - // valid, because props are used by two components each accessing one prop + // all props are used + code: tsx` + function Component({ abc: abc2, hello: hello2 }: { abc: string; hello: string; }) { + return null; + } + `, + }, + { + // props are used by two components each accessing one prop code: tsx` interface Props { abc: string; @@ -146,7 +379,7 @@ ruleTesterWithTypes.run(RULE_NAME, rule, { `, }, { - // valid, because we can't track what happens to the props object + // we can't track what happens to the props object code: tsx` import { Component2 } from "./component2"; @@ -161,7 +394,7 @@ ruleTesterWithTypes.run(RULE_NAME, rule, { `, }, { - // valid, because we can't track what happens to the props object + // we can't track what happens to the props object code: tsx` import { anyFunction } from "./anyFunction"; @@ -178,7 +411,7 @@ ruleTesterWithTypes.run(RULE_NAME, rule, { `, }, { - // valid, because we can't track what happens to the props object + // we can't track what happens to the props object code: tsx` import { anyFunction } from "./anyFunction"; @@ -194,6 +427,52 @@ ruleTesterWithTypes.run(RULE_NAME, rule, { } `, }, + { + // one value used in jsx, the other in effect + code: tsx` + import { useEffect } from "react"; + + function Component({ abc, hello }: { abc: string; hello: string }) { + useEffect(() => { + console.log(hello); + }, []); + return
{abc}
; + } + `, + }, + { + // we can't track what happens to the rest object + code: tsx` + import { anyFunction } from "./anyFunction"; + + function Component({ abc, ...rest }: { abc: string; hello: string }) { + anyFunction(rest); + return null; + } + `, + }, + { + // props used inside nested function + code: tsx` + function Component(props: { abc: string; hello: string }) { + function inner() { + return props.hello; + } + return props.abc; + } + `, + }, + { + // props used conditionally + code: tsx` + function Component(props: { abc: string; hello: string }) { + if (Math.random() > 0.5) { + return
{props.abc}
; + } + return
{props.hello}
; + } + `, + }, ...allValid, ], }); diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.ts index 6b3a063f5a..ff67339bfd 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.ts @@ -50,7 +50,10 @@ export function create(context: RuleContext): RuleListener { if (props == null) continue; const usedPropKeys = collectUsedPropKeys(context, props); - if (usedPropKeys == null) continue; + if (usedPropKeys == null) { + // unable to determine prop keys, bail out to avoid false positives + continue; + } const tsNode = services.esTreeNodeToTSNodeMap.get(props); const declaredProps = checker.getTypeAtLocation(tsNode).getProperties(); @@ -81,6 +84,7 @@ function collectUsedPropKeys(context: RuleContext, props: TSESTre return collectUsedPropKeysOfObjectPattern(context, props); } default: { + // unable to determine prop keys, bail out to avoid false positives return null; } } @@ -89,18 +93,28 @@ function collectUsedPropKeys(context: RuleContext, props: TSESTre function collectUsedPropKeysOfObjectPattern( context: RuleContext, props: TSESTree.ObjectPattern, -): Set { +): Set | null { const usedKeys = new Set(); for (const prop of props.properties) { if (prop.type === T.Property) { + // Property if (prop.key.type === T.Identifier) { usedKeys.add(prop.key.name); } else if (prop.key.type === T.Literal && typeof prop.key.value === "string") { usedKeys.add(prop.key.value); } } else if (prop.argument.type === T.Identifier) { - // TODO: handle rest props destructuring here + // RestElement + const usedKeysOnRestElement = collectUsedPropKeysOfIdentifier(context, prop.argument); + if (usedKeysOnRestElement == null) { + // unable to determine prop keys, bail out to avoid false positives + return null; + } + + for (const usedKeyOnRestElement of usedKeysOnRestElement) { + usedKeys.add(usedKeyOnRestElement); + } } } @@ -150,7 +164,8 @@ function collectUsedPropKeysOfIdentifier( } default: { // the whole props object is referenced in some way we probably can't track - return null; // return null to avoid false positives + // => unable to determine prop keys, bail out to avoid false positives + return null; } } } From aa343f2c87f5d365db635edd5d3862d1451b1de6 Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Sat, 26 Jul 2025 16:17:56 +0200 Subject: [PATCH 12/23] make it work in node 20 --- .../eslint-plugin-react-x/src/rules/no-unused-props.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.ts index ff67339bfd..6f6df080d6 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.ts @@ -67,7 +67,9 @@ export function create(context: RuleContext): RuleListener { } } - const unusedProps = totalDeclaredProps.difference(totalUsedProps); + // TODO: Node 20 doesn't support Set.difference. Use it when minimum Node version is 22. + const unusedProps = [...totalDeclaredProps].filter((x) => !totalUsedProps.has(x)); + for (const unusedProp of unusedProps) { reportUnusedProp(context, services, unusedProp); } From 8ed58078502c0857c1f3009ad9a15aceb9677fe4 Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Sat, 26 Jul 2025 18:02:36 +0200 Subject: [PATCH 13/23] refactor to avoid false positives, detect more cases, add tests --- .../src/rules/no-unused-props.spec.ts | 65 +++++++++ .../src/rules/no-unused-props.ts | 138 +++++++++++------- 2 files changed, 149 insertions(+), 54 deletions(-) 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 index 50b61494ee..5eeb8714c4 100644 --- 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 @@ -280,6 +280,59 @@ ruleTesterWithTypes.run(RULE_NAME, rule, { 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, + }], }], valid: [ { @@ -451,6 +504,18 @@ ruleTesterWithTypes.run(RULE_NAME, rule, { } `, }, + { + // we can't track what happens to the rest object + code: tsx` + import { anyFunction } from "./anyFunction"; + + function Component(props: { abc: string; hello: string; }) { + const { abc, ...rest } = props; + anyFunction(rest); + return null; + } + `, + }, { // props used inside nested function code: tsx` diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.ts index 6f6df080d6..410119b511 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.ts @@ -49,9 +49,10 @@ export function create(context: RuleContext): RuleListener { const [props] = component.node.params; if (props == null) continue; - const usedPropKeys = collectUsedPropKeys(context, props); - if (usedPropKeys == null) { - // unable to determine prop keys, bail out to avoid false positives + const usedPropKeys = new Set(); + const couldFindAllUsedPropKeys = collectUsedPropKeysOfParameter(context, usedPropKeys, props); + if (!couldFindAllUsedPropKeys) { + // unable to determine all used prop keys => bail out to avoid false positives continue; } @@ -77,102 +78,131 @@ export function create(context: RuleContext): RuleListener { }; } -function collectUsedPropKeys(context: RuleContext, props: TSESTree.Parameter): Set | null { - switch (props.type) { +function collectUsedPropKeysOfParameter( + context: RuleContext, + usedPropKeys: Set, + parameter: TSESTree.Parameter, +): boolean { + switch (parameter.type) { case T.Identifier: { - return collectUsedPropKeysOfIdentifier(context, props); + return collectUsedPropKeysOfIdentifier(context, usedPropKeys, parameter); } case T.ObjectPattern: { - return collectUsedPropKeysOfObjectPattern(context, props); + return collectUsedPropKeysOfObjectPattern(context, usedPropKeys, parameter); } default: { - // unable to determine prop keys, bail out to avoid false positives - return null; + return false; } } } function collectUsedPropKeysOfObjectPattern( context: RuleContext, - props: TSESTree.ObjectPattern, -): Set | null { - const usedKeys = new Set(); - - for (const prop of props.properties) { - if (prop.type === T.Property) { - // Property - if (prop.key.type === T.Identifier) { - usedKeys.add(prop.key.name); - } else if (prop.key.type === T.Literal && typeof prop.key.value === "string") { - usedKeys.add(prop.key.value); - } - } else if (prop.argument.type === T.Identifier) { - // RestElement - const usedKeysOnRestElement = collectUsedPropKeysOfIdentifier(context, prop.argument); - if (usedKeysOnRestElement == null) { - // unable to determine prop keys, bail out to avoid false positives - return null; + usedPropKeys: Set, + objectPattern: TSESTree.ObjectPattern, +): boolean { + for (const property of objectPattern.properties) { + switch (property.type) { + case T.Property: { + const key = getKeyOfExpression(property.key); + if (key == null) return false; + usedPropKeys.add(key); + break; } - - for (const usedKeyOnRestElement of usedKeysOnRestElement) { - usedKeys.add(usedKeyOnRestElement); + case T.RestElement: { + if (!collectUsedPropsOfRestElement(context, usedPropKeys, property)) { + return false; + } + break; } } } - return usedKeys; + return true; } -function collectUsedPropKeysOfIdentifier( +function collectUsedPropsOfRestElement( context: RuleContext, - props: TSESTree.Identifier, -): Set | null { - const propsName = props.name; - const scope = context.sourceCode.getScope(props); - const variable = scope.variables.find((v) => v.name === propsName); + usedPropKeys: Set, + restElement: TSESTree.RestElement, +): boolean { + switch (restElement.argument.type) { + case T.Identifier: { + return collectUsedPropKeysOfIdentifier(context, usedPropKeys, restElement.argument); + } + default: { + return false; + } + } +} - if (variable == null) return null; +function collectUsedPropKeysOfIdentifier( + context: RuleContext, + usedPropKeys: Set, + identifier: TSESTree.Identifier, +): boolean { + const scope = context.sourceCode.getScope(identifier); + const variable = scope.variables.find((v) => v.name === identifier.name); + if (variable == null) return false; - const usedPropKeys = new Set(); for (const ref of variable.references) { + if (ref.identifier === identifier) { + continue; + } + const { parent } = ref.identifier; switch (parent.type) { case T.MemberExpression: { if ( parent.object.type === T.Identifier - && parent.object.name === propsName - && parent.property.type === T.Identifier + && parent.object.name === identifier.name ) { - usedPropKeys.add(parent.property.name); + const key = getKeyOfExpression(parent.property); + if (key == null) return false; + usedPropKeys.add(key); + } else { + return false; } break; } case T.VariableDeclarator: { if ( parent.id.type === T.ObjectPattern - && ref.identifier === parent.init + && parent.init === ref.identifier ) { - for (const prop of parent.id.properties) { - if ( - prop.type === T.Property - && prop.key.type === T.Identifier - ) { - usedPropKeys.add(prop.key.name); - } + if (!collectUsedPropKeysOfObjectPattern(context, usedPropKeys, parent.id)) { + return false; } + } else { + return false; } break; } default: { - // the whole props object is referenced in some way we probably can't track - // => unable to determine prop keys, bail out to avoid false positives - return null; + return false; + } + } + } + + return true; +} + +function getKeyOfExpression( + expression: TSESTree.Expression | TSESTree.PrivateIdentifier, +): string | null { + switch (expression.type) { + case T.Identifier: { + return expression.name; + } + case T.Literal: { + if (typeof expression.value === "string") { + return expression.value; } } } - return usedPropKeys; + return null; } function reportUnusedProp( From c52d0a28c78a863a988d161a2d1118b261707626 Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Sat, 26 Jul 2025 18:16:07 +0200 Subject: [PATCH 14/23] simplify code --- .../src/rules/no-unused-props.ts | 109 +++++++++--------- 1 file changed, 55 insertions(+), 54 deletions(-) diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.ts index 410119b511..7688741765 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.ts @@ -1,4 +1,5 @@ import type { RuleContext, RuleFeature } from "@eslint-react/kit"; +import type { Reference } from "@typescript-eslint/scope-manager"; import type { TSESTree } from "@typescript-eslint/types"; import type { RuleListener } from "@typescript-eslint/utils/ts-eslint"; import type { CamelCase } from "string-ts"; @@ -150,54 +151,59 @@ function collectUsedPropKeysOfIdentifier( continue; } - const { parent } = ref.identifier; - - switch (parent.type) { - case T.MemberExpression: { - if ( - parent.object.type === T.Identifier - && parent.object.name === identifier.name - ) { - const key = getKeyOfExpression(parent.property); - if (key == null) return false; - usedPropKeys.add(key); - } else { - return false; - } - break; - } - case T.VariableDeclarator: { - if ( - parent.id.type === T.ObjectPattern - && parent.init === ref.identifier - ) { - if (!collectUsedPropKeysOfObjectPattern(context, usedPropKeys, parent.id)) { - return false; - } - } else { - return false; - } - break; + if (!collectUsedPropKeysOfReference(context, usedPropKeys, identifier, ref)) { + return false; + } + } + + return true; +} + +function collectUsedPropKeysOfReference( + context: RuleContext, + usedPropKeys: Set, + identifier: TSESTree.Identifier, + ref: Reference, +): boolean { + const { parent } = ref.identifier; + + switch (parent.type) { + case T.MemberExpression: { + if ( + parent.object.type === T.Identifier + && parent.object.name === identifier.name + ) { + const key = getKeyOfExpression(parent.property); + if (key == null) return false; + usedPropKeys.add(key); + return true; } - default: { - return false; + break; + } + case T.VariableDeclarator: { + if ( + parent.id.type === T.ObjectPattern + && parent.init === ref.identifier + ) { + return collectUsedPropKeysOfObjectPattern(context, usedPropKeys, parent.id); } + break; } } - return true; + return false; } function getKeyOfExpression( - expression: TSESTree.Expression | TSESTree.PrivateIdentifier, + expr: TSESTree.Expression | TSESTree.PrivateIdentifier, ): string | null { - switch (expression.type) { + switch (expr.type) { case T.Identifier: { - return expression.name; + return expr.name; } case T.Literal: { - if (typeof expression.value === "string") { - return expression.value; + if (typeof expr.value === "string") { + return expr.value; } } } @@ -210,22 +216,17 @@ function reportUnusedProp( services: ParserServicesWithTypeInformation, prop: ts.Symbol, ) { - const declarations = prop.getDeclarations(); - if (declarations != null) { - const decl = declarations[0]; - if (decl == null) return; - const node = services.tsNodeToESTreeNodeMap.get(decl); - - const nodeKey = node.type === T.TSPropertySignature - || node.type === T.PropertyDefinition - || node.type === T.Property - ? node.key - : node; - - context.report({ - messageId: "noUnusedProps", - node: nodeKey, - data: { name: prop.name }, - }); - } + const declaration = prop.getDeclarations()?.[0]; + if (declaration == null) return; + + const node = services.tsNodeToESTreeNodeMap.get(declaration); + const keyNode = node.type === T.TSPropertySignature + ? node.key + : node; + + context.report({ + messageId: "noUnusedProps", + node: keyNode, + data: { name: prop.name }, + }); } From f7032d93b07859ce3e0108d16bccc88860c11179 Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Sat, 26 Jul 2025 18:30:15 +0200 Subject: [PATCH 15/23] two more tests --- .../src/rules/no-unused-props.spec.ts | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) 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 index 5eeb8714c4..9aaa3578dc 100644 --- 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 @@ -333,6 +333,40 @@ ruleTesterWithTypes.run(RULE_NAME, rule, { 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, + }], }], valid: [ { From 3ee1ae9f44a61e41f540422f938fdcf0c37862e1 Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Sat, 26 Jul 2025 19:09:14 +0200 Subject: [PATCH 16/23] fix crash if declaration is in another file --- .../src/rules/no-unused-props.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.ts index 7688741765..f1ef69f1a2 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-props.ts @@ -219,14 +219,18 @@ function reportUnusedProp( const declaration = prop.getDeclarations()?.[0]; if (declaration == null) return; - const node = services.tsNodeToESTreeNodeMap.get(declaration); - const keyNode = node.type === T.TSPropertySignature - ? node.key - : node; + const declarationNode = services.tsNodeToESTreeNodeMap.get(declaration); + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (declarationNode == null) return; // is undefined if declaration is in a different file + + const nodeToReport = declarationNode.type === T.TSPropertySignature + ? declarationNode.key + : declarationNode; context.report({ messageId: "noUnusedProps", - node: keyNode, + node: nodeToReport, data: { name: prop.name }, }); } From 7891387b32de9fa683d1571ffd327409f71aeeb9 Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Sat, 26 Jul 2025 19:13:51 +0200 Subject: [PATCH 17/23] add a second sentence to the description --- .../plugins/eslint-plugin-react-x/src/rules/no-unused-props.md | 2 ++ 1 file changed, 2 insertions(+) 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 index f064185faf..729f18aeb9 100644 --- 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 @@ -18,6 +18,8 @@ react-x/no-unused-props 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. + ## Examples ### Failing From 68c0d8fd523dbd690993f1bd40efb71fe7b41636 Mon Sep 17 00:00:00 2001 From: Ulrich Stark Date: Mon, 28 Jul 2025 19:47:00 +0200 Subject: [PATCH 18/23] some final hopefully correct docs changes --- apps/website/content/docs/migration.mdx | 2 +- apps/website/content/docs/rules/meta.json | 1 + .../eslint-plugin-react-x/src/rules/no-unused-props.md | 7 +++++++ 3 files changed, 9 insertions(+), 1 deletion(-) 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/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 index 729f18aeb9..e9c035edd0 100644 --- 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 @@ -20,6 +20,8 @@ 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). + ## Examples ### Failing @@ -54,3 +56,8 @@ function Component(props: Props) { - [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` From d22953027d3981525a31d9b9b5f872ea23734d09 Mon Sep 17 00:00:00 2001 From: REL1CX Date: Fri, 1 Aug 2025 05:11:03 +0800 Subject: [PATCH 19/23] test: add test cases related to `PropsWithChildren` and `forwardRef` Add some test cases that are closer to real-world scenarios Signed-off-by: REL1CX --- .../src/rules/no-unused-props.spec.ts | 1096 ++++++++++------- 1 file changed, 657 insertions(+), 439 deletions(-) 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 index 9aaa3578dc..131729e6b0 100644 --- 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 @@ -4,373 +4,9 @@ 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, - }], - }], - valid: [ + invalid: [ { - // all props are used + // interface type and later destructuring code: tsx` interface Props { abc: string; @@ -378,26 +14,46 @@ ruleTesterWithTypes.run(RULE_NAME, rule, { } function Component(props: Props) { - const { abc, hello } = props; + const { abc } = props; return null; } `, + errors: [{ + messageId: "noUnusedProps", + column: 3, + data: { + name: "hello", + }, + endColumn: 8, + endLine: 3, + line: 3, + }], }, { - // all props are used + // interface type and direct destructuring code: tsx` interface Props { abc: string; hello: string; } - function Component({ abc, hello }: Props) { + function Component({ abc }: Props) { return null; } `, + errors: [{ + messageId: "noUnusedProps", + column: 3, + data: { + name: "hello", + }, + endColumn: 8, + endLine: 3, + line: 3, + }], }, { - // all props are used + // named type and later destructuring code: tsx` type Props = { abc: string; @@ -405,173 +61,735 @@ ruleTesterWithTypes.run(RULE_NAME, rule, { } function Component(props: Props) { - const { abc, hello } = props; + const { abc } = props; return null; } `, + errors: [{ + messageId: "noUnusedProps", + column: 3, + data: { + name: "hello", + }, + endColumn: 8, + endLine: 3, + line: 3, + }], }, { - // all props are used + // interface type and direct destructuring code: tsx` type Props = { abc: string; hello: string; } - function Component({ abc, hello }: Props) { + function Component({ abc }: Props) { return null; } `, + errors: [{ + messageId: "noUnusedProps", + column: 3, + data: { + name: "hello", + }, + endColumn: 8, + endLine: 3, + line: 3, + }], }, { - // all props are used + // inline type and later destructuring code: tsx` function Component(props: { abc: string; hello: string; }) { - const { abc, hello } = props; + const { abc } = props; return null; } `, + errors: [{ + messageId: "noUnusedProps", + column: 42, + data: { + name: "hello", + }, + endColumn: 47, + endLine: 1, + line: 1, + }], }, { - // all props are used + // inline type and direct destructuring code: tsx` - function Component({ abc, hello }: { abc: string; hello: string; }) { + function Component({ abc }: { abc: string; hello: string; }) { return null; } `, + errors: [{ + messageId: "noUnusedProps", + column: 44, + data: { + name: "hello", + }, + endColumn: 49, + endLine: 1, + line: 1, + }], }, { - // all props are used + // multiple properties unused code: tsx` - function Component({ abc: abc2, hello: hello2 }: { abc: string; hello: string; }) { + 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, + }], }, { - // props are used by two components each accessing one prop + // interface augmentation code: tsx` interface Props { + used1: string; abc: string; - hello: string; } - function Component({ abc }: Props) { - return null; + interface Props { + used2: string; + hello: string; } - function Component2({ hello }: Props) { + 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, + }], }, { - // we can't track what happens to the props object + // interface union code: tsx` - import { Component2 } from "./component2"; - - interface Props { + interface Props1 { + used1: string; abc: string; + } + + interface Props2 { + used2: string; hello: string; } - function Component(props: Props) { - return ; + 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, + }], }, { - // we can't track what happens to the props object + // interface extends code: tsx` - import { anyFunction } from "./anyFunction"; - - interface Props { + interface PropsBase { + used1: string; abc: string; - hello: string; } - function Component(props: Props) { - anyFunction(props); + 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, + }], }, { - // we can't track what happens to the props object + // track uses of properties on rest element code: tsx` - import { anyFunction } from "./anyFunction"; - - interface Props { - abc: string; - hello: string; + function Component({ ...rest }: { abc: string; hello: string; }) { + return
{rest.abc}
; } - - function Component(props: Props) { - anyFunction({ props }); - - return null; + `, + 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, + }], }, { - // one value used in jsx, the other in effect + // track assignment code: tsx` - import { useEffect } from "react"; - - function Component({ abc, hello }: { abc: string; hello: string }) { - useEffect(() => { - console.log(hello); - }, []); + 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, + }], }, { - // we can't track what happens to the rest object + // track computed member access code: tsx` - import { anyFunction } from "./anyFunction"; - - function Component({ abc, ...rest }: { abc: string; hello: string }) { - anyFunction(rest); - return null; + function Component(props: { abc: string; hello: string; }) { + return
{props["abc"]}
; } `, + errors: [{ + messageId: "noUnusedProps", + column: 42, + data: { + name: "hello", + }, + endColumn: 47, + endLine: 1, + line: 1, + }], }, { - // we can't track what happens to the rest object + // correct error span on complex prop type code: tsx` - import { anyFunction } from "./anyFunction"; - - function Component(props: { abc: string; hello: string; }) { - const { abc, ...rest } = props; - anyFunction(rest); + 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, + }], }, { - // props used inside nested function + // access of sub property should mark property as used code: tsx` - function Component(props: { abc: string; hello: string }) { - function inner() { - return props.hello; - } - return props.abc; + 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, + }], }, { - // props used conditionally + // expect no false negatives when using PropsWithChildren code: tsx` - function Component(props: { abc: string; hello: string }) { - if (Math.random() > 0.5) { - return
{props.abc}
; - } - return
{props.hello}
; + 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 ( +