Skip to content

Commit e7de144

Browse files
authored
feat: new rule attr-value-no-duplication (#1650)
* feat: new rule `attr-value-no-duplication` * Update sarif.sarif
1 parent 5877588 commit e7de144

File tree

8 files changed

+175
-6
lines changed

8 files changed

+175
-6
lines changed

dist/core/rules/index.js

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { Rule } from '../types'
2+
3+
export default {
4+
id: 'attr-value-no-duplication',
5+
description: 'Attribute values should not contain duplicates.',
6+
init(parser, reporter) {
7+
parser.addListener('tagstart', (event) => {
8+
const attrs = event.attrs
9+
let attr
10+
const col = event.col + event.tagName.length + 1
11+
12+
for (let i = 0, l = attrs.length; i < l; i++) {
13+
attr = attrs[i]
14+
15+
if (attr.value) {
16+
// Split attribute value by whitespace to get individual values
17+
const values = attr.value.trim().split(/\s+/)
18+
const duplicateMap: { [value: string]: boolean } = {}
19+
20+
for (const value of values) {
21+
if (duplicateMap[value] === true) {
22+
reporter.error(
23+
`Duplicate value [ ${value} ] was found in attribute [ ${attr.name} ].`,
24+
event.line,
25+
col + attr.index,
26+
this,
27+
attr.raw
28+
)
29+
break // Only report the first duplicate found per attribute
30+
}
31+
duplicateMap[value] = true
32+
}
33+
}
34+
}
35+
})
36+
},
37+
} as Rule

src/core/rules/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export { default as altRequire } from './alt-require'
22
export { default as attrLowercase } from './attr-lowercase'
33
export { default as attrNoDuplication } from './attr-no-duplication'
44
export { default as attrNoUnnecessaryWhitespace } from './attr-no-unnecessary-whitespace'
5+
export { default as attrValueNoDuplication } from './attr-value-no-duplication'
56
export { default as attrSort } from './attr-sorted'
67
export { default as attrUnsafeChars } from './attr-unsafe-chars'
78
export { default as attrValueDoubleQuotes } from './attr-value-double-quotes'

src/core/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export interface Ruleset {
1717
'attr-value-double-quotes'?: boolean
1818
'attr-value-not-empty'?: boolean
1919
'attr-value-single-quotes'?: boolean
20+
'attr-value-no-duplication'?: boolean
2021
'attr-whitespace'?: boolean
2122
'doctype-first'?: boolean
2223
'doctype-html5'?: boolean

test/cli/formatters/sarif.sarif

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"helpUri": "https://htmlhint.com/rules/attr-value-double-quotes",
1616
"help": {
1717
"text": "Attribute values must be in double quotes.",
18-
"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```"
18+
"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```"
1919
}
2020
},
2121
{
@@ -26,7 +26,7 @@
2626
"helpUri": "https://htmlhint.com/rules/attr-no-duplication",
2727
"help": {
2828
"text": "Elements cannot have duplicate attributes.",
29-
"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```"
29+
"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```"
3030
}
3131
},
3232
{
@@ -37,7 +37,7 @@
3737
"helpUri": "https://htmlhint.com/rules/tag-pair",
3838
"help": {
3939
"text": "Tag must be paired.",
40-
"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```"
40+
"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```"
4141
}
4242
},
4343
{
@@ -48,7 +48,7 @@
4848
"helpUri": "https://htmlhint.com/rules/spec-char-escape",
4949
"help": {
5050
"text": "Special characters must be escaped.",
51-
"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```"
51+
"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```"
5252
}
5353
}
5454
],
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
const HTMLHint = require('../../dist/htmlhint.js').HTMLHint
2+
3+
const ruleId = 'attr-value-no-duplication'
4+
const ruleOptions = {}
5+
6+
ruleOptions[ruleId] = true
7+
8+
describe(`Rules: ${ruleId}`, () => {
9+
it('Duplicate values in class attribute should result in an error', () => {
10+
const code = '<div class="d-none small d-none">Test</div>'
11+
const messages = HTMLHint.verify(code, ruleOptions)
12+
expect(messages.length).toBe(1)
13+
expect(messages[0].rule.id).toBe(ruleId)
14+
expect(messages[0].line).toBe(1)
15+
expect(messages[0].col).toBe(5)
16+
expect(messages[0].message).toBe(
17+
'Duplicate value [ d-none ] was found in attribute [ class ].'
18+
)
19+
})
20+
21+
it('Duplicate values in data attribute should result in an error', () => {
22+
const code = '<span data-attributes="dark light dark">Test</span>'
23+
const messages = HTMLHint.verify(code, ruleOptions)
24+
expect(messages.length).toBe(1)
25+
expect(messages[0].rule.id).toBe(ruleId)
26+
expect(messages[0].line).toBe(1)
27+
expect(messages[0].col).toBe(6)
28+
expect(messages[0].message).toBe(
29+
'Duplicate value [ dark ] was found in attribute [ data-attributes ].'
30+
)
31+
})
32+
33+
it('No duplicate values should not result in an error', () => {
34+
const code = '<div class="container fluid small">Test</div>'
35+
const messages = HTMLHint.verify(code, ruleOptions)
36+
expect(messages.length).toBe(0)
37+
})
38+
39+
it('Single value should not result in an error', () => {
40+
const code = '<div class="container">Test</div>'
41+
const messages = HTMLHint.verify(code, ruleOptions)
42+
expect(messages.length).toBe(0)
43+
})
44+
45+
it('Empty attribute value should not result in an error', () => {
46+
const code = '<div class="">Test</div>'
47+
const messages = HTMLHint.verify(code, ruleOptions)
48+
expect(messages.length).toBe(0)
49+
})
50+
51+
it('Multiple attributes with no duplicates should not result in an error', () => {
52+
const code =
53+
'<div class="btn btn-primary" id="submit-button" data-toggle="modal">Test</div>'
54+
const messages = HTMLHint.verify(code, ruleOptions)
55+
expect(messages.length).toBe(0)
56+
})
57+
58+
it('Multiple spaces between values should still detect duplicates', () => {
59+
const code = '<div class="btn primary btn">Test</div>'
60+
const messages = HTMLHint.verify(code, ruleOptions)
61+
expect(messages.length).toBe(1)
62+
expect(messages[0].rule.id).toBe(ruleId)
63+
expect(messages[0].message).toBe(
64+
'Duplicate value [ btn ] was found in attribute [ class ].'
65+
)
66+
})
67+
})
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
---
2+
id: attr-value-no-duplication
3+
title: attr-value-no-duplication
4+
description: Prevents duplicate values within the same attribute to ensure clean and efficient markup.
5+
draft: true
6+
sidebar:
7+
badge: New
8+
9+
---
10+
11+
import { Badge } from '@astrojs/starlight/components';
12+
13+
Attribute values should not contain duplicates.
14+
15+
Level: <Badge text="Error" variant="danger" />
16+
17+
## Config value
18+
19+
- `true`: enable rule
20+
- `false`: disable rule
21+
22+
### The following patterns are **not** considered rule violations
23+
24+
```html
25+
<div class="container fluid small">Content</div>
26+
```
27+
28+
```html
29+
<span data-toggle="modal">Content</span>
30+
```
31+
32+
```html
33+
<div class="btn btn-primary btn-large">Button</div>
34+
```
35+
36+
### The following patterns are considered rule violations:
37+
38+
```html
39+
<div class="d-none small d-none">Content</div>
40+
```
41+
42+
```html
43+
<span data-attributes="dark light dark">Content</span>
44+
```
45+
46+
```html
47+
<div class="btn primary btn">Button</div>
48+
```
49+
50+
## Why does this rule exist?
51+
52+
Having duplicate values in attributes like `class` or custom data attributes can:
53+
54+
- Make the markup unnecessarily verbose
55+
- Cause confusion during development
56+
- Lead to inefficient CSS specificity calculations
57+
- Indicate potential copy-paste errors or oversight
58+
59+
This rule helps maintain clean, efficient markup by catching these duplicates early.

website/src/content/docs/rules/index.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ description: A complete list of all the rules for HTMLHint
3131
- [`button-type-require`](button-type-require/): The type attribute of a button element must be present with a valid value: "button", "submit", or "reset".
3232
- [`input-requires-label`](input-requires-label/): All [ input ] tags must have a corresponding [ label ] tag.
3333

34+
{/* [`attr-value-no-duplication`](attr-value-no-duplication/): Attribute values should not contain duplicates. */}
35+
3436
## Tags
3537

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

0 commit comments

Comments
 (0)