Skip to content

Commit 5894c57

Browse files
authored
feat: add jsx-key-before-spread, closes #1093, closes #1087 (#1105)
1 parent 32e2a75 commit 5894c57

File tree

9 files changed

+173
-0
lines changed

9 files changed

+173
-0
lines changed

apps/website/content/docs/rules/meta.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"pages": [
33
"overview",
44
"---X Rules---",
5+
"jsx-key-before-spread",
56
"jsx-no-duplicate-props",
67
"jsx-no-undef",
78
"jsx-uses-react",

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ The `jsx-*` rules check for issues exclusive to JSX syntax, which are absent fro
3131

3232
| Rule || 🌟 | Description | `react` |
3333
| :----------------------------------------------------------------------------------- | :-- | :-------: | :-------------------------------------------------------------------------------------------------- | :------: |
34+
| [`jsx-key-before-spread`](./jsx-key-before-spread) | 1️⃣ | | Enforces that the `key` attribute is placed before the spread attribute in JSX elements | |
3435
| [`jsx-no-duplicate-props`](./jsx-no-duplicate-props) | 1️⃣ | | Disallow duplicate props in JSX elements | |
3536
| [`jsx-no-undef`](./jsx-no-undef) | 0️⃣ | | Disallow undefined variables in JSX elements | |
3637
| [`jsx-uses-react`](./jsx-uses-react) | 1️⃣ | | Marks React variables as used when JSX is used | |

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { DEFAULT_ESLINT_REACT_SETTINGS } from "@eslint-react/shared";
44
export const name = "react-x/recommended";
55

66
export const rules = {
7+
"react-x/jsx-key-before-spread": "warn",
78
"react-x/jsx-no-duplicate-props": "warn",
89
"react-x/jsx-uses-react": "warn",
910
"react-x/jsx-uses-vars": "warn",

packages/plugins/eslint-plugin-react-x/src/plugin.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { name, version } from "../package.json";
22
import avoidShorthandBoolean from "./rules/avoid-shorthand-boolean";
33
import avoidShorthandFragment from "./rules/avoid-shorthand-fragment";
4+
import jsxKeyBeforeSpread from "./rules/jsx-key-before-spread";
45
import jsxNoDuplicateProps from "./rules/jsx-no-duplicate-props";
56
import jsxNoUndef from "./rules/jsx-no-undef";
67
import jsxUsesReact from "./rules/jsx-uses-react";
@@ -116,6 +117,7 @@ export const plugin = {
116117
"prefer-shorthand-fragment": preferShorthandFragment,
117118

118119
// Part: JSX only rules
120+
"jsx-key-before-spread": jsxKeyBeforeSpread,
119121
"jsx-no-duplicate-props": jsxNoDuplicateProps,
120122
"jsx-no-undef": jsxNoUndef,
121123
"jsx-uses-react": jsxUsesReact,
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
title: jsx-key-before-spread
3+
---
4+
5+
**Full Name in `eslint-plugin-react-x`**
6+
7+
```sh copy
8+
react-x/jsx-key-before-spread
9+
```
10+
11+
**Full Name in `@eslint-react/eslint-plugin`**
12+
13+
```sh copy
14+
@eslint-react/jsx-key-before-spread
15+
```
16+
17+
**Presets**
18+
19+
- `x`
20+
- `recommended`
21+
- `recommended-typescript`
22+
- `recommended-type-checked`
23+
24+
## Description
25+
26+
Enforces that the `key` attribute is placed before the spread attribute in JSX elements.
27+
28+
When using the JSX automatic runtime, `key` is a special attribute in the JSX transform. See the [Babel repl](https://babeljs.io/repl#?browsers=last%202%20chrome%20versions&build=&builtIns=false&corejs=3.21&spec=false&loose=false&code_lz=DwEwlgbgBA1gpgTwLwCICMKoG8B0eAOATgPb4DOAvlAPQB8A3AFCiTZ45GmWyKoBMmOkxbR4yFAGZMAYwA2AQzJkAcvIC2cVIIbNw0OYpXrNKTGNRSaDIA&forceAllTransforms=false&modules=false&shippedProposals=false&evaluate=true&fileSize=false&timeTravel=false&sourceType=module&lineWrap=true&presets=react&prettier=false&targets=&version=7.27.0&externalPlugins=&assumptions=%7B%7D) and [TypeScript playground](https://www.typescriptlang.org/play/?target=99&jsx=4#code/DwEwlgbgBA1gpgTwLwCICMKoG8B0eAOATgPb4DOAvlAPQB8A3ALABQok2eORplsiqAJkx0mrcNHjIUAZkwBjADYBDMmQBySgLZxUwhizbRFK9Vp0pMk1ABY99IA)
29+
30+
If the `key` prop is _before_ any spread props, it is passed as the `key` argument of the `_jsx` / `_jsxs` / `_jsxDev` function. But if the `key` prop is _after_ spread props, The compiler uses `createElement` instead and passes `key` as a regular prop.
31+
32+
## Examples
33+
34+
### Failing
35+
36+
```tsx
37+
<div {...props} key="2" />;
38+
```
39+
40+
### Passing
41+
42+
```tsx
43+
<div key="1" {...props} />;
44+
<div key="3" className="" />;
45+
<div className="" key="3" />;
46+
```
47+
48+
## Implementation
49+
50+
- [Rule source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/jsx-key-before-spread.ts)
51+
- [Test source](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x/src/rules/jsx-key-before-spread.spec.ts)
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import tsx from "dedent";
2+
3+
import { allValid, ruleTester } from "../../../../../test";
4+
import rule, { RULE_NAME } from "./jsx-key-before-spread";
5+
6+
ruleTester.run(RULE_NAME, rule, {
7+
invalid: [
8+
{
9+
code: tsx`
10+
const App = (props) => {
11+
return [
12+
<div {...props} key="1">1</div>,
13+
<div {...props} key="1">2</div>,
14+
<div {...props} key="1">3</div>,
15+
]
16+
};
17+
`,
18+
errors: [
19+
{ messageId: "jsxKeyBeforeSpread" },
20+
{ messageId: "jsxKeyBeforeSpread" },
21+
{ messageId: "jsxKeyBeforeSpread" },
22+
],
23+
},
24+
{
25+
code: tsx`
26+
27+
const App = (props) => {
28+
return [
29+
<div {...props} key="1">1</div>,
30+
<div {...props} key="1">2</div>,
31+
<div {...props} key="1">3</div>,
32+
]
33+
};
34+
`,
35+
errors: [
36+
{ messageId: "jsxKeyBeforeSpread" },
37+
{ messageId: "jsxKeyBeforeSpread" },
38+
{ messageId: "jsxKeyBeforeSpread" },
39+
],
40+
},
41+
],
42+
valid: [
43+
...allValid,
44+
tsx`
45+
const App = (props) => {
46+
return [<div key="1">1</div>]
47+
};
48+
`,
49+
tsx`
50+
const App = (props) => {
51+
return [
52+
<div key="1" {...props}>1</div>,
53+
<div key="2" {...props}>2</div>,
54+
<div key="3" {...props}>3</div>,
55+
]
56+
};
57+
`,
58+
tsx`
59+
const App = (props) => {
60+
return [1, 2, 3].map((item) => <div key={Math.random()}>{item}</div>)
61+
};
62+
`,
63+
],
64+
});
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import type { RuleContext, RuleFeature } from "@eslint-react/kit";
2+
import type { RuleListener } from "@typescript-eslint/utils/ts-eslint";
3+
import type { CamelCase } from "string-ts";
4+
5+
import { AST_NODE_TYPES as T } from "@typescript-eslint/types";
6+
import { createRule } from "../utils";
7+
8+
export const RULE_NAME = "jsx-key-before-spread";
9+
10+
export const RULE_FEATURES = [
11+
"EXP",
12+
] as const satisfies RuleFeature[];
13+
14+
export type MessageID = CamelCase<typeof RULE_NAME>;
15+
16+
export default createRule<[], MessageID>({
17+
meta: {
18+
type: "problem",
19+
docs: {
20+
description: "Enforces that the 'key' attribute is placed before the spread attribute in JSX elements.",
21+
[Symbol.for("rule_features")]: RULE_FEATURES,
22+
},
23+
messages: {
24+
jsxKeyBeforeSpread: "The 'key' attribute must be placed before the spread attribute.",
25+
},
26+
schema: [],
27+
},
28+
name: RULE_NAME,
29+
create,
30+
defaultOptions: [],
31+
});
32+
33+
export function create(context: RuleContext<MessageID, []>): RuleListener {
34+
return {
35+
JSXOpeningElement(node) {
36+
let firstSpreadAttributeIndex: null | number = null;
37+
for (const [index, attr] of node.attributes.entries()) {
38+
if (attr.type === T.JSXSpreadAttribute) {
39+
firstSpreadAttributeIndex ??= index;
40+
continue;
41+
}
42+
if (attr.name.name === "key" && firstSpreadAttributeIndex != null && index > firstSpreadAttributeIndex) {
43+
context.report({
44+
messageId: "jsxKeyBeforeSpread",
45+
node: attr,
46+
});
47+
}
48+
}
49+
},
50+
};
51+
}

packages/plugins/eslint-plugin/src/configs/all.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export const name = "@eslint-react/all";
1212
export const rules = {
1313
"@eslint-react/avoid-shorthand-boolean": "warn",
1414
"@eslint-react/avoid-shorthand-fragment": "warn",
15+
"@eslint-react/jsx-key-before-spread": "warn",
1516
"@eslint-react/jsx-no-duplicate-props": "warn",
1617
"@eslint-react/jsx-no-undef": "error",
1718
"@eslint-react/jsx-uses-react": "warn",

packages/plugins/eslint-plugin/src/configs/x.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import react from "eslint-plugin-react-x";
55
export const name = "@eslint-react/x";
66

77
export const rules = {
8+
"@eslint-react/jsx-key-before-spread": "warn",
89
"@eslint-react/jsx-no-duplicate-props": "warn",
910
"@eslint-react/jsx-uses-react": "warn",
1011
"@eslint-react/jsx-uses-vars": "warn",

0 commit comments

Comments
 (0)