Skip to content

Commit 423d8ee

Browse files
Rel1cxCopilotCopilot
authored
Refactor no-implicit-key rule to use type checking (#1466)
Signed-off-by: REL1CX <solarflamex@hotmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
1 parent c6894f7 commit 423d8ee

15 files changed

Lines changed: 51 additions & 62 deletions

apps/website/content/docs/rules/overview.mdx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ full: true
5555
| [`no-direct-mutation-state`](no-direct-mutation-state) | 2️⃣ 2️⃣ | | Disallows direct mutation of `this.state` | |
5656
| [`no-duplicate-key`](no-duplicate-key) | 0️⃣ 0️⃣ | `🧪` | Prevents duplicate `key` props on sibling elements when rendering lists | |
5757
| [`no-forward-ref`](no-forward-ref) | 1️⃣ 1️⃣ | `🔄` | Replaces usage of `forwardRef` with passing `ref` as a prop | >=19.0.0 |
58-
| [`no-implicit-key`](no-implicit-key) | 1️⃣ 1️⃣ | `🧪` | Prevents `key` from not being explicitly specified (e.g., spreading `key` from objects) | |
58+
| [`no-implicit-key`](no-implicit-key) | 0️⃣ 0️⃣ | `💭` `🧪` | Prevents implicitly passing the 'key' prop to components | |
5959
| [`no-leaked-conditional-rendering`](no-leaked-conditional-rendering) | 0️⃣ 0️⃣ | `💭` | Prevents problematic leaked values from being rendered | |
6060
| [`no-missing-component-display-name`](no-missing-component-display-name) | 0️⃣ 0️⃣ | | Enforces that all components have a `displayName` that can be used in DevTools | |
6161
| [`no-missing-context-display-name`](no-missing-context-display-name) | 0️⃣ 0️⃣ | `🔧` | Enforces that all contexts have a `displayName` that can be used in DevTools | |
@@ -121,9 +121,9 @@ full: true
121121
RSC rules target [React Server Components](https://react.dev/reference/rsc/server-components), [React Server Functions](https://react.dev/reference/rsc/server-functions) and RSC [Directives](https://react.dev/reference/rsc/directives).
122122
</Callout>
123123

124-
| Rule || 🌟 | Description | `react` |
125-
| :--------------------------------------------------------------------------------- | :-----: | :-------: | :---------------------------------------------------------------------------------------------------------------------------- | :------: |
126-
| [`function-definition`](rsc-function-definition) | 0️⃣ 0️⃣ | `🔧` `🧪` | Validate and transform React Client/Server Function definitions | >=19.0.0 |
124+
| Rule || 🌟 | Description | `react` |
125+
| :----------------------------------------------- | :-----: | :-------: | :-------------------------------------------------------------- | :------: |
126+
| [`function-definition`](rsc-function-definition) | 0️⃣ 0️⃣ | `🔧` `🧪` | Validate and transform React Client/Server Function definitions | >=19.0.0 |
127127

128128
## Web API Rules
129129

packages/plugins/eslint-plugin-react-x/src/configs/recommended-type-checked.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export const name = "react-x/recommended-type-checked";
66

77
export const rules = {
88
...recommendedTypescript.rules,
9+
"react-x/no-implicit-key": "error",
910
"react-x/no-leaked-conditional-rendering": "error",
1011
} as const satisfies Record<string, RuleConfig>;
1112

packages/plugins/eslint-plugin-react-x/src/configs/strict-type-checked.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export const name = "react-x/strict-type-checked";
66

77
export const rules = {
88
...strictTypeScript.rules,
9+
"react-x/no-implicit-key": "error",
910
"react-x/no-leaked-conditional-rendering": "error",
1011
"react-x/no-unused-props": "warn",
1112
} as const satisfies Record<string, RuleConfig>;

packages/plugins/eslint-plugin-react-x/src/rules/jsx-key-before-spread.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,4 @@ If the `key` prop is before any spread props, it is passed as the `key` argument
6969
## See Also
7070

7171
- [`no-implicit-key`](./no-implicit-key)\
72-
Prevents `key` from not being explicitly specified (e.g., spreading `key` from objects).
72+
Prevents implicitly passing the 'key' prop to components.

packages/plugins/eslint-plugin-react-x/src/rules/no-array-index-key.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,4 @@ function MyComponent({ items }: MyComponentProps) {
9090
- [`no-duplicate-key`](./no-duplicate-key)\
9191
Prevents duplicate `key` props on sibling elements when rendering lists.
9292
- [`no-implicit-key`](./no-implicit-key)\
93-
Prevents `key` from not being explicitly specified (e.g., spreading `key` from objects).
93+
Prevents implicitly passing the 'key' prop to components.

packages/plugins/eslint-plugin-react-x/src/rules/no-duplicate-key.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ function MyComponent() {
126126
- [`no-missing-key`](./no-missing-key)\
127127
Prevents missing `key` on items in list rendering.
128128
- [`no-implicit-key`](./no-implicit-key)\
129-
Prevents `key` from not being explicitly specified (e.g., spreading `key` from objects).
129+
Prevents implicitly passing the 'key' prop to components.
130130
- [`no-array-index-key`](./no-array-index-key)\
131131
Warns when an array `index` is used as a `key` prop.
132132
- [`no-unnecessary-key`](./no-unnecessary-key)\

packages/plugins/eslint-plugin-react-x/src/rules/no-implicit-key.mdx

Lines changed: 18 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,16 @@ react-x/no-implicit-key
1616

1717
**Features**
1818

19-
`🧪`
19+
`💭` `🧪`
2020

2121
**Presets**
2222

23-
`x`
24-
`recommended`
25-
`recommended-typescript`
2623
`recommended-type-checked`
27-
`strict`
28-
`strict-typescript`
2924
`strict-type-checked`
3025

3126
## Description
3227

33-
Prevents `key` from not being explicitly specified (e.g., spreading `key` from objects).
28+
Prevents implicitly passing the 'key' prop to components.
3429

3530
This makes it hard to see whether the key was passed correctly to the element or where it came from.
3631

@@ -43,21 +38,17 @@ It is also proposed to be deprecated in this RFC: [Deprecate spreading key from
4338
```tsx
4439
import React from "react";
4540

46-
interface MyComponentProps {
47-
items: { id: string; name: string }[];
41+
interface Foo = { key?: string; }
42+
43+
interface MyComponentProps extends Foo {
44+
className: string;
45+
children: React.ReactNode;
4846
}
4947

50-
function MyComponent({ items }: MyComponentProps) {
51-
return (
52-
<ul>
53-
{items.map(({ id, name, ...rest }) => {
54-
const props = { key: id, ...rest };
55-
return <li {...props}>{name}</li>;
56-
// ^^^^^^^^^^
57-
// - Do not use implicit 'key' props.
58-
})}
59-
</ul>
60-
);
48+
function MyComponent(props: MyComponentProps) {
49+
return <div {...props} />;
50+
// ^^^^^^^^^^
51+
// - This spread attribute implicitly passes the 'key' prop to a component, this could lead to unexpected behavior. If you intend to pass the 'key' prop, use 'key={value}'.
6152
}
6253
```
6354

@@ -66,18 +57,15 @@ function MyComponent({ items }: MyComponentProps) {
6657
```tsx
6758
import React from "react";
6859

69-
interface MyComponentProps {
70-
items: { id: string; name: string }[];
60+
interface Foo = { key?: string; }
61+
62+
interface MyComponentProps extends Foo {
63+
className: string;
64+
children: React.ReactNode;
7165
}
7266

73-
function MyComponent({ items }: MyComponentProps) {
74-
return (
75-
<ul>
76-
{items.map(({ id, name, ...rest }) => {
77-
return <li key={id} {...rest}>{name}</li>;
78-
})}
79-
</ul>
80-
);
67+
function MyComponent({ key, ...rest }) {
68+
return <div {...rest} />;
8169
}
8270
```
8371

packages/plugins/eslint-plugin-react-x/src/rules/no-implicit-key.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import tsx from "dedent";
22

3-
import { allValid, ruleTester } from "../../../../../test";
3+
import { allValid, ruleTesterWithTypes } from "../../../../../test";
44
import rule, { RULE_NAME } from "./no-implicit-key";
55

6-
ruleTester.run(RULE_NAME, rule, {
6+
ruleTesterWithTypes.run(RULE_NAME, rule, {
77
invalid: [
88
{
99
code: tsx`

packages/plugins/eslint-plugin-react-x/src/rules/no-implicit-key.ts

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1-
import * as core from "@eslint-react/core";
21
import type { RuleContext, RuleFeature } from "@eslint-react/shared";
3-
import type { TSESTree } from "@typescript-eslint/types";
4-
import { AST_NODE_TYPES as AST } from "@typescript-eslint/types";
2+
import { getConstrainedTypeAtLocation } from "@typescript-eslint/type-utils";
3+
import { ESLintUtils } from "@typescript-eslint/utils";
54
import type { RuleListener } from "@typescript-eslint/utils/ts-eslint";
5+
import { unionConstituents } from "ts-api-utils";
66

77
import { createRule } from "../utils";
88

99
export const RULE_NAME = "no-implicit-key";
1010

1111
export const RULE_FEATURES = [
12+
"TSC",
1213
"EXP",
1314
] as const satisfies RuleFeature[];
1415

@@ -18,10 +19,11 @@ export default createRule<[], MessageID>({
1819
meta: {
1920
type: "problem",
2021
docs: {
21-
description: "Prevents 'key' from not being explicitly specified (e.g., spreading 'key' from objects).",
22+
description: "Prevents implicitly passing the 'key' prop to components.",
2223
},
2324
messages: {
24-
default: "Do not use implicit 'key' props.",
25+
default:
26+
"This spread attribute implicitly passes the 'key' prop to a component, this could lead to unexpected behavior. If you intend to pass the 'key' prop, use 'key={value}'.",
2527
},
2628
schema: [],
2729
},
@@ -30,23 +32,18 @@ export default createRule<[], MessageID>({
3032
defaultOptions: [],
3133
});
3234

33-
// TODO: Rewrite the rule to use type checking
3435
export function create(context: RuleContext<MessageID, []>): RuleListener {
36+
const services = ESLintUtils.getParserServices(context, false);
3537
return {
36-
JSXOpeningElement(node: TSESTree.JSXOpeningElement) {
37-
// Find the 'key' prop, including those from spread attributes
38-
const keyProp = core.getJsxAttribute(context, node.parent)("key");
39-
// Check if the 'key' prop is explicitly defined on the element
40-
const isKeyPropOnElement = node.attributes
41-
.some((n) =>
42-
n.type === AST.JSXAttribute
43-
&& n.name.type === AST.JSXIdentifier
44-
&& n.name.name === "key"
45-
);
46-
// If a 'key' prop exists but is not explicitly on the element, it's implicit
47-
if (keyProp != null && !isKeyPropOnElement) {
48-
// Report an error for the implicit 'key'
49-
context.report({ messageId: "default", node: keyProp });
38+
JSXSpreadAttribute(node) {
39+
for (const type of unionConstituents(getConstrainedTypeAtLocation(services, node.argument))) {
40+
if (type.getProperty("key") != null) {
41+
context.report({
42+
messageId: "default",
43+
node,
44+
});
45+
break;
46+
}
5047
}
5148
},
5249
};

packages/plugins/eslint-plugin-react-x/src/rules/no-missing-key.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,6 @@ function MyComponent({ items }: MyComponentProps) {
8484
- [`no-duplicate-key`](./no-duplicate-key)\
8585
Prevents duplicate `key` props on sibling elements when rendering lists.
8686
- [`no-implicit-key`](./no-implicit-key)\
87-
Prevents `key` from not being explicitly specified (e.g., spreading `key` from objects).
87+
Prevents implicitly passing the 'key' prop to components.
8888
- [`no-array-index-key`](./no-array-index-key)\
8989
Warns when an array `index` is used as a `key` prop.

0 commit comments

Comments
 (0)