From 12ba068fb10ee48c4365630218fce1b94f17e284 Mon Sep 17 00:00:00 2001 From: Chris Ng Date: Wed, 4 Oct 2023 15:18:44 -0400 Subject: [PATCH] [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. --- __tests__/src/rules/no-duplicate-ids.js | 44 ++++++++++++++++++++ docs/rules/no-duplicate-ids.md | 32 ++++++++++++++ src/index.js | 3 ++ src/rules/no-duplicate-ids.js | 55 +++++++++++++++++++++++++ 4 files changed, 134 insertions(+) create mode 100644 __tests__/src/rules/no-duplicate-ids.js create mode 100644 docs/rules/no-duplicate-ids.md create mode 100644 src/rules/no-duplicate-ids.js diff --git a/__tests__/src/rules/no-duplicate-ids.js b/__tests__/src/rules/no-duplicate-ids.js new file mode 100644 index 000000000..71a0bc6c1 --- /dev/null +++ b/__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: '
' }, + { code: '
' }, + { code: '
' }, + { code: '
' }, + )).map(parserOptionsMapper), + invalid: parsers.all([].concat( + { code: '
', errors: [expectedError('chris')] }, + { code: '
', errors: [expectedJSXError('chris')] }, + { code: '
', errors: [expectedError('chris')] }, + { code: '
', errors: [expectedJSXError('chris')] }, + )).map(parserOptionsMapper), +}); diff --git a/docs/rules/no-duplicate-ids.md b/docs/rules/no-duplicate-ids.md new file mode 100644 index 000000000..50f016a80 --- /dev/null +++ b/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`. + + + +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 +
+
+ +
+``` + +### Fail + +```jsx +
+
+ +
+``` + +## Accessibility guidelines +- [WCAG 4.1.1](https://www.w3.org/WAI/WCAG21/Understanding/parsing.html) diff --git a/src/index.js b/src/index.js index 7b931fe34..0a6c856da 100644 --- a/src/index.js +++ b/src/index.js @@ -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'), @@ -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', { @@ -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', diff --git a/src/rules/no-duplicate-ids.js b/src/rules/no-duplicate-ids.js new file mode 100644 index 000000000..46ad3dc97 --- /dev/null +++ b/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); + } + }, + }; + }, +};