Skip to content

feat: new rule attr-value-no-duplication #1650

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

Merged
merged 2 commits into from
Jun 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions dist/core/rules/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 37 additions & 0 deletions src/core/rules/attr-value-no-duplication.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Rule } from '../types'

export default {
id: 'attr-value-no-duplication',
description: 'Attribute values should not contain duplicates.',
init(parser, reporter) {
parser.addListener('tagstart', (event) => {
const attrs = event.attrs
let attr
const col = event.col + event.tagName.length + 1

for (let i = 0, l = attrs.length; i < l; i++) {
attr = attrs[i]

if (attr.value) {
// Split attribute value by whitespace to get individual values
const values = attr.value.trim().split(/\s+/)
const duplicateMap: { [value: string]: boolean } = {}

for (const value of values) {
if (duplicateMap[value] === true) {
reporter.error(
`Duplicate value [ ${value} ] was found in attribute [ ${attr.name} ].`,
event.line,
col + attr.index,
this,
attr.raw
)
break // Only report the first duplicate found per attribute
}
duplicateMap[value] = true
}
}
}
})
},
} as Rule
1 change: 1 addition & 0 deletions src/core/rules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export { default as altRequire } from './alt-require'
export { default as attrLowercase } from './attr-lowercase'
export { default as attrNoDuplication } from './attr-no-duplication'
export { default as attrNoUnnecessaryWhitespace } from './attr-no-unnecessary-whitespace'
export { default as attrValueNoDuplication } from './attr-value-no-duplication'
export { default as attrSort } from './attr-sorted'
export { default as attrUnsafeChars } from './attr-unsafe-chars'
export { default as attrValueDoubleQuotes } from './attr-value-double-quotes'
Expand Down
1 change: 1 addition & 0 deletions src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface Ruleset {
'attr-value-double-quotes'?: boolean
'attr-value-not-empty'?: boolean
'attr-value-single-quotes'?: boolean
'attr-value-no-duplication'?: boolean
'attr-whitespace'?: boolean
'doctype-first'?: boolean
'doctype-html5'?: boolean
Expand Down
8 changes: 4 additions & 4 deletions test/cli/formatters/sarif.sarif
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"helpUri": "https://htmlhint.com/rules/attr-value-double-quotes",
"help": {
"text": "Attribute values must be in double quotes.",
"markdown": "\nAttribute value must closed by double quotes.\n\nLevel: Error\n\n## Config value\n\n1. true: enable rule\n2. false: disable rule\n\n### The following patterns are **not** considered rule violations\n\n```html\n`<a href=\"\" title=\"abc\">``</a>`\n```\n\n### The following pattern is considered a rule violation:\n\n```html\n`<a href='' title='abc'>``</a>`\n```"
"markdown": "\nAttribute value must closed by double quotes.\n\nLevel: Error\n\n## Config value\n\n- `true`: enable rule\n- `false`: disable rule\n\n### The following patterns are **not** considered rule violations\n\n```html\n`<a href=\"\" title=\"abc\">``</a>`\n```\n\n### The following pattern is considered a rule violation:\n\n```html\n`<a href='' title='abc'>``</a>`\n```"
}
},
{
Expand All @@ -26,7 +26,7 @@
"helpUri": "https://htmlhint.com/rules/attr-no-duplication",
"help": {
"text": "Elements cannot have duplicate attributes.",
"markdown": "\nThe same attribute can't be specified twice.\n\nLevel: Error\n\n## Config value\n\n1. true: enable rule\n2. false: disable rule\n\n### The following patterns are **not** considered rule violations\n\n```html\n`<img src=\"a.png\" />`\n```\n\n### The following pattern is considered a rule violation:\n\n```html\n`<img src=\"a.png\" src=\"b.png\" />`\n```"
"markdown": "\nThe same attribute can't be specified twice.\n\nLevel: Error\n\n## Config value\n\n- `true`: enable rule\n- `false`: disable rule\n\n### The following patterns are **not** considered rule violations\n\n```html\n`<img src=\"a.png\" />`\n```\n\n### The following pattern is considered a rule violation:\n\n```html\n`<img src=\"a.png\" src=\"b.png\" />`\n```"
}
},
{
Expand All @@ -37,7 +37,7 @@
"helpUri": "https://htmlhint.com/rules/tag-pair",
"help": {
"text": "Tag must be paired.",
"markdown": "\nTag must be paired.\n\nLevel: Error\n\n## Config value\n\n1. true: enable rule\n2. false: disable rule\n\n### The following patterns are **not** considered rule violations\n\n```html\n`<ul>``<li>``</li>``</ul>`\n```\n\n### The following pattern is considered a rule violation:\n\n```html\n`<ul>``<li>``</ul>`\n`<ul>``</li>``</ul>`\n```"
"markdown": "\nTag must be paired.\n\nLevel: Error\n\n## Config value\n\n- `true`: enable rule\n- `false`: disable rule\n\n### The following patterns are **not** considered rule violations\n\n```html\n`<ul>``<li>``</li>``</ul>`\n```\n\n### The following pattern is considered a rule violation:\n\n```html\n`<ul>``<li>``</ul>`\n`<ul>``</li>``</ul>`\n```"
}
},
{
Expand All @@ -48,7 +48,7 @@
"helpUri": "https://htmlhint.com/rules/spec-char-escape",
"help": {
"text": "Special characters must be escaped.",
"markdown": "\nSpecial characters must be escaped.\n\nLevel: Error\n\n## Config value\n\n1. true: enable rule\n2. false: disable rule\n\n### The following patterns are **not** considered violations\n\n```html\n`<span>`aaa&gt;bbb&lt;ccc`</span>`\n`<span>`Steinway &amp; Sons, Q&amp;A`</span>`\n`<span>`Steinway & Sons, Q&A`</span>`\n```\n\n### The following pattern is considered a rule violation:\n\n```html\n`<span>`aaa>bbb`<ccc</span>`\n```"
"markdown": "\nSpecial characters must be escaped.\n\nLevel: Error\n\n## Config value\n\n- `true`: enable rule\n- `false`: disable rule\n\n### The following patterns are **not** considered violations\n\n```html\n`<span>`aaa&gt;bbb&lt;ccc`</span>`\n`<span>`Steinway &amp; Sons, Q&amp;A`</span>`\n`<span>`Steinway & Sons, Q&A`</span>`\n```\n\n### The following pattern is considered a rule violation:\n\n```html\n`<span>`aaa>bbb`<ccc</span>`\n```"
}
}
],
Expand Down
67 changes: 67 additions & 0 deletions test/rules/attr-value-no-duplication.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
const HTMLHint = require('../../dist/htmlhint.js').HTMLHint

const ruleId = 'attr-value-no-duplication'
const ruleOptions = {}

ruleOptions[ruleId] = true

describe(`Rules: ${ruleId}`, () => {
it('Duplicate values in class attribute should result in an error', () => {
const code = '<div class="d-none small d-none">Test</div>'
const messages = HTMLHint.verify(code, ruleOptions)
expect(messages.length).toBe(1)
expect(messages[0].rule.id).toBe(ruleId)
expect(messages[0].line).toBe(1)
expect(messages[0].col).toBe(5)
expect(messages[0].message).toBe(
'Duplicate value [ d-none ] was found in attribute [ class ].'
)
})

it('Duplicate values in data attribute should result in an error', () => {
const code = '<span data-attributes="dark light dark">Test</span>'
const messages = HTMLHint.verify(code, ruleOptions)
expect(messages.length).toBe(1)
expect(messages[0].rule.id).toBe(ruleId)
expect(messages[0].line).toBe(1)
expect(messages[0].col).toBe(6)
expect(messages[0].message).toBe(
'Duplicate value [ dark ] was found in attribute [ data-attributes ].'
)
})

it('No duplicate values should not result in an error', () => {
const code = '<div class="container fluid small">Test</div>'
const messages = HTMLHint.verify(code, ruleOptions)
expect(messages.length).toBe(0)
})

it('Single value should not result in an error', () => {
const code = '<div class="container">Test</div>'
const messages = HTMLHint.verify(code, ruleOptions)
expect(messages.length).toBe(0)
})

it('Empty attribute value should not result in an error', () => {
const code = '<div class="">Test</div>'
const messages = HTMLHint.verify(code, ruleOptions)
expect(messages.length).toBe(0)
})

it('Multiple attributes with no duplicates should not result in an error', () => {
const code =
'<div class="btn btn-primary" id="submit-button" data-toggle="modal">Test</div>'
const messages = HTMLHint.verify(code, ruleOptions)
expect(messages.length).toBe(0)
})

it('Multiple spaces between values should still detect duplicates', () => {
const code = '<div class="btn primary btn">Test</div>'
const messages = HTMLHint.verify(code, ruleOptions)
expect(messages.length).toBe(1)
expect(messages[0].rule.id).toBe(ruleId)
expect(messages[0].message).toBe(
'Duplicate value [ btn ] was found in attribute [ class ].'
)
})
})
59 changes: 59 additions & 0 deletions website/src/content/docs/rules/attr-value-no-duplication.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
---
id: attr-value-no-duplication
title: attr-value-no-duplication
description: Prevents duplicate values within the same attribute to ensure clean and efficient markup.
draft: true
sidebar:
badge: New

---

import { Badge } from '@astrojs/starlight/components';

Attribute values should not contain duplicates.

Level: <Badge text="Error" variant="danger" />

## Config value

- `true`: enable rule
- `false`: disable rule

### The following patterns are **not** considered rule violations

```html
<div class="container fluid small">Content</div>
```

```html
<span data-toggle="modal">Content</span>
```

```html
<div class="btn btn-primary btn-large">Button</div>
```

### The following patterns are considered rule violations:

```html
<div class="d-none small d-none">Content</div>
```

```html
<span data-attributes="dark light dark">Content</span>
```

```html
<div class="btn primary btn">Button</div>
```

## Why does this rule exist?

Having duplicate values in attributes like `class` or custom data attributes can:

- Make the markup unnecessarily verbose
- Cause confusion during development
- Lead to inefficient CSS specificity calculations
- Indicate potential copy-paste errors or oversight

This rule helps maintain clean, efficient markup by catching these duplicates early.
2 changes: 2 additions & 0 deletions website/src/content/docs/rules/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ description: A complete list of all the rules for HTMLHint
- [`button-type-require`](button-type-require/): The type attribute of a button element must be present with a valid value: "button", "submit", or "reset".
- [`input-requires-label`](input-requires-label/): All [ input ] tags must have a corresponding [ label ] tag.

{/* [`attr-value-no-duplication`](attr-value-no-duplication/): Attribute values should not contain duplicates. */}

## Tags

- [`empty-tag-not-self-closed`](empty-tag-not-self-closed/): The empty tag should not be closed by self.
Expand Down