Skip to content

Commit 080c590

Browse files
authored
feat(plugins/x): add 'jsx-no-duplicate-props' (#851)
1 parent 469e624 commit 080c590

File tree

8 files changed

+109
-0
lines changed

8 files changed

+109
-0
lines changed

packages/plugins/eslint-plugin-react-x/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export default [
3030
rules: {
3131
// react-x recommended rules
3232
"react-x/ensure-forward-ref-using-ref": "warn",
33+
"react-x/jsx-no-duplicate-props": "warn",
3334
"react-x/jsx-uses-vars": "warn",
3435
"react-x/no-access-state-in-setstate": "error",
3536
"react-x/no-array-index-key": "warn",
@@ -75,6 +76,7 @@ export default [
7576
| `avoid-shorthand-boolean` | Prevents using shorthand syntax for boolean attributes. | 🎨 | | |
7677
| `avoid-shorthand-fragment` | Prevents using shorthand syntax for fragments. | 🎨 | | |
7778
| `ensure-forward-ref-using-ref` | Requires that components wrapped with `forwardRef` must have a `ref` parameter. | ✔️ | | |
79+
| `jsx-no-duplicate-props` | Prevents duplicate props in JSX. | ✔️ | | |
7880
| `jsx-uses-vars` | Prevents variables used in JSX to be marked as unused. | ✔️ | | |
7981
| `no-access-state-in-setstate` | Prevents accessing `this.state` inside `setState` calls. | ✔️ | | |
8082
| `no-array-index-key` | Prevents using array `index` as `key`. | 🧐 | | |

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { name, version } from "../package.json";
55
import avoidShorthandBoolean from "./rules/avoid-shorthand-boolean";
66
import avoidShorthandFragment from "./rules/avoid-shorthand-fragment";
77
import forwardRefUsingRef from "./rules/ensure-forward-ref-using-ref";
8+
import jsxNoDuplicateProps from "./rules/jsx-no-duplicate-props";
89
import jsxUsesVars from "./rules/jsx-uses-vars";
910
import noAccessStateInSetstate from "./rules/no-access-state-in-setstate";
1011
import noArrayIndexKey from "./rules/no-array-index-key";
@@ -71,6 +72,7 @@ export default {
7172
"avoid-shorthand-boolean": avoidShorthandBoolean,
7273
"avoid-shorthand-fragment": avoidShorthandFragment,
7374
"ensure-forward-ref-using-ref": forwardRefUsingRef,
75+
"jsx-no-duplicate-props": jsxNoDuplicateProps,
7476
"jsx-uses-vars": jsxUsesVars,
7577
"no-access-state-in-setstate": noAccessStateInSetstate,
7678
"no-array-index-key": noArrayIndexKey,
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { allValid, ruleTester } from "../../../../../test";
2+
import rule, { RULE_NAME } from "./jsx-no-duplicate-props";
3+
4+
ruleTester.run(RULE_NAME, rule, {
5+
invalid: [
6+
{
7+
code: /* tsx */ `<div a="1" a="2" />;`,
8+
errors: [{ messageId: "jsxNoDuplicateProps" }],
9+
},
10+
{
11+
code: /* tsx */ `<div a="1" b="2" a="3" />;`,
12+
errors: [{ messageId: "jsxNoDuplicateProps" }],
13+
},
14+
{
15+
code: /* tsx */ `<div a="1" {...b} a="2" />;`,
16+
errors: [{ messageId: "jsxNoDuplicateProps" }],
17+
},
18+
{
19+
code: /* tsx */ `<div a="1" {...a} {...b} a="2" />;`,
20+
errors: [{ messageId: "jsxNoDuplicateProps" }],
21+
},
22+
],
23+
valid: [
24+
...allValid,
25+
/* tsx */ `const a = <div a="1" aa="2" />;`,
26+
/* tsx */ `const a = <div a="1" aa="2"><span a="1" aa="2" /></div>;`,
27+
/* tsx */ `const a = <div a="1" b="2" />;`,
28+
/* tsx */ `const a = <div a="1" {...b} />;`,
29+
/* tsx */ `const a = <div {...a} {...b} />;`,
30+
],
31+
});
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { isString } from "@eslint-react/tools";
2+
import { AST_NODE_TYPES } from "@typescript-eslint/types";
3+
import type { CamelCase } from "string-ts";
4+
5+
import { createRule } from "../utils";
6+
7+
export const RULE_NAME = "jsx-no-duplicate-props";
8+
9+
export type MessageID = CamelCase<typeof RULE_NAME>;
10+
11+
export default createRule<[], MessageID>({
12+
meta: {
13+
type: "problem",
14+
docs: {
15+
description: "disallow duplicate props",
16+
},
17+
messages: {
18+
jsxNoDuplicateProps: "Duplicate prop '{{name}}'",
19+
},
20+
schema: [],
21+
},
22+
name: RULE_NAME,
23+
create(context) {
24+
return {
25+
JSXOpeningElement(node) {
26+
const props: string[] = [];
27+
for (const attr of node.attributes) {
28+
if (attr.type === AST_NODE_TYPES.JSXSpreadAttribute) continue;
29+
const name = attr.name.name;
30+
if (!isString(name)) continue;
31+
if (!props.includes(name)) {
32+
props.push(name);
33+
continue;
34+
}
35+
context.report({
36+
messageId: "jsxNoDuplicateProps",
37+
node: attr,
38+
data: { name },
39+
});
40+
}
41+
},
42+
};
43+
},
44+
defaultOptions: [],
45+
});

packages/plugins/eslint-plugin/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const allPreset = {
1818
"avoid-shorthand-boolean": "warn",
1919
"avoid-shorthand-fragment": "warn",
2020
"ensure-forward-ref-using-ref": "warn",
21+
"jsx-no-duplicate-props": "warn",
2122
"jsx-uses-vars": "warn",
2223
"no-access-state-in-setstate": "error",
2324
"no-array-index-key": "warn",
@@ -100,6 +101,7 @@ const corePreset = {
100101
"ensure-forward-ref-using-ref": "warn",
101102
// "avoid-shorthand-boolean": "warn",
102103
// "avoid-shorthand-fragment": "warn",
104+
"jsx-no-duplicate-props": "warn",
103105
"jsx-uses-vars": "warn",
104106
"no-access-state-in-setstate": "error",
105107
"no-array-index-key": "warn",
@@ -183,6 +185,8 @@ const recommendedPreset = {
183185
const recommendedTypeScriptPreset = {
184186
...recommendedPreset,
185187
"dom/no-unknown-property": "off",
188+
"jsx-no-duplicate-props": "off",
189+
"jsx-uses-vars": "off",
186190
} as const satisfies RulePreset;
187191

188192
const recommendedTypeCheckedPreset = {

website/pages/docs/rules/_meta.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export default {
99
type: "separator",
1010
},
1111
"ensure-forward-ref-using-ref": "ensure-forward-ref-using-ref",
12+
"jsx-no-duplicate-props": "jsx-no-duplicate-props",
1213
"jsx-uses-vars": "jsx-uses-vars",
1314
"no-access-state-in-setstate": "no-access-state-in-setstate",
1415
"no-array-index-key": "no-array-index-key",
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# jsx-no-duplicate-props
2+
3+
## Rule category
4+
5+
Correctness.
6+
7+
## What it does
8+
9+
This rule prevents the use of duplicate props in JSX elements.
10+
11+
## Examples
12+
13+
### Failing
14+
15+
```tsx
16+
<Hello name="John" name="Doe" />;
17+
```
18+
19+
### Passing
20+
21+
```tsx
22+
<Hello name="John" />;
23+
```

website/pages/docs/rules/overview.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
| [`avoid-shorthand-boolean`](avoid-shorthand-boolean) | Prevents using shorthand syntax for boolean attributes. | 🎨 | | |
2727
| [`avoid-shorthand-fragment`](avoid-shorthand-fragment) | Prevents using shorthand syntax for fragments. | 🎨 | | |
2828
| [`ensure-forward-ref-using-ref`](ensure-forward-ref-using-ref) | Requires that components wrapped with `forwardRef` must have a `ref` parameter. | ✔️ | | |
29+
| [`jsx-no-duplicate-props`](jsx-no-duplicate-props) | Prevents duplicate props in JSX. | ✔️ | | |
2930
| [`jsx-uses-vars`](jsx-uses-vars) | Prevents variables used in JSX to be marked as unused. | ✔️ | | |
3031
| [`no-access-state-in-setstate`](no-access-state-in-setstate) | Prevents accessing `this.state` inside `setState` calls. | ✔️ | | |
3132
| [`no-array-index-key`](no-array-index-key) | Prevents using array `index` as `key`. | 🧐 | | |

0 commit comments

Comments
 (0)