Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[New]: Add no-duplicate-ids lint rule #955

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -165,6 +165,7 @@ will be evaluated as an `h3`. If no `polymorphicPropName` is set, then the compo
| [no-aria-hidden-on-focusable](docs/rules/no-aria-hidden-on-focusable.md) | Disallow `aria-hidden="true"` from being set on focusable elements. | | | |
| [no-autofocus](docs/rules/no-autofocus.md) | Enforce autoFocus prop is not used. | ☑️ 🔒 | | |
| [no-distracting-elements](docs/rules/no-distracting-elements.md) | Enforce distracting elements are not used. | ☑️ 🔒 | | |
| [no-duplicate-ids](docs/rules/no-duplicate-ids.md) | Disallow duplicate ids. | ☑️ 🔒 | | |
| [no-interactive-element-to-noninteractive-role](docs/rules/no-interactive-element-to-noninteractive-role.md) | Interactive elements should not be assigned non-interactive roles. | ☑️ 🔒 | | |
| [no-noninteractive-element-interactions](docs/rules/no-noninteractive-element-interactions.md) | Non-interactive elements should not be assigned mouse or keyboard event listeners. | ☑️ 🔒 | | |
| [no-noninteractive-element-to-interactive-role](docs/rules/no-noninteractive-element-to-interactive-role.md) | Non-interactive elements should not be assigned interactive roles. | ☑️ 🔒 | | |
Expand Down
44 changes: 44 additions & 0 deletions __tests__/src/rules/no-duplicate-ids.js
@@ -0,0 +1,44 @@
/**
* @fileoverview Disallow duplicate ids.
* @author Chris Ng
*/

// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------

import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/no-duplicate-ids';

// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------

const ruleTester = new RuleTester();

const expectedError = (idValue) => ({
message: `Duplicate ID "${idValue}" found. ID attribute values must be unique.`,
type: 'JSXOpeningElement',
});

const expectedJSXError = (idValue) => ({
message: `Duplicate ID "${idValue}" found. ID attribute JSX experssions must be unique.`,
type: 'JSXOpeningElement',
});

ruleTester.run('no-duplicate-ids', rule, {
valid: parsers.all([].concat(
{ code: '<div><div id="chris"></div><div id="chris2"></div></div>' },
{ code: '<div><div id={chris}></div><div id={chris2}></div></div>' },
{ code: '<div><MyComponent id="chris" /><MyComponent id="chris2" /></div>' },
{ code: '<div><div id="chris"></div><MyComponent id={chris} /></div>' },
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
{ code: '<div><div id="chris"></div><div id="chris"></div></div>', errors: [expectedError('chris')] },
{ code: '<div><div id={chris}></div><div id={chris}></div></div>', errors: [expectedJSXError('chris')] },
{ code: '<div><MyComponent id="chris" /><MyComponent id="chris" /></div>', errors: [expectedError('chris')] },
{ code: '<div><div id={chris}></div><MyComponent id={chris} /></div>', errors: [expectedJSXError('chris')] },
)).map(parserOptionsMapper),
});
32 changes: 32 additions & 0 deletions docs/rules/no-duplicate-ids.md
@@ -0,0 +1,32 @@
# jsx-a11y/no-duplicate-ids

💼 This rule is enabled in the following configs: ☑️ `recommended`, 🔒 `strict`.

<!-- end auto-generated rule header -->

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.

## Rule details

This rule takes no arguments.

### Succeed

```jsx
<div id="chris"></div><div id="chris2"></div>
<div id={chris}></div><div id={chris2}></div>
<MyComponent id="chris" /><MyComponent id="chris2" />
<div id="chris"></div><MyComponent id={chris} />
```

### Fail

```jsx
<div id="chris"></div><div id="chris"></div>
<div id={chris}></div><div id={chris}></div>
<MyComponent id="chris" /><MyComponent id="chris" />
<MyComponent id={chris} /><div id={chris}></div>
```

## Accessibility guidelines
- [WCAG 4.1.1](https://www.w3.org/WAI/WCAG21/Understanding/parsing.html)
3 changes: 3 additions & 0 deletions src/index.js
Expand Up @@ -29,6 +29,7 @@ module.exports = {
'no-aria-hidden-on-focusable': require('./rules/no-aria-hidden-on-focusable'),
'no-autofocus': require('./rules/no-autofocus'),
'no-distracting-elements': require('./rules/no-distracting-elements'),
'no-duplicate-ids': require('./rules/no-duplicate-ids'),
'no-interactive-element-to-noninteractive-role': require('./rules/no-interactive-element-to-noninteractive-role'),
'no-noninteractive-element-interactions': require('./rules/no-noninteractive-element-interactions'),
'no-noninteractive-element-to-interactive-role': require('./rules/no-noninteractive-element-to-interactive-role'),
Expand Down Expand Up @@ -116,6 +117,7 @@ module.exports = {
'jsx-a11y/no-access-key': 'error',
'jsx-a11y/no-autofocus': 'error',
'jsx-a11y/no-distracting-elements': 'error',
'jsx-a11y/no-duplicate-ids': 'error',
'jsx-a11y/no-interactive-element-to-noninteractive-role': [
'error',
{
Expand Down Expand Up @@ -273,6 +275,7 @@ module.exports = {
'jsx-a11y/no-access-key': 'error',
'jsx-a11y/no-autofocus': 'error',
'jsx-a11y/no-distracting-elements': 'error',
'jsx-a11y/no-duplicate-ids': 'error',
'jsx-a11y/no-interactive-element-to-noninteractive-role': 'error',
'jsx-a11y/no-noninteractive-element-interactions': [
'error',
Expand Down
55 changes: 55 additions & 0 deletions src/rules/no-duplicate-ids.js
@@ -0,0 +1,55 @@
/**
* @fileoverview Disallow duplicate ids.
* @author Chris Ng
*/

// ----------------------------------------------------------------------------
// Rule Definition
// ----------------------------------------------------------------------------

import { getProp, getPropValue } from 'jsx-ast-utils';
import { generateObjSchema } from '../util/schemas';

const schema = generateObjSchema();

export default {
meta: {
docs: {
url: 'https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/tree/HEAD/docs/rules/no-duplicate-ids.md',
description: 'Disallow duplicate ids.',
},
schema: [schema],
},

create(context) {
const idsUsedSet = new Set();
const jsxExperissionIDsUsedSet = new Set();

return {
JSXOpeningElement(node) {
const { attributes } = node;
const idProp = getProp(attributes, 'id');
const idValue = getPropValue(idProp);

// Special case if id is assigned using JSXExpressionContainer
if (idProp && idProp.type === 'JSXAttribute' && idProp.value.type === 'JSXExpressionContainer') {
if (jsxExperissionIDsUsedSet.has(idValue)) {
context.report({
node,
message: `Duplicate ID "${idValue}" found. ID attribute JSX experssions must be unique.`,
});
} else {
jsxExperissionIDsUsedSet.add(idValue);
}
} else if (idsUsedSet.has(idValue)) {
context.report({
node,
message: `Duplicate ID "${idValue}" found. ID attribute values must be unique.`,
});
} else {
idsUsedSet.add(idValue);
}
},
};
},
};