Skip to content

Commit dbc8e8c

Browse files
committed
[New] jsx-key: add warnDuplicates option to warn on duplicate jsx keys in an array
Fixes #2614
1 parent 227cf88 commit dbc8e8c

File tree

4 files changed

+78
-10
lines changed

4 files changed

+78
-10
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
1212
* add [`iframe-missing-sandbox`] rule ([#2753][] @tosmolka @ljharb)
1313
* [`no-did-mount-set-state`], [`no-did-update-set-state`]: no-op with react >= 16.3 ([#1754][] @ljharb)
1414
* [`jsx-sort-props`]: support multiline prop groups ([#3198][] @duhamelgm)
15+
* [`jsx-key`]: add `warnDuplicates` option to warn on duplicate jsx keys in an array ([#2614][] @ljharb)
1516

1617
### Fixed
1718
* [`prop-types`], `propTypes`: add support for exported type inference ([#3163][] @vedadeepta)
@@ -57,6 +58,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
5758
[#2921]: https://github.com/yannickcr/eslint-plugin-react/pull/2921
5859
[#2813]: https://github.com/yannickcr/eslint-plugin-react/pull/2813
5960
[#2753]: https://github.com/yannickcr/eslint-plugin-react/pull/2753
61+
[#2614]: https://github.com/yannickcr/eslint-plugin-react/issues/2614
6062
[#2596]: https://github.com/yannickcr/eslint-plugin-react/issues/2596
6163
[#2061]: https://github.com/yannickcr/eslint-plugin-react/issues/2061
6264
[#1817]: https://github.com/yannickcr/eslint-plugin-react/issues/1817

docs/rules/jsx-key.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,19 @@ Examples of **incorrect** code for this rule:
5757
<span {...spread} key={"key-after-spread"} />;
5858
```
5959

60+
### `warnOnDuplicates` (default: `false`)
61+
62+
When `true` the rule will check for any duplicate key prop values.
63+
64+
Examples of **incorrect** code for this rule:
65+
66+
```jsx
67+
const spans = [
68+
<span key="notunique"/>,
69+
<span key="notunique"/>,
70+
];
71+
```
72+
6073
## When Not To Use It
6174

6275
If you are not using JSX then you can disable this rule.

lib/rules/jsx-key.js

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
const hasProp = require('jsx-ast-utils/hasProp');
99
const propName = require('jsx-ast-utils/propName');
10+
const values = require('object.values');
1011
const docsUrl = require('../util/docsUrl');
1112
const pragmaUtil = require('../util/pragma');
1213
const report = require('../util/report');
@@ -18,6 +19,7 @@ const report = require('../util/report');
1819
const defaultOptions = {
1920
checkFragmentShorthand: false,
2021
checkKeyMustBeforeSpread: false,
22+
warnOnDuplicates: false,
2123
};
2224

2325
const messages = {
@@ -26,6 +28,7 @@ const messages = {
2628
missingArrayKey: 'Missing "key" prop for element in array',
2729
missingArrayKeyUsePrag: 'Missing "key" prop for element in array. Shorthand fragment syntax does not support providing keys. Use {{reactPrag}}.{{fragPrag}} instead',
2830
keyBeforeSpread: '`key` prop must be placed before any `{...spread}, to avoid conflicting with React’s new JSX transform: https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html`',
31+
nonUniqueKeys: '`key` prop must be unique',
2932
};
3033

3134
module.exports = {
@@ -50,6 +53,10 @@ module.exports = {
5053
type: 'boolean',
5154
default: defaultOptions.checkKeyMustBeforeSpread,
5255
},
56+
warnOnDuplicates: {
57+
type: 'boolean',
58+
default: defaultOptions.warnOnDuplicates,
59+
},
5360
},
5461
additionalProperties: false,
5562
}],
@@ -59,6 +66,7 @@ module.exports = {
5966
const options = Object.assign({}, defaultOptions, context.options[0]);
6067
const checkFragmentShorthand = options.checkFragmentShorthand;
6168
const checkKeyMustBeforeSpread = options.checkKeyMustBeforeSpread;
69+
const warnOnDuplicates = options.warnOnDuplicates;
6270
const reactPragma = pragmaUtil.getFromContext(context);
6371
const fragmentPragma = pragmaUtil.getFragmentFromContext(context);
6472

@@ -97,19 +105,43 @@ module.exports = {
97105
}
98106

99107
return {
100-
JSXElement(node) {
101-
if (hasProp(node.openingElement.attributes, 'key')) {
102-
if (checkKeyMustBeforeSpread && isKeyAfterSpread(node.openingElement.attributes)) {
103-
report(context, messages.keyBeforeSpread, 'keyBeforeSpread', {
104-
node,
105-
});
106-
}
108+
ArrayExpression(node) {
109+
const jsx = node.elements.filter((x) => x.type === 'JSXElement');
110+
if (jsx.length === 0) {
107111
return;
108112
}
109113

110-
if (node.parent.type === 'ArrayExpression') {
111-
report(context, messages.missingArrayKey, 'missingArrayKey', {
112-
node,
114+
const map = {};
115+
jsx.forEach((element) => {
116+
const attrs = element.openingElement.attributes;
117+
const keys = attrs.filter((x) => x.name && x.name.name === 'key');
118+
119+
if (keys.length === 0) {
120+
report(context, messages.missingArrayKey, 'missingArrayKey', {
121+
node: element,
122+
});
123+
} else {
124+
keys.forEach((attr) => {
125+
const value = context.getSourceCode().getText(attr.value);
126+
if (!map[value]) { map[value] = []; }
127+
map[value].push(attr);
128+
129+
if (checkKeyMustBeforeSpread && isKeyAfterSpread(attrs)) {
130+
report(context, messages.keyBeforeSpread, 'keyBeforeSpread', {
131+
node,
132+
});
133+
}
134+
});
135+
}
136+
});
137+
138+
if (warnOnDuplicates) {
139+
values(map).filter((v) => v.length > 1).forEach((v) => {
140+
v.forEach((n) => {
141+
report(context, messages.nonUniqueKeys, 'nonUniqueKeys', {
142+
node: n,
143+
});
144+
});
113145
});
114146
}
115147
},

tests/lib/rules/jsx-key.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,14 @@ ruleTester.run('jsx-key', rule, {
6666
code: '<div key="keyBeforeSpread" {...{}} />;',
6767
options: [{ checkKeyMustBeforeSpread: true }],
6868
},
69+
{
70+
code: `
71+
const spans = [
72+
<span key="notunique"/>,
73+
<span key="notunique"/>,
74+
];
75+
`,
76+
},
6977
]),
7078
invalid: parsers.all([
7179
{
@@ -144,5 +152,18 @@ ruleTester.run('jsx-key', rule, {
144152
settings,
145153
errors: [{ messageId: 'keyBeforeSpread' }],
146154
},
155+
{
156+
code: `
157+
const spans = [
158+
<span key="notunique"/>,
159+
<span key="notunique"/>,
160+
];
161+
`,
162+
options: [{ warnOnDuplicates: true }],
163+
errors: [
164+
{ messageId: 'nonUniqueKeys', line: 3 },
165+
{ messageId: 'nonUniqueKeys', line: 4 },
166+
],
167+
},
147168
]),
148169
});

0 commit comments

Comments
 (0)