@herb-tools/linter 0.4.3 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +216 -19
- package/dist/herb-lint.js +5559 -1860
- package/dist/herb-lint.js.map +1 -1
- package/dist/index.cjs +722 -187
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +714 -189
- package/dist/index.js.map +1 -1
- package/dist/package.json +4 -4
- package/dist/src/cli/argument-parser.js +28 -22
- package/dist/src/cli/argument-parser.js.map +1 -1
- package/dist/src/cli/file-processor.js +19 -13
- package/dist/src/cli/file-processor.js.map +1 -1
- package/dist/src/cli/formatters/detailed-formatter.js +9 -9
- package/dist/src/cli/formatters/detailed-formatter.js.map +1 -1
- package/dist/src/cli/formatters/github-actions-formatter.js +50 -0
- package/dist/src/cli/formatters/github-actions-formatter.js.map +1 -0
- package/dist/src/cli/formatters/index.js +2 -0
- package/dist/src/cli/formatters/index.js.map +1 -1
- package/dist/src/cli/formatters/json-formatter.js +58 -0
- package/dist/src/cli/formatters/json-formatter.js.map +1 -0
- package/dist/src/cli/formatters/simple-formatter.js +15 -15
- package/dist/src/cli/formatters/simple-formatter.js.map +1 -1
- package/dist/src/cli/output-manager.js +120 -0
- package/dist/src/cli/output-manager.js.map +1 -0
- package/dist/src/cli/summary-reporter.js +22 -22
- package/dist/src/cli/summary-reporter.js.map +1 -1
- package/dist/src/cli.js +41 -26
- package/dist/src/cli.js.map +1 -1
- package/dist/src/default-rules.js +22 -0
- package/dist/src/default-rules.js.map +1 -1
- package/dist/src/linter.js +29 -4
- package/dist/src/linter.js.map +1 -1
- package/dist/src/rules/erb-no-empty-tags.js +2 -2
- package/dist/src/rules/erb-no-empty-tags.js.map +1 -1
- package/dist/src/rules/erb-no-output-control-flow.js +2 -2
- package/dist/src/rules/erb-no-output-control-flow.js.map +1 -1
- package/dist/src/rules/erb-no-silent-tag-in-attribute-name.js +26 -0
- package/dist/src/rules/erb-no-silent-tag-in-attribute-name.js.map +1 -0
- package/dist/src/rules/erb-prefer-image-tag-helper.js +2 -6
- package/dist/src/rules/erb-prefer-image-tag-helper.js.map +1 -1
- package/dist/src/rules/erb-require-whitespace-inside-tags.js +2 -2
- package/dist/src/rules/erb-require-whitespace-inside-tags.js.map +1 -1
- package/dist/src/rules/erb-requires-trailing-newline.js.map +1 -1
- package/dist/src/rules/html-anchor-require-href.js +2 -2
- package/dist/src/rules/html-anchor-require-href.js.map +1 -1
- package/dist/src/rules/html-aria-attribute-must-be-valid.js +13 -12
- package/dist/src/rules/html-aria-attribute-must-be-valid.js.map +1 -1
- package/dist/src/rules/html-aria-label-is-well-formatted.js +33 -0
- package/dist/src/rules/html-aria-label-is-well-formatted.js.map +1 -0
- package/dist/src/rules/html-aria-level-must-be-valid.js +28 -6
- package/dist/src/rules/html-aria-level-must-be-valid.js.map +1 -1
- package/dist/src/rules/html-aria-role-heading-requires-level.js +9 -15
- package/dist/src/rules/html-aria-role-heading-requires-level.js.map +1 -1
- package/dist/src/rules/html-aria-role-must-be-valid.js +5 -5
- package/dist/src/rules/html-aria-role-must-be-valid.js.map +1 -1
- package/dist/src/rules/html-attribute-double-quotes.js +16 -6
- package/dist/src/rules/html-attribute-double-quotes.js.map +1 -1
- package/dist/src/rules/html-attribute-equals-spacing.js +24 -0
- package/dist/src/rules/html-attribute-equals-spacing.js.map +1 -0
- package/dist/src/rules/html-attribute-values-require-quotes.js +21 -10
- package/dist/src/rules/html-attribute-values-require-quotes.js.map +1 -1
- package/dist/src/rules/html-avoid-both-disabled-and-aria-disabled.js +47 -0
- package/dist/src/rules/html-avoid-both-disabled-and-aria-disabled.js.map +1 -0
- package/dist/src/rules/html-boolean-attributes-no-value.js +11 -4
- package/dist/src/rules/html-boolean-attributes-no-value.js.map +1 -1
- package/dist/src/rules/html-iframe-has-title.js +39 -0
- package/dist/src/rules/html-iframe-has-title.js.map +1 -0
- package/dist/src/rules/html-img-require-alt.js +2 -6
- package/dist/src/rules/html-img-require-alt.js.map +1 -1
- package/dist/src/rules/html-navigation-has-label.js +43 -0
- package/dist/src/rules/html-navigation-has-label.js.map +1 -0
- package/dist/src/rules/html-no-aria-hidden-on-focusable.js +67 -0
- package/dist/src/rules/html-no-aria-hidden-on-focusable.js.map +1 -0
- package/dist/src/rules/html-no-block-inside-inline.js +4 -4
- package/dist/src/rules/html-no-block-inside-inline.js.map +1 -1
- package/dist/src/rules/html-no-duplicate-attributes.js +24 -27
- package/dist/src/rules/html-no-duplicate-attributes.js.map +1 -1
- package/dist/src/rules/html-no-duplicate-ids.js +4 -4
- package/dist/src/rules/html-no-duplicate-ids.js.map +1 -1
- package/dist/src/rules/html-no-empty-headings.js +2 -23
- package/dist/src/rules/html-no-empty-headings.js.map +1 -1
- package/dist/src/rules/html-no-nested-links.js +2 -2
- package/dist/src/rules/html-no-nested-links.js.map +1 -1
- package/dist/src/rules/html-no-positive-tab-index.js +21 -0
- package/dist/src/rules/html-no-positive-tab-index.js.map +1 -0
- package/dist/src/rules/html-no-self-closing.js +22 -0
- package/dist/src/rules/html-no-self-closing.js.map +1 -0
- package/dist/src/rules/html-no-title-attribute.js +27 -0
- package/dist/src/rules/html-no-title-attribute.js.map +1 -0
- package/dist/src/rules/html-tag-name-lowercase.js +37 -25
- package/dist/src/rules/html-tag-name-lowercase.js.map +1 -1
- package/dist/src/rules/index.js +10 -0
- package/dist/src/rules/index.js.map +1 -1
- package/dist/src/rules/parser-no-errors.js +18 -0
- package/dist/src/rules/parser-no-errors.js.map +1 -0
- package/dist/src/rules/rule-utils.js +176 -22
- package/dist/src/rules/rule-utils.js.map +1 -1
- package/dist/src/rules/svg-tag-name-capitalization.js +2 -10
- package/dist/src/rules/svg-tag-name-capitalization.js.map +1 -1
- package/dist/src/types.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/cli/argument-parser.d.ts +2 -1
- package/dist/types/cli/file-processor.d.ts +6 -5
- package/dist/types/cli/formatters/base-formatter.d.ts +2 -2
- package/dist/types/cli/formatters/detailed-formatter.d.ts +2 -2
- package/dist/types/cli/formatters/github-actions-formatter.d.ts +12 -0
- package/dist/types/cli/formatters/index.d.ts +2 -0
- package/dist/types/cli/formatters/json-formatter.d.ts +42 -0
- package/dist/types/cli/formatters/simple-formatter.d.ts +2 -2
- package/dist/types/cli/index.d.ts +4 -0
- package/dist/types/cli/output-manager.d.ts +31 -0
- package/dist/types/cli/summary-reporter.d.ts +3 -3
- package/dist/types/cli.d.ts +3 -1
- package/dist/types/rules/erb-no-empty-tags.d.ts +2 -2
- package/dist/types/rules/erb-no-output-control-flow.d.ts +2 -2
- package/dist/types/rules/erb-no-silent-tag-in-attribute-name.d.ts +7 -0
- package/dist/types/rules/erb-prefer-image-tag-helper.d.ts +2 -2
- package/dist/types/rules/erb-require-whitespace-inside-tags.d.ts +2 -2
- package/dist/types/rules/html-anchor-require-href.d.ts +2 -2
- package/dist/types/rules/html-aria-attribute-must-be-valid.d.ts +2 -2
- package/dist/types/rules/html-aria-label-is-well-formatted.d.ts +7 -0
- package/dist/types/rules/html-aria-level-must-be-valid.d.ts +2 -2
- package/dist/types/rules/html-aria-role-heading-requires-level.d.ts +2 -2
- package/dist/types/rules/html-aria-role-must-be-valid.d.ts +2 -2
- package/dist/types/rules/html-attribute-double-quotes.d.ts +2 -2
- package/dist/types/rules/html-attribute-equals-spacing.d.ts +7 -0
- package/dist/types/rules/html-attribute-values-require-quotes.d.ts +2 -2
- package/dist/types/rules/html-avoid-both-disabled-and-aria-disabled.d.ts +7 -0
- package/dist/types/rules/html-boolean-attributes-no-value.d.ts +2 -2
- package/dist/types/rules/html-iframe-has-title.d.ts +7 -0
- package/dist/types/rules/html-img-require-alt.d.ts +2 -2
- package/dist/types/rules/html-navigation-has-label.d.ts +7 -0
- package/dist/types/rules/html-no-aria-hidden-on-focusable.d.ts +7 -0
- package/dist/types/rules/html-no-block-inside-inline.d.ts +2 -2
- package/dist/types/rules/html-no-duplicate-attributes.d.ts +2 -2
- package/dist/types/rules/html-no-duplicate-ids.d.ts +2 -2
- package/dist/types/rules/html-no-empty-headings.d.ts +2 -2
- package/dist/types/rules/html-no-nested-links.d.ts +2 -2
- package/dist/types/rules/html-no-positive-tab-index.d.ts +7 -0
- package/dist/types/rules/html-no-self-closing.d.ts +7 -0
- package/dist/types/rules/html-no-title-attribute.d.ts +7 -0
- package/dist/types/rules/html-tag-name-lowercase.d.ts +3 -2
- package/dist/types/rules/index.d.ts +10 -0
- package/dist/types/rules/parser-no-errors.d.ts +8 -0
- package/dist/types/rules/rule-utils.d.ts +107 -13
- package/dist/types/rules/svg-tag-name-capitalization.d.ts +2 -2
- package/dist/types/src/cli/argument-parser.d.ts +2 -1
- package/dist/types/src/cli/file-processor.d.ts +6 -5
- package/dist/types/src/cli/formatters/base-formatter.d.ts +2 -2
- package/dist/types/src/cli/formatters/detailed-formatter.d.ts +2 -2
- package/dist/types/src/cli/formatters/github-actions-formatter.d.ts +12 -0
- package/dist/types/src/cli/formatters/index.d.ts +2 -0
- package/dist/types/src/cli/formatters/json-formatter.d.ts +42 -0
- package/dist/types/src/cli/formatters/simple-formatter.d.ts +2 -2
- package/dist/types/src/cli/output-manager.d.ts +31 -0
- package/dist/types/src/cli/summary-reporter.d.ts +3 -3
- package/dist/types/src/cli.d.ts +3 -1
- package/dist/types/src/rules/erb-no-empty-tags.d.ts +2 -2
- package/dist/types/src/rules/erb-no-output-control-flow.d.ts +2 -2
- package/dist/types/src/rules/erb-no-silent-tag-in-attribute-name.d.ts +7 -0
- package/dist/types/src/rules/erb-prefer-image-tag-helper.d.ts +2 -2
- package/dist/types/src/rules/erb-require-whitespace-inside-tags.d.ts +2 -2
- package/dist/types/src/rules/html-anchor-require-href.d.ts +2 -2
- package/dist/types/src/rules/html-aria-attribute-must-be-valid.d.ts +2 -2
- package/dist/types/src/rules/html-aria-label-is-well-formatted.d.ts +7 -0
- package/dist/types/src/rules/html-aria-level-must-be-valid.d.ts +2 -2
- package/dist/types/src/rules/html-aria-role-heading-requires-level.d.ts +2 -2
- package/dist/types/src/rules/html-aria-role-must-be-valid.d.ts +2 -2
- package/dist/types/src/rules/html-attribute-double-quotes.d.ts +2 -2
- package/dist/types/src/rules/html-attribute-equals-spacing.d.ts +7 -0
- package/dist/types/src/rules/html-attribute-values-require-quotes.d.ts +2 -2
- package/dist/types/src/rules/html-avoid-both-disabled-and-aria-disabled.d.ts +7 -0
- package/dist/types/src/rules/html-boolean-attributes-no-value.d.ts +2 -2
- package/dist/types/src/rules/html-iframe-has-title.d.ts +7 -0
- package/dist/types/src/rules/html-img-require-alt.d.ts +2 -2
- package/dist/types/src/rules/html-navigation-has-label.d.ts +7 -0
- package/dist/types/src/rules/html-no-aria-hidden-on-focusable.d.ts +7 -0
- package/dist/types/src/rules/html-no-block-inside-inline.d.ts +2 -2
- package/dist/types/src/rules/html-no-duplicate-attributes.d.ts +2 -2
- package/dist/types/src/rules/html-no-duplicate-ids.d.ts +2 -2
- package/dist/types/src/rules/html-no-empty-headings.d.ts +2 -2
- package/dist/types/src/rules/html-no-nested-links.d.ts +2 -2
- package/dist/types/src/rules/html-no-positive-tab-index.d.ts +7 -0
- package/dist/types/src/rules/html-no-self-closing.d.ts +7 -0
- package/dist/types/src/rules/html-no-title-attribute.d.ts +7 -0
- package/dist/types/src/rules/html-tag-name-lowercase.d.ts +3 -2
- package/dist/types/src/rules/index.d.ts +10 -0
- package/dist/types/src/rules/parser-no-errors.d.ts +8 -0
- package/dist/types/src/rules/rule-utils.d.ts +107 -13
- package/dist/types/src/rules/svg-tag-name-capitalization.d.ts +2 -2
- package/dist/types/src/types.d.ts +27 -3
- package/dist/types/types.d.ts +27 -3
- package/docs/rules/README.md +13 -2
- package/docs/rules/erb-no-silent-tag-in-attribute-name.md +34 -0
- package/docs/rules/html-aria-label-is-well-formatted.md +49 -0
- package/docs/rules/html-attribute-equals-spacing.md +35 -0
- package/docs/rules/html-avoid-both-disabled-and-aria-disabled.md +48 -0
- package/docs/rules/html-iframe-has-title.md +43 -0
- package/docs/rules/html-navigation-has-label.md +61 -0
- package/docs/rules/html-no-aria-hidden-on-focusable.md +54 -0
- package/docs/rules/html-no-positive-tab-index.md +55 -0
- package/docs/rules/html-no-self-closing.md +65 -0
- package/docs/rules/html-no-title-attribute.md +69 -0
- package/docs/rules/html-tag-name-lowercase.md +16 -3
- package/docs/rules/parser-no-errors.md +84 -0
- package/package.json +4 -4
- package/src/cli/argument-parser.ts +33 -24
- package/src/cli/file-processor.ts +25 -17
- package/src/cli/formatters/base-formatter.ts +2 -2
- package/src/cli/formatters/detailed-formatter.ts +9 -9
- package/src/cli/formatters/github-actions-formatter.ts +70 -0
- package/src/cli/formatters/index.ts +2 -0
- package/src/cli/formatters/json-formatter.ts +107 -0
- package/src/cli/formatters/simple-formatter.ts +15 -15
- package/src/cli/output-manager.ts +143 -0
- package/src/cli/summary-reporter.ts +24 -24
- package/src/cli.ts +48 -31
- package/src/default-rules.ts +22 -0
- package/src/linter.ts +30 -4
- package/src/rules/erb-no-empty-tags.ts +3 -3
- package/src/rules/erb-no-output-control-flow.ts +3 -3
- package/src/rules/erb-no-silent-tag-in-attribute-name.ts +40 -0
- package/src/rules/erb-prefer-image-tag-helper.ts +4 -9
- package/src/rules/erb-require-whitespace-inside-tags.ts +3 -3
- package/src/rules/erb-requires-trailing-newline.ts +2 -0
- package/src/rules/html-anchor-require-href.ts +3 -3
- package/src/rules/html-aria-attribute-must-be-valid.ts +29 -33
- package/src/rules/html-aria-label-is-well-formatted.ts +59 -0
- package/src/rules/html-aria-level-must-be-valid.ts +40 -7
- package/src/rules/html-aria-role-heading-requires-level.ts +18 -30
- package/src/rules/html-aria-role-must-be-valid.ts +7 -7
- package/src/rules/html-attribute-double-quotes.ts +23 -8
- package/src/rules/html-attribute-equals-spacing.ts +41 -0
- package/src/rules/html-attribute-values-require-quotes.ts +32 -12
- package/src/rules/html-avoid-both-disabled-and-aria-disabled.ts +66 -0
- package/src/rules/html-boolean-attributes-no-value.ts +19 -6
- package/src/rules/html-iframe-has-title.ts +62 -0
- package/src/rules/html-img-require-alt.ts +4 -9
- package/src/rules/html-navigation-has-label.ts +64 -0
- package/src/rules/html-no-aria-hidden-on-focusable.ts +90 -0
- package/src/rules/html-no-block-inside-inline.ts +5 -5
- package/src/rules/html-no-duplicate-attributes.ts +30 -30
- package/src/rules/html-no-duplicate-ids.ts +6 -5
- package/src/rules/html-no-empty-headings.ts +4 -33
- package/src/rules/html-no-nested-links.ts +3 -3
- package/src/rules/html-no-positive-tab-index.ts +33 -0
- package/src/rules/html-no-self-closing.ts +36 -0
- package/src/rules/html-no-title-attribute.ts +42 -0
- package/src/rules/html-tag-name-lowercase.ts +44 -31
- package/src/rules/index.ts +10 -0
- package/src/rules/parser-no-errors.ts +25 -0
- package/src/rules/rule-utils.ts +260 -39
- package/src/rules/svg-tag-name-capitalization.ts +4 -11
- package/src/types.ts +30 -3
|
@@ -1,46 +1,44 @@
|
|
|
1
|
-
import { BaseRuleVisitor, forEachAttribute } from "./rule-utils.js"
|
|
2
|
-
|
|
3
1
|
import { ParserRule } from "../types.js"
|
|
2
|
+
import { AttributeVisitorMixin, StaticAttributeStaticValueParams, StaticAttributeDynamicValueParams } from "./rule-utils.js"
|
|
3
|
+
|
|
4
4
|
import type { LintOffense, LintContext } from "../types.js"
|
|
5
|
-
import type { HTMLOpenTagNode,
|
|
5
|
+
import type { HTMLOpenTagNode, HTMLAttributeNode, ParseResult } from "@herb-tools/core"
|
|
6
|
+
|
|
7
|
+
class NoDuplicateAttributesVisitor extends AttributeVisitorMixin {
|
|
8
|
+
private attributeNames = new Map<string, HTMLAttributeNode[]>()
|
|
6
9
|
|
|
7
|
-
class NoDuplicateAttributesVisitor extends BaseRuleVisitor {
|
|
8
10
|
visitHTMLOpenTagNode(node: HTMLOpenTagNode): void {
|
|
9
|
-
this.
|
|
11
|
+
this.attributeNames.clear()
|
|
10
12
|
super.visitHTMLOpenTagNode(node)
|
|
13
|
+
this.reportDuplicates()
|
|
11
14
|
}
|
|
12
15
|
|
|
13
|
-
visitHTMLSelfCloseTagNode(node: HTMLSelfCloseTagNode): void {
|
|
14
|
-
this.checkDuplicateAttributes(node)
|
|
15
|
-
super.visitHTMLSelfCloseTagNode(node)
|
|
16
|
-
}
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
forEachAttribute(node, (attributeNode) => {
|
|
22
|
-
if (attributeNode.name?.type !== "AST_HTML_ATTRIBUTE_NAME_NODE") return
|
|
23
|
-
|
|
24
|
-
const nameNode = attributeNode.name as HTMLAttributeNameNode
|
|
25
|
-
if (!nameNode.name) return
|
|
17
|
+
protected checkStaticAttributeStaticValue({ attributeName, attributeNode }: StaticAttributeStaticValueParams): void {
|
|
18
|
+
this.trackAttributeName(attributeName, attributeNode)
|
|
19
|
+
}
|
|
26
20
|
|
|
27
|
-
|
|
21
|
+
protected checkStaticAttributeDynamicValue({ attributeName, attributeNode }: StaticAttributeDynamicValueParams): void {
|
|
22
|
+
this.trackAttributeName(attributeName, attributeNode)
|
|
23
|
+
}
|
|
28
24
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
private trackAttributeName(attributeName: string, attributeNode: HTMLAttributeNode): void {
|
|
26
|
+
if (!this.attributeNames.has(attributeName)) {
|
|
27
|
+
this.attributeNames.set(attributeName, [])
|
|
28
|
+
}
|
|
32
29
|
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
this.attributeNames.get(attributeName)!.push(attributeNode)
|
|
31
|
+
}
|
|
35
32
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
33
|
+
private reportDuplicates(): void {
|
|
34
|
+
for (const [attributeName, attributeNodes] of this.attributeNames) {
|
|
35
|
+
if (attributeNodes.length > 1) {
|
|
36
|
+
for (let i = 1; i < attributeNodes.length; i++) {
|
|
37
|
+
const attributeNode = attributeNodes[i]
|
|
40
38
|
|
|
41
39
|
this.addOffense(
|
|
42
40
|
`Duplicate attribute \`${attributeName}\` found on tag. Remove the duplicate occurrence.`,
|
|
43
|
-
|
|
41
|
+
attributeNode.name!.location,
|
|
44
42
|
"error"
|
|
45
43
|
)
|
|
46
44
|
}
|
|
@@ -52,9 +50,11 @@ class NoDuplicateAttributesVisitor extends BaseRuleVisitor {
|
|
|
52
50
|
export class HTMLNoDuplicateAttributesRule extends ParserRule {
|
|
53
51
|
name = "html-no-duplicate-attributes"
|
|
54
52
|
|
|
55
|
-
check(
|
|
53
|
+
check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
|
|
56
54
|
const visitor = new NoDuplicateAttributesVisitor(this.name, context)
|
|
57
|
-
|
|
55
|
+
|
|
56
|
+
visitor.visit(result.value)
|
|
57
|
+
|
|
58
58
|
return visitor.offenses
|
|
59
59
|
}
|
|
60
60
|
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { AttributeVisitorMixin } from "./rule-utils"
|
|
2
1
|
import { ParserRule } from "../types"
|
|
3
|
-
import
|
|
2
|
+
import { AttributeVisitorMixin, StaticAttributeStaticValueParams } from "./rule-utils"
|
|
3
|
+
|
|
4
|
+
import type { ParseResult } from "@herb-tools/core"
|
|
4
5
|
import type { LintOffense, LintContext } from "../types"
|
|
5
6
|
|
|
6
7
|
class NoDuplicateIdsVisitor extends AttributeVisitorMixin {
|
|
7
8
|
private documentIds: Set<string> = new Set<string>()
|
|
8
9
|
|
|
9
|
-
protected
|
|
10
|
+
protected checkStaticAttributeStaticValue({ attributeName, attributeValue, attributeNode }: StaticAttributeStaticValueParams): void {
|
|
10
11
|
if (attributeName.toLowerCase() !== "id") return
|
|
11
12
|
if (!attributeValue) return
|
|
12
13
|
|
|
@@ -29,10 +30,10 @@ class NoDuplicateIdsVisitor extends AttributeVisitorMixin {
|
|
|
29
30
|
export class HTMLNoDuplicateIdsRule extends ParserRule {
|
|
30
31
|
name = "html-no-duplicate-ids"
|
|
31
32
|
|
|
32
|
-
check(
|
|
33
|
+
check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
|
|
33
34
|
const visitor = new NoDuplicateIdsVisitor(this.name, context)
|
|
34
35
|
|
|
35
|
-
visitor.visit(
|
|
36
|
+
visitor.visit(result.value)
|
|
36
37
|
|
|
37
38
|
return visitor.offenses
|
|
38
39
|
}
|
|
@@ -2,7 +2,7 @@ import { BaseRuleVisitor, getTagName, getAttributes, findAttributeByName, getAtt
|
|
|
2
2
|
|
|
3
3
|
import { ParserRule } from "../types.js"
|
|
4
4
|
import type { LintOffense, LintContext } from "../types.js"
|
|
5
|
-
import type { HTMLElementNode, HTMLOpenTagNode,
|
|
5
|
+
import type { HTMLElementNode, HTMLOpenTagNode, ParseResult, LiteralNode, HTMLTextNode } from "@herb-tools/core"
|
|
6
6
|
|
|
7
7
|
class NoEmptyHeadingsVisitor extends BaseRuleVisitor {
|
|
8
8
|
visitHTMLElementNode(node: HTMLElementNode): void {
|
|
@@ -10,10 +10,6 @@ class NoEmptyHeadingsVisitor extends BaseRuleVisitor {
|
|
|
10
10
|
super.visitHTMLElementNode(node)
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
visitHTMLSelfCloseTagNode(node: HTMLSelfCloseTagNode): void {
|
|
14
|
-
this.checkSelfClosingHeading(node)
|
|
15
|
-
super.visitHTMLSelfCloseTagNode(node)
|
|
16
|
-
}
|
|
17
13
|
|
|
18
14
|
private checkHeadingElement(node: HTMLElementNode): void {
|
|
19
15
|
if (!node.open_tag || node.open_tag.type !== "AST_HTML_OPEN_TAG_NODE") {
|
|
@@ -47,31 +43,6 @@ class NoEmptyHeadingsVisitor extends BaseRuleVisitor {
|
|
|
47
43
|
}
|
|
48
44
|
}
|
|
49
45
|
|
|
50
|
-
private checkSelfClosingHeading(node: HTMLSelfCloseTagNode): void {
|
|
51
|
-
const tagName = getTagName(node)
|
|
52
|
-
if (!tagName) {
|
|
53
|
-
return
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Check if it's a standard heading tag (h1-h6) or has role="heading"
|
|
57
|
-
const isStandardHeading = HEADING_TAGS.has(tagName)
|
|
58
|
-
const isAriaHeading = this.hasHeadingRole(node)
|
|
59
|
-
|
|
60
|
-
if (!isStandardHeading && !isAriaHeading) {
|
|
61
|
-
return
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Self-closing headings are always empty
|
|
65
|
-
const elementDescription = isStandardHeading
|
|
66
|
-
? `\`<${tagName}>\``
|
|
67
|
-
: `\`<${tagName} role="heading">\``
|
|
68
|
-
|
|
69
|
-
this.addOffense(
|
|
70
|
-
`Heading element ${elementDescription} must not be empty. Provide accessible text content for screen readers and SEO.`,
|
|
71
|
-
node.tag_name!.location,
|
|
72
|
-
"error"
|
|
73
|
-
)
|
|
74
|
-
}
|
|
75
46
|
|
|
76
47
|
private isEmptyHeading(node: HTMLElementNode): boolean {
|
|
77
48
|
if (!node.body || node.body.length === 0) {
|
|
@@ -114,7 +85,7 @@ class NoEmptyHeadingsVisitor extends BaseRuleVisitor {
|
|
|
114
85
|
return !hasAccessibleContent
|
|
115
86
|
}
|
|
116
87
|
|
|
117
|
-
private hasHeadingRole(node: HTMLOpenTagNode
|
|
88
|
+
private hasHeadingRole(node: HTMLOpenTagNode): boolean {
|
|
118
89
|
const attributes = getAttributes(node)
|
|
119
90
|
const roleAttribute = findAttributeByName(attributes, "role")
|
|
120
91
|
|
|
@@ -178,9 +149,9 @@ class NoEmptyHeadingsVisitor extends BaseRuleVisitor {
|
|
|
178
149
|
export class HTMLNoEmptyHeadingsRule extends ParserRule {
|
|
179
150
|
name = "html-no-empty-headings"
|
|
180
151
|
|
|
181
|
-
check(
|
|
152
|
+
check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
|
|
182
153
|
const visitor = new NoEmptyHeadingsVisitor(this.name, context)
|
|
183
|
-
visitor.visit(
|
|
154
|
+
visitor.visit(result.value)
|
|
184
155
|
return visitor.offenses
|
|
185
156
|
}
|
|
186
157
|
}
|
|
@@ -2,7 +2,7 @@ import { BaseRuleVisitor, getTagName } from "./rule-utils.js"
|
|
|
2
2
|
|
|
3
3
|
import { ParserRule } from "../types.js"
|
|
4
4
|
import type { LintOffense, LintContext } from "../types.js"
|
|
5
|
-
import type { HTMLOpenTagNode, HTMLElementNode,
|
|
5
|
+
import type { HTMLOpenTagNode, HTMLElementNode, ParseResult } from "@herb-tools/core"
|
|
6
6
|
|
|
7
7
|
class NestedLinkVisitor extends BaseRuleVisitor {
|
|
8
8
|
private linkStack: HTMLOpenTagNode[] = []
|
|
@@ -58,9 +58,9 @@ class NestedLinkVisitor extends BaseRuleVisitor {
|
|
|
58
58
|
export class HTMLNoNestedLinksRule extends ParserRule {
|
|
59
59
|
name = "html-no-nested-links"
|
|
60
60
|
|
|
61
|
-
check(
|
|
61
|
+
check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
|
|
62
62
|
const visitor = new NestedLinkVisitor(this.name, context)
|
|
63
|
-
visitor.visit(
|
|
63
|
+
visitor.visit(result.value)
|
|
64
64
|
return visitor.offenses
|
|
65
65
|
}
|
|
66
66
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ParserRule } from "../types.js"
|
|
2
|
+
import { AttributeVisitorMixin, StaticAttributeStaticValueParams } from "./rule-utils.js"
|
|
3
|
+
|
|
4
|
+
import type { LintOffense, LintContext } from "../types.js"
|
|
5
|
+
import type { ParseResult } from "@herb-tools/core"
|
|
6
|
+
|
|
7
|
+
class NoPositiveTabIndexVisitor extends AttributeVisitorMixin {
|
|
8
|
+
protected checkStaticAttributeStaticValue({ attributeName, attributeValue, attributeNode }: StaticAttributeStaticValueParams): void {
|
|
9
|
+
if (attributeName !== "tabindex") return
|
|
10
|
+
|
|
11
|
+
const tabIndexValue = parseInt(attributeValue, 10)
|
|
12
|
+
|
|
13
|
+
if (!isNaN(tabIndexValue) && tabIndexValue > 0) {
|
|
14
|
+
this.addOffense(
|
|
15
|
+
`Do not use positive \`tabindex\` values as they are error prone and can severely disrupt navigation experience for keyboard users. Use \`tabindex="0"\` to make an element focusable or \`tabindex=\"-1\"\` to remove it from the tab sequence.`,
|
|
16
|
+
attributeNode.location,
|
|
17
|
+
"error"
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class HTMLNoPositiveTabIndexRule extends ParserRule {
|
|
24
|
+
name = "html-no-positive-tab-index"
|
|
25
|
+
|
|
26
|
+
check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
|
|
27
|
+
const visitor = new NoPositiveTabIndexVisitor(this.name, context)
|
|
28
|
+
|
|
29
|
+
visitor.visit(result.value)
|
|
30
|
+
|
|
31
|
+
return visitor.offenses
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ParserRule } from "../types.js"
|
|
2
|
+
import { BaseRuleVisitor, getTagName, isVoidElement } from "./rule-utils.js"
|
|
3
|
+
|
|
4
|
+
import type { LintContext, LintOffense } from "../types.js"
|
|
5
|
+
import type { HTMLOpenTagNode, ParseResult } from "@herb-tools/core"
|
|
6
|
+
|
|
7
|
+
class NoSelfClosingVisitor extends BaseRuleVisitor {
|
|
8
|
+
visitHTMLOpenTagNode(node: HTMLOpenTagNode): void {
|
|
9
|
+
if (node.tag_closing?.value === "/>") {
|
|
10
|
+
const tagName = getTagName(node)
|
|
11
|
+
|
|
12
|
+
const shouldBeVoid = tagName ? isVoidElement(tagName) : false
|
|
13
|
+
const instead = shouldBeVoid ? `Use \`<${tagName}>\` instead.` : `Use \`<${tagName}></${tagName}>\` instead.`
|
|
14
|
+
|
|
15
|
+
this.addOffense(
|
|
16
|
+
`Self-closing syntax \`<${tagName} />\` is not allowed in HTML. ${instead}`,
|
|
17
|
+
node.location,
|
|
18
|
+
"error"
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
super.visitHTMLOpenTagNode(node)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class HTMLNoSelfClosingRule extends ParserRule {
|
|
27
|
+
name = "html-no-self-closing"
|
|
28
|
+
|
|
29
|
+
check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
|
|
30
|
+
const visitor = new NoSelfClosingVisitor(this.name, context)
|
|
31
|
+
|
|
32
|
+
visitor.visit(result.value)
|
|
33
|
+
|
|
34
|
+
return visitor.offenses
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ParserRule } from "../types.js"
|
|
2
|
+
import { BaseRuleVisitor, getTagName, hasAttribute } from "./rule-utils.js"
|
|
3
|
+
|
|
4
|
+
import type { LintOffense, LintContext } from "../types.js"
|
|
5
|
+
import type { HTMLOpenTagNode, ParseResult } from "@herb-tools/core"
|
|
6
|
+
|
|
7
|
+
class NoTitleAttributeVisitor extends BaseRuleVisitor {
|
|
8
|
+
ALLOWED_ELEMENTS_WITH_TITLE = new Set(["iframe", "link"])
|
|
9
|
+
|
|
10
|
+
visitHTMLOpenTagNode(node: HTMLOpenTagNode): void {
|
|
11
|
+
this.checkTitleAttribute(node)
|
|
12
|
+
super.visitHTMLOpenTagNode(node)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
private checkTitleAttribute(node: HTMLOpenTagNode): void {
|
|
16
|
+
const tagName = getTagName(node)
|
|
17
|
+
|
|
18
|
+
if (!tagName || this.ALLOWED_ELEMENTS_WITH_TITLE.has(tagName)) {
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (hasAttribute(node, "title")) {
|
|
23
|
+
this.addOffense(
|
|
24
|
+
"The `title` attribute should never be used as it is inaccessible for several groups of users. Use `aria-label` or `aria-describedby` instead. Exceptions are provided for `<iframe>` and `<link>` elements.",
|
|
25
|
+
node.tag_name!.location,
|
|
26
|
+
"error"
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class HTMLNoTitleAttributeRule extends ParserRule {
|
|
33
|
+
name = "html-no-title-attribute"
|
|
34
|
+
|
|
35
|
+
check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
|
|
36
|
+
const visitor = new NoTitleAttributeVisitor(this.name, context)
|
|
37
|
+
|
|
38
|
+
visitor.visit(result.value)
|
|
39
|
+
|
|
40
|
+
return visitor.offenses
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -1,53 +1,56 @@
|
|
|
1
|
+
import { ParserRule } from "../types.js"
|
|
1
2
|
import { BaseRuleVisitor } from "./rule-utils.js"
|
|
2
3
|
|
|
3
|
-
import {
|
|
4
|
+
import { isNode, getTagName, HTMLElementNode, HTMLOpenTagNode, HTMLCloseTagNode, ParseResult, XMLDeclarationNode, Node } from "@herb-tools/core"
|
|
5
|
+
|
|
4
6
|
import type { LintOffense, LintContext } from "../types.js"
|
|
5
|
-
import type { HTMLElementNode, HTMLOpenTagNode, HTMLCloseTagNode, HTMLSelfCloseTagNode, Node } from "@herb-tools/core"
|
|
6
7
|
|
|
7
|
-
class
|
|
8
|
-
|
|
9
|
-
const tagName = node.tag_name?.value
|
|
8
|
+
class XMLDeclarationChecker extends BaseRuleVisitor {
|
|
9
|
+
hasXMLDeclaration: boolean = false
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
visitXMLDeclarationNode(_node: XMLDeclarationNode): void {
|
|
12
|
+
this.hasXMLDeclaration = true
|
|
13
|
+
}
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
visitChildNodes(node: Node): void {
|
|
16
|
+
if (this.hasXMLDeclaration) return
|
|
17
|
+
super.visitChildNodes(node)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
class TagNameLowercaseVisitor extends BaseRuleVisitor {
|
|
22
|
+
visitHTMLElementNode(node: HTMLElementNode): void {
|
|
23
|
+
if (getTagName(node).toLowerCase() === "svg") {
|
|
24
|
+
this.checkTagName(node.open_tag)
|
|
25
|
+
this.checkTagName(node.close_tag)
|
|
26
|
+
} else {
|
|
27
|
+
super.visitHTMLElementNode(node)
|
|
21
28
|
}
|
|
29
|
+
}
|
|
22
30
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (node.close_tag) {
|
|
26
|
-
this.checkTagName(node.close_tag as HTMLCloseTagNode)
|
|
27
|
-
}
|
|
31
|
+
visitHTMLOpenTagNode(node: HTMLOpenTagNode) {
|
|
32
|
+
this.checkTagName(node)
|
|
28
33
|
}
|
|
29
34
|
|
|
30
|
-
|
|
35
|
+
visitHTMLCloseTagNode(node: HTMLCloseTagNode) {
|
|
31
36
|
this.checkTagName(node)
|
|
32
|
-
this.visitChildNodes(node)
|
|
33
37
|
}
|
|
34
38
|
|
|
35
|
-
private checkTagName(node: HTMLOpenTagNode | HTMLCloseTagNode |
|
|
36
|
-
|
|
39
|
+
private checkTagName(node: HTMLOpenTagNode | HTMLCloseTagNode | null): void {
|
|
40
|
+
if (!node) return
|
|
41
|
+
|
|
42
|
+
const tagName = getTagName(node)
|
|
37
43
|
|
|
38
44
|
if (!tagName) return
|
|
39
45
|
|
|
40
46
|
const lowercaseTagName = tagName.toLowerCase()
|
|
41
47
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (node.type == "AST_HTML_OPEN_TAG_NODE") type = "Opening"
|
|
46
|
-
if (node.type == "AST_HTML_CLOSE_TAG_NODE") type = "Closing"
|
|
47
|
-
if (node.type == "AST_HTML_SELF_CLOSE_TAG_NODE") type = "Self-closing"
|
|
48
|
+
const type = isNode(node, HTMLOpenTagNode) ? "Opening" : "Closing"
|
|
49
|
+
const open = isNode(node, HTMLOpenTagNode) ? "<" : "</"
|
|
48
50
|
|
|
51
|
+
if (tagName !== lowercaseTagName) {
|
|
49
52
|
this.addOffense(
|
|
50
|
-
`${type} tag name \`${tagName}
|
|
53
|
+
`${type} tag name \`${open}${tagName}>\` should be lowercase. Use \`${open}${lowercaseTagName}>\` instead.`,
|
|
51
54
|
node.tag_name!.location,
|
|
52
55
|
"error"
|
|
53
56
|
)
|
|
@@ -58,9 +61,19 @@ class TagNameLowercaseVisitor extends BaseRuleVisitor {
|
|
|
58
61
|
export class HTMLTagNameLowercaseRule extends ParserRule {
|
|
59
62
|
name = "html-tag-name-lowercase"
|
|
60
63
|
|
|
61
|
-
|
|
64
|
+
isEnabled(result: ParseResult, context?: Partial<LintContext>): boolean {
|
|
65
|
+
if (context?.fileName?.endsWith(".xml") || context?.fileName?.endsWith(".xml.erb")) {
|
|
66
|
+
return false
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const checker = new XMLDeclarationChecker(this.name)
|
|
70
|
+
checker.visit(result.value)
|
|
71
|
+
return !checker.hasXMLDeclaration
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
check(result: ParseResult, context?: Partial<LintContext>): LintOffense[] {
|
|
62
75
|
const visitor = new TagNameLowercaseVisitor(this.name, context)
|
|
63
|
-
visitor.visit(
|
|
76
|
+
visitor.visit(result.value)
|
|
64
77
|
return visitor.offenses
|
|
65
78
|
}
|
|
66
79
|
}
|
package/src/rules/index.ts
CHANGED
|
@@ -1,19 +1,29 @@
|
|
|
1
1
|
export * from "./erb-no-empty-tags.js"
|
|
2
2
|
export * from "./erb-no-output-control-flow.js"
|
|
3
|
+
export * from "./erb-no-silent-tag-in-attribute-name.js"
|
|
3
4
|
export * from "./erb-prefer-image-tag-helper.js"
|
|
4
5
|
export * from "./erb-requires-trailing-newline.js"
|
|
5
6
|
export * from "./html-anchor-require-href.js"
|
|
7
|
+
export * from "./html-aria-label-is-well-formatted.js"
|
|
6
8
|
export * from "./html-aria-level-must-be-valid.js"
|
|
7
9
|
export * from "./html-aria-role-heading-requires-level.js"
|
|
8
10
|
export * from "./html-aria-role-must-be-valid.js"
|
|
9
11
|
export * from "./html-attribute-double-quotes.js"
|
|
12
|
+
export * from "./html-attribute-equals-spacing.js"
|
|
10
13
|
export * from "./html-attribute-values-require-quotes.js"
|
|
14
|
+
export * from "./html-avoid-both-disabled-and-aria-disabled.js"
|
|
11
15
|
export * from "./html-boolean-attributes-no-value.js"
|
|
16
|
+
export * from "./html-iframe-has-title.js"
|
|
12
17
|
export * from "./html-img-require-alt.js"
|
|
18
|
+
export * from "./html-navigation-has-label.js"
|
|
19
|
+
export * from "./html-no-aria-hidden-on-focusable.js"
|
|
13
20
|
export * from "./html-no-block-inside-inline.js"
|
|
14
21
|
export * from "./html-no-duplicate-attributes.js"
|
|
15
22
|
export * from "./html-no-duplicate-ids.js"
|
|
16
23
|
export * from "./html-no-empty-headings.js"
|
|
17
24
|
export * from "./html-no-nested-links.js"
|
|
25
|
+
export * from "./html-no-positive-tab-index.js"
|
|
26
|
+
export * from "./html-no-self-closing.js"
|
|
27
|
+
export * from "./html-no-title-attribute.js"
|
|
18
28
|
export * from "./html-tag-name-lowercase.js"
|
|
19
29
|
export * from "./svg-tag-name-capitalization.js"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ParserRule } from "../types.js"
|
|
2
|
+
|
|
3
|
+
import type { LintOffense } from "../types.js"
|
|
4
|
+
import type { ParseResult, HerbError } from "@herb-tools/core"
|
|
5
|
+
|
|
6
|
+
export class ParserNoErrorsRule extends ParserRule {
|
|
7
|
+
name = "parser-no-errors"
|
|
8
|
+
|
|
9
|
+
check(result: ParseResult): LintOffense[] {
|
|
10
|
+
return result.recursiveErrors().map(error =>
|
|
11
|
+
this.herbErrorToLintOffense(error)
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
private herbErrorToLintOffense(error: HerbError): LintOffense {
|
|
16
|
+
return {
|
|
17
|
+
message: `${error.message} (\`${error.type}\`)`,
|
|
18
|
+
location: error.location,
|
|
19
|
+
severity: error.severity,
|
|
20
|
+
rule: this.name,
|
|
21
|
+
code: this.name,
|
|
22
|
+
source: "linter"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|