Skip to content

Commit 68bf3dc

Browse files
committed
fix: stop ruleset/config leaking between runs and fix rule checks
- core: copy the ruleset in verify() so inline <!-- htmlhint --> rules don't mutate the caller's or the default ruleset - tags-check: don't persist custom config in module state across runs - doctype-html5: listen to comment events only, so the listener is actually removed after the first tag - html-lang-require: anchor the BCP47 pattern to the whole value - tag-pair: treat bgsound as a void element Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FN9Si1bR6igGbuoJ7ZGJWb
1 parent 3cf648e commit 68bf3dc

10 files changed

Lines changed: 44 additions & 5 deletions

‎src/core/core.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ class HTMLHintCore {
3232
ruleset = this.defaultRuleset
3333
}
3434

35+
// Copy the ruleset so inline <!-- htmlhint --> rules don't leak into the
36+
// caller's ruleset (or the shared default ruleset) on later calls
37+
ruleset = { ...ruleset }
38+
3539
// parse inline ruleset
3640
html = html.replace(
3741
/^\s*<!--\s*htmlhint\s+([^\r\n]+?)\s*-->/i,

‎src/core/rules/doctype-html5.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export default {
2525
parser.removeListener('tagstart', onTagStart)
2626
}
2727

28-
parser.addListener('all', onComment)
28+
parser.addListener('comment', onComment)
2929
parser.addListener('tagstart', onTagStart)
3030
},
3131
} as Rule

‎src/core/rules/html-lang-require.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export default {
3232
const tagName = event.tagName.toLowerCase()
3333
const mapAttrs = parser.getMapAttrs(event.attrs)
3434
const col = event.col + tagName.length + 1
35-
const langValidityPattern = new RegExp(languageTag, 'g')
35+
const langValidityPattern = new RegExp(`^${languageTag}$`)
3636

3737
if (tagName === 'html') {
3838
if ('lang' in mapAttrs) {

‎src/core/rules/tag-pair.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export default {
77
init(parser, reporter) {
88
const stack: Array<Partial<Block>> = []
99
const mapEmptyTags = parser.makeMap(
10-
'area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed,track,command,source,keygen,wbr'
10+
'area,base,basefont,bgsound,br,col,frame,hr,img,input,isindex,link,meta,param,embed,track,command,source,keygen,wbr'
1111
) //HTML 4.01 + HTML 5
1212

1313
parser.addListener('tagstart', (event) => {

‎src/core/rules/tags-check.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Rule } from '../types'
22

3-
let tagsTypings: Record<string, Record<string, unknown>> = {
3+
const defaultTagsTypings: Record<string, Record<string, unknown>> = {
44
a: {
55
selfclosing: false,
66
attrsRequired: ['href', 'title'],
@@ -33,7 +33,7 @@ export default {
3333
id: 'tags-check',
3434
description: 'Checks html tags.',
3535
init(parser, reporter, options: Record<string, Record<string, unknown>>) {
36-
tagsTypings = { ...tagsTypings, ...options }
36+
const tagsTypings = { ...defaultTagsTypings, ...options }
3737

3838
parser.addListener('tagstart', (event) => {
3939
const attrs = event.attrs

‎test/core.spec.js‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,4 +206,17 @@ describe('Core', () => {
206206
expect(messages[0].rule.id).toBe('attr-lowercase')
207207
})
208208
})
209+
210+
it('Inline ruleset should not leak into later verify calls', () => {
211+
const ruleset = { 'tag-pair': true }
212+
HTMLHint.verify(
213+
'<!-- htmlhint alt-require:true --><img src="a.png">',
214+
ruleset
215+
)
216+
expect(ruleset).toEqual({ 'tag-pair': true })
217+
218+
HTMLHint.verify('<!-- htmlhint alt-require:true --><img src="a.png">')
219+
const messages = HTMLHint.verify('<img src="a.png">')
220+
expect(messages.filter((m) => m.rule.id === 'alt-require').length).toBe(0)
221+
})
209222
})

‎test/rules/doctype-html5.spec.js‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,10 @@ describe(`Rules: ${ruleId}`, () => {
2222
const messages = HTMLHint.verify(code, ruleOptions)
2323
expect(messages.length).toBe(0)
2424
})
25+
26+
it('Non-doctype declarations after the first tag should not result in an error', () => {
27+
const code = '<!DOCTYPE html><html><![CDATA[ x ]]></html>'
28+
const messages = HTMLHint.verify(code, ruleOptions)
29+
expect(messages.length).toBe(0)
30+
})
2531
})

‎test/rules/html-lang-require.spec.js‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,9 @@ describe(`Rules: ${ruleId}`, () => {
3636
const messages = HTMLHint.verify(code, ruleOptions)
3737
expect(messages.length).toBe(0)
3838
})
39+
it('HTML tag with a lang value containing invalid characters should result in an error', () => {
40+
const code = '<html lang="en_US"></html>'
41+
const messages = HTMLHint.verify(code, ruleOptions)
42+
expect(messages.length).toBe(1)
43+
})
3944
})

‎test/rules/tag-pair.spec.js‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,10 @@ describe(`Rules: ${ruleId}`, () => {
4949
const messages = HTMLHint.verify(code, ruleOptions)
5050
expect(messages.length).toBe(0)
5151
})
52+
53+
it('Void bgsound tag should not result in an error', () => {
54+
const code = '<body><bgsound src="a.mid"></body>'
55+
const messages = HTMLHint.verify(code, ruleOptions)
56+
expect(messages.length).toBe(0)
57+
})
5258
})

‎test/rules/tags-check.spec.js‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,9 @@ describe(`Rules: ${ruleId}`, () => {
5757
messages = HTMLHint.verify(code, ruleOptions)
5858
expect(messages.length).toBe(1)
5959
})
60+
it('Should not leak custom config into later runs', () => {
61+
const code = '<sometag></sometag>'
62+
const messages = HTMLHint.verify(code, { [ruleId]: {} })
63+
expect(messages.length).toBe(0)
64+
})
6065
})

0 commit comments

Comments
 (0)