Skip to content

Commit 12ba068

Browse files
committed
[New]: Add no-duplicate-ids lint rule
Enforces that no `id` attributes are reused. This rule does a basic check to ensure that `id` attribute values are not the same. In the case of a JSX expression, it checks that no `id` attributes reuse the same expression.
1 parent fffb05b commit 12ba068

File tree

4 files changed

+134
-0
lines changed

4 files changed

+134
-0
lines changed
+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* @fileoverview Disallow duplicate ids.
3+
* @author Chris Ng
4+
*/
5+
6+
// -----------------------------------------------------------------------------
7+
// Requirements
8+
// -----------------------------------------------------------------------------
9+
10+
import { RuleTester } from 'eslint';
11+
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
12+
import parsers from '../../__util__/helpers/parsers';
13+
import rule from '../../../src/rules/no-duplicate-ids';
14+
15+
// -----------------------------------------------------------------------------
16+
// Tests
17+
// -----------------------------------------------------------------------------
18+
19+
const ruleTester = new RuleTester();
20+
21+
const expectedError = (idValue) => ({
22+
message: `Duplicate ID "${idValue}" found. ID attribute values must be unique.`,
23+
type: 'JSXOpeningElement',
24+
});
25+
26+
const expectedJSXError = (idValue) => ({
27+
message: `Duplicate ID "${idValue}" found. ID attribute JSX experssions must be unique.`,
28+
type: 'JSXOpeningElement',
29+
});
30+
31+
ruleTester.run('no-duplicate-ids', rule, {
32+
valid: parsers.all([].concat(
33+
{ code: '<div><div id="chris"></div><div id="chris2"></div></div>' },
34+
{ code: '<div><div id={chris}></div><div id={chris2}></div></div>' },
35+
{ code: '<div><MyComponent id="chris" /><MyComponent id="chris2" /></div>' },
36+
{ code: '<div><div id="chris"></div><MyComponent id={chris} /></div>' },
37+
)).map(parserOptionsMapper),
38+
invalid: parsers.all([].concat(
39+
{ code: '<div><div id="chris"></div><div id="chris"></div></div>', errors: [expectedError('chris')] },
40+
{ code: '<div><div id={chris}></div><div id={chris}></div></div>', errors: [expectedJSXError('chris')] },
41+
{ code: '<div><MyComponent id="chris" /><MyComponent id="chris" /></div>', errors: [expectedError('chris')] },
42+
{ code: '<div><div id={chris}></div><MyComponent id={chris} /></div>', errors: [expectedJSXError('chris')] },
43+
)).map(parserOptionsMapper),
44+
});

docs/rules/no-duplicate-ids.md

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# jsx-a11y/no-duplicate-ids
2+
3+
💼 This rule is enabled in the following configs: ☑️ `recommended`, 🔒 `strict`.
4+
5+
<!-- end auto-generated rule header -->
6+
7+
Enforces that no `id` attributes are reused. This rule does a basic check to ensure that `id` attribute values are not the same. In the case of a JSX expression, it checks that no `id` attributes reuse the same expression.
8+
9+
## Rule details
10+
11+
This rule takes no arguments.
12+
13+
### Succeed
14+
15+
```jsx
16+
<div id="chris"></div><div id="chris2"></div>
17+
<div id={chris}></div><div id={chris2}></div>
18+
<MyComponent id="chris" /><MyComponent id="chris2" />
19+
<div id="chris"></div><MyComponent id={chris} />
20+
```
21+
22+
### Fail
23+
24+
```jsx
25+
<div id="chris"></div><div id="chris"></div>
26+
<div id={chris}></div><div id={chris}></div>
27+
<MyComponent id="chris" /><MyComponent id="chris" />
28+
<MyComponent id={chris} /><div id={chris}></div>
29+
```
30+
31+
## Accessibility guidelines
32+
- [WCAG 4.1.1](https://www.w3.org/WAI/WCAG21/Understanding/parsing.html)

src/index.js

+3
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ module.exports = {
2929
'no-aria-hidden-on-focusable': require('./rules/no-aria-hidden-on-focusable'),
3030
'no-autofocus': require('./rules/no-autofocus'),
3131
'no-distracting-elements': require('./rules/no-distracting-elements'),
32+
'no-duplicate-ids': require('./rules/no-duplicate-ids'),
3233
'no-interactive-element-to-noninteractive-role': require('./rules/no-interactive-element-to-noninteractive-role'),
3334
'no-noninteractive-element-interactions': require('./rules/no-noninteractive-element-interactions'),
3435
'no-noninteractive-element-to-interactive-role': require('./rules/no-noninteractive-element-to-interactive-role'),
@@ -116,6 +117,7 @@ module.exports = {
116117
'jsx-a11y/no-access-key': 'error',
117118
'jsx-a11y/no-autofocus': 'error',
118119
'jsx-a11y/no-distracting-elements': 'error',
120+
'jsx-a11y/no-duplicate-ids': 'error',
119121
'jsx-a11y/no-interactive-element-to-noninteractive-role': [
120122
'error',
121123
{
@@ -273,6 +275,7 @@ module.exports = {
273275
'jsx-a11y/no-access-key': 'error',
274276
'jsx-a11y/no-autofocus': 'error',
275277
'jsx-a11y/no-distracting-elements': 'error',
278+
'jsx-a11y/no-duplicate-ids': 'error',
276279
'jsx-a11y/no-interactive-element-to-noninteractive-role': 'error',
277280
'jsx-a11y/no-noninteractive-element-interactions': [
278281
'error',

src/rules/no-duplicate-ids.js

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/**
2+
* @fileoverview Disallow duplicate ids.
3+
* @author Chris Ng
4+
*/
5+
6+
// ----------------------------------------------------------------------------
7+
// Rule Definition
8+
// ----------------------------------------------------------------------------
9+
10+
import { getProp, getPropValue } from 'jsx-ast-utils';
11+
import { generateObjSchema } from '../util/schemas';
12+
13+
const schema = generateObjSchema();
14+
15+
export default {
16+
meta: {
17+
docs: {
18+
url: 'https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/tree/HEAD/docs/rules/no-duplicate-ids.md',
19+
description: 'Disallow duplicate ids.',
20+
},
21+
schema: [schema],
22+
},
23+
24+
create(context) {
25+
const idsUsedSet = new Set();
26+
const jsxExperissionIDsUsedSet = new Set();
27+
28+
return {
29+
JSXOpeningElement(node) {
30+
const { attributes } = node;
31+
const idProp = getProp(attributes, 'id');
32+
const idValue = getPropValue(idProp);
33+
34+
// Special case if id is assigned using JSXExpressionContainer
35+
if (idProp && idProp.type === 'JSXAttribute' && idProp.value.type === 'JSXExpressionContainer') {
36+
if (jsxExperissionIDsUsedSet.has(idValue)) {
37+
context.report({
38+
node,
39+
message: `Duplicate ID "${idValue}" found. ID attribute JSX experssions must be unique.`,
40+
});
41+
} else {
42+
jsxExperissionIDsUsedSet.add(idValue);
43+
}
44+
} else if (idsUsedSet.has(idValue)) {
45+
context.report({
46+
node,
47+
message: `Duplicate ID "${idValue}" found. ID attribute values must be unique.`,
48+
});
49+
} else {
50+
idsUsedSet.add(idValue);
51+
}
52+
},
53+
};
54+
},
55+
};

0 commit comments

Comments
 (0)