@herb-tools/linter 0.6.0 → 0.7.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 +60 -16
- package/dist/herb-lint.js +1684 -295
- package/dist/herb-lint.js.map +1 -1
- package/dist/index.cjs +1226 -158
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1188 -160
- package/dist/index.js.map +1 -1
- package/dist/package.json +11 -4
- package/dist/src/cli/argument-parser.js +11 -6
- package/dist/src/cli/argument-parser.js.map +1 -1
- package/dist/src/cli/file-processor.js +5 -6
- package/dist/src/cli/file-processor.js.map +1 -1
- package/dist/src/cli/formatters/detailed-formatter.js +3 -5
- package/dist/src/cli/formatters/detailed-formatter.js.map +1 -1
- package/dist/src/cli/formatters/github-actions-formatter.js +55 -11
- package/dist/src/cli/formatters/github-actions-formatter.js.map +1 -1
- package/dist/src/cli/index.js +1 -0
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/cli/output-manager.js +23 -5
- package/dist/src/cli/output-manager.js.map +1 -1
- package/dist/src/cli/summary-reporter.js +2 -11
- package/dist/src/cli/summary-reporter.js.map +1 -1
- package/dist/src/cli.js +88 -4
- package/dist/src/cli.js.map +1 -1
- package/dist/src/default-rules.js +8 -4
- package/dist/src/default-rules.js.map +1 -1
- package/dist/src/linter.js.map +1 -1
- package/dist/src/rules/erb-prefer-image-tag-helper.js +50 -60
- package/dist/src/rules/erb-prefer-image-tag-helper.js.map +1 -1
- package/dist/src/rules/html-boolean-attributes-no-value.js +8 -8
- package/dist/src/rules/html-boolean-attributes-no-value.js.map +1 -1
- package/dist/src/rules/html-no-duplicate-ids.js +134 -9
- package/dist/src/rules/html-no-duplicate-ids.js.map +1 -1
- package/dist/src/rules/html-no-empty-attributes.js +56 -0
- package/dist/src/rules/html-no-empty-attributes.js.map +1 -0
- package/dist/src/rules/html-no-positive-tab-index.js +1 -1
- package/dist/src/rules/html-no-positive-tab-index.js.map +1 -1
- package/dist/src/rules/html-no-self-closing.js +12 -5
- package/dist/src/rules/html-no-self-closing.js.map +1 -1
- package/dist/src/rules/html-no-underscores-in-attribute-names.js +36 -0
- package/dist/src/rules/html-no-underscores-in-attribute-names.js.map +1 -0
- package/dist/src/rules/index.js +3 -0
- package/dist/src/rules/index.js.map +1 -1
- package/dist/src/rules/rule-utils.js +80 -7
- package/dist/src/rules/rule-utils.js.map +1 -1
- package/dist/src/rules/svg-tag-name-capitalization.js +2 -2
- package/dist/src/rules/svg-tag-name-capitalization.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 -1
- package/dist/types/cli/formatters/github-actions-formatter.d.ts +6 -1
- package/dist/types/cli/index.d.ts +1 -0
- package/dist/types/cli/output-manager.d.ts +1 -0
- package/dist/types/cli.d.ts +20 -5
- package/dist/types/linter.d.ts +7 -7
- package/dist/types/rules/html-no-empty-attributes.d.ts +7 -0
- package/dist/types/rules/html-no-underscores-in-attribute-names.d.ts +7 -0
- package/dist/types/rules/index.d.ts +3 -0
- package/dist/types/rules/rule-utils.d.ts +46 -5
- package/dist/types/src/cli/argument-parser.d.ts +2 -1
- package/dist/types/src/cli/file-processor.d.ts +6 -1
- package/dist/types/src/cli/formatters/github-actions-formatter.d.ts +6 -1
- package/dist/types/src/cli/index.d.ts +1 -0
- package/dist/types/src/cli/output-manager.d.ts +1 -0
- package/dist/types/src/cli.d.ts +20 -5
- package/dist/types/src/linter.d.ts +7 -7
- package/dist/types/src/rules/html-no-empty-attributes.d.ts +7 -0
- package/dist/types/src/rules/html-no-underscores-in-attribute-names.d.ts +7 -0
- package/dist/types/src/rules/index.d.ts +3 -0
- package/dist/types/src/rules/rule-utils.d.ts +46 -5
- package/docs/rules/README.md +2 -0
- package/docs/rules/html-img-require-alt.md +0 -2
- package/docs/rules/html-no-empty-attributes.md +77 -0
- package/docs/rules/html-no-underscores-in-attribute-names.md +45 -0
- package/package.json +11 -4
- package/src/cli/argument-parser.ts +15 -7
- package/src/cli/file-processor.ts +11 -7
- package/src/cli/formatters/detailed-formatter.ts +5 -7
- package/src/cli/formatters/github-actions-formatter.ts +64 -11
- package/src/cli/index.ts +2 -0
- package/src/cli/output-manager.ts +27 -5
- package/src/cli/summary-reporter.ts +3 -11
- package/src/cli.ts +125 -20
- package/src/default-rules.ts +8 -4
- package/src/linter.ts +6 -6
- package/src/rules/erb-no-silent-tag-in-attribute-name.ts +1 -1
- package/src/rules/erb-prefer-image-tag-helper.ts +53 -71
- package/src/rules/erb-require-whitespace-inside-tags.ts +2 -2
- package/src/rules/html-attribute-double-quotes.ts +1 -1
- package/src/rules/html-boolean-attributes-no-value.ts +9 -11
- package/src/rules/html-no-duplicate-ids.ts +188 -14
- package/src/rules/html-no-empty-attributes.ts +75 -0
- package/src/rules/html-no-positive-tab-index.ts +1 -1
- package/src/rules/html-no-self-closing.ts +13 -8
- package/src/rules/html-no-underscores-in-attribute-names.ts +58 -0
- package/src/rules/html-tag-name-lowercase.ts +1 -1
- package/src/rules/index.ts +3 -0
- package/src/rules/rule-utils.ts +110 -9
- package/src/rules/svg-tag-name-capitalization.ts +2 -2
package/src/rules/rule-utils.ts
CHANGED
|
@@ -26,8 +26,14 @@ import type {
|
|
|
26
26
|
|
|
27
27
|
import { DEFAULT_LINT_CONTEXT } from "../types.js"
|
|
28
28
|
|
|
29
|
+
import type * as Nodes from "@herb-tools/core"
|
|
29
30
|
import type { LintOffense, LintSeverity, LintContext } from "../types.js"
|
|
30
31
|
|
|
32
|
+
export enum ControlFlowType {
|
|
33
|
+
CONDITIONAL,
|
|
34
|
+
LOOP
|
|
35
|
+
}
|
|
36
|
+
|
|
31
37
|
/**
|
|
32
38
|
* Base visitor class that provides common functionality for rule visitors
|
|
33
39
|
*/
|
|
@@ -65,6 +71,95 @@ export abstract class BaseRuleVisitor extends Visitor {
|
|
|
65
71
|
}
|
|
66
72
|
}
|
|
67
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Mixin that adds control flow tracking capabilities to rule visitors
|
|
76
|
+
* This allows rules to track state across different control flow structures
|
|
77
|
+
* like if/else branches, loops, etc.
|
|
78
|
+
*
|
|
79
|
+
* @template TControlFlowState - Type for state passed between onEnterControlFlow and onExitControlFlow
|
|
80
|
+
* @template TBranchState - Type for state passed between onEnterBranch and onExitBranch
|
|
81
|
+
*/
|
|
82
|
+
export abstract class ControlFlowTrackingVisitor<TControlFlowState = any, TBranchState = any> extends BaseRuleVisitor {
|
|
83
|
+
protected isInControlFlow: boolean = false
|
|
84
|
+
protected currentControlFlowType: ControlFlowType | null = null
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Handle visiting a control flow node with proper scope management
|
|
88
|
+
*/
|
|
89
|
+
protected handleControlFlowNode(node: Node, controlFlowType: ControlFlowType, visitChildren: () => void): void {
|
|
90
|
+
const wasInControlFlow = this.isInControlFlow
|
|
91
|
+
const previousControlFlowType = this.currentControlFlowType
|
|
92
|
+
|
|
93
|
+
this.isInControlFlow = true
|
|
94
|
+
this.currentControlFlowType = controlFlowType
|
|
95
|
+
|
|
96
|
+
const stateToRestore = this.onEnterControlFlow(controlFlowType, wasInControlFlow)
|
|
97
|
+
|
|
98
|
+
visitChildren()
|
|
99
|
+
|
|
100
|
+
this.onExitControlFlow(controlFlowType, wasInControlFlow, stateToRestore)
|
|
101
|
+
|
|
102
|
+
this.isInControlFlow = wasInControlFlow
|
|
103
|
+
this.currentControlFlowType = previousControlFlowType
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Handle visiting a branch node (like else, when) with proper scope management
|
|
108
|
+
*/
|
|
109
|
+
protected startNewBranch(visitChildren: () => void): void {
|
|
110
|
+
const stateToRestore = this.onEnterBranch()
|
|
111
|
+
|
|
112
|
+
visitChildren()
|
|
113
|
+
|
|
114
|
+
this.onExitBranch(stateToRestore)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
visitERBIfNode(node: Nodes.ERBIfNode): void {
|
|
118
|
+
this.handleControlFlowNode(node, ControlFlowType.CONDITIONAL, () => super.visitERBIfNode(node))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
visitERBUnlessNode(node: Nodes.ERBUnlessNode): void {
|
|
122
|
+
this.handleControlFlowNode(node, ControlFlowType.CONDITIONAL, () => super.visitERBUnlessNode(node))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
visitERBCaseNode(node: Nodes.ERBCaseNode): void {
|
|
126
|
+
this.handleControlFlowNode(node, ControlFlowType.CONDITIONAL, () => super.visitERBCaseNode(node))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
visitERBCaseMatchNode(node: Nodes.ERBCaseMatchNode): void {
|
|
130
|
+
this.handleControlFlowNode(node, ControlFlowType.CONDITIONAL, () => super.visitERBCaseMatchNode(node))
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
visitERBWhileNode(node: Nodes.ERBWhileNode): void {
|
|
134
|
+
this.handleControlFlowNode(node, ControlFlowType.LOOP, () => super.visitERBWhileNode(node))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
visitERBForNode(node: Nodes.ERBForNode): void {
|
|
138
|
+
this.handleControlFlowNode(node, ControlFlowType.LOOP, () => super.visitERBForNode(node))
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
visitERBUntilNode(node: Nodes.ERBUntilNode): void {
|
|
142
|
+
this.handleControlFlowNode(node, ControlFlowType.LOOP, () => super.visitERBUntilNode(node))
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
visitERBBlockNode(node: Nodes.ERBBlockNode): void {
|
|
146
|
+
this.handleControlFlowNode(node, ControlFlowType.CONDITIONAL, () => super.visitERBBlockNode(node))
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
visitERBElseNode(node: Nodes.ERBElseNode): void {
|
|
150
|
+
this.startNewBranch(() => super.visitERBElseNode(node))
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
visitERBWhenNode(node: Nodes.ERBWhenNode): void {
|
|
154
|
+
this.startNewBranch(() => super.visitERBWhenNode(node))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
protected abstract onEnterControlFlow(controlFlowType: ControlFlowType, wasAlreadyInControlFlow: boolean): TControlFlowState
|
|
158
|
+
protected abstract onExitControlFlow(controlFlowType: ControlFlowType, wasAlreadyInControlFlow: boolean, stateToRestore: TControlFlowState): void
|
|
159
|
+
protected abstract onEnterBranch(): TBranchState
|
|
160
|
+
protected abstract onExitBranch(stateToRestore: TBranchState): void
|
|
161
|
+
}
|
|
162
|
+
|
|
68
163
|
/**
|
|
69
164
|
* Gets attributes from an HTMLOpenTagNode
|
|
70
165
|
*/
|
|
@@ -83,11 +178,13 @@ export function getTagName(node: HTMLOpenTagNode): string | null {
|
|
|
83
178
|
* Gets the attribute name from an HTMLAttributeNode (lowercased)
|
|
84
179
|
* Returns null if the attribute name contains dynamic content (ERB)
|
|
85
180
|
*/
|
|
86
|
-
export function getAttributeName(attributeNode: HTMLAttributeNode): string | null {
|
|
181
|
+
export function getAttributeName(attributeNode: HTMLAttributeNode, lowercase = true): string | null {
|
|
87
182
|
if (attributeNode.name?.type === "AST_HTML_ATTRIBUTE_NAME_NODE") {
|
|
88
183
|
const nameNode = attributeNode.name as HTMLAttributeNameNode
|
|
89
184
|
const staticName = getStaticAttributeName(nameNode)
|
|
90
185
|
|
|
186
|
+
if (!lowercase) return staticName
|
|
187
|
+
|
|
91
188
|
return staticName ? staticName.toLowerCase() : null
|
|
92
189
|
}
|
|
93
190
|
|
|
@@ -190,7 +287,7 @@ export function getStaticAttributeValueContent(attributeNode: HTMLAttributeNode)
|
|
|
190
287
|
* Gets the attribute value content from an HTMLAttributeValueNode
|
|
191
288
|
*/
|
|
192
289
|
export function getAttributeValue(attributeNode: HTMLAttributeNode): string | null {
|
|
193
|
-
const valueNode: HTMLAttributeValueNode |
|
|
290
|
+
const valueNode: HTMLAttributeValueNode | null = attributeNode.value as HTMLAttributeValueNode
|
|
194
291
|
|
|
195
292
|
if (valueNode === null) return null
|
|
196
293
|
|
|
@@ -284,7 +381,7 @@ export function hasAttribute(node: HTMLOpenTagNode, attributeName: string): bool
|
|
|
284
381
|
/**
|
|
285
382
|
* Checks if a tag has a specific attribute
|
|
286
383
|
*/
|
|
287
|
-
export function getAttribute(node: HTMLOpenTagNode, attributeName: string): HTMLAttributeNode |
|
|
384
|
+
export function getAttribute(node: HTMLOpenTagNode, attributeName: string): HTMLAttributeNode | null {
|
|
288
385
|
const attributes = getAttributes(node)
|
|
289
386
|
|
|
290
387
|
return findAttributeByName(attributes, attributeName)
|
|
@@ -389,6 +486,7 @@ export interface StaticAttributeStaticValueParams {
|
|
|
389
486
|
attributeName: string
|
|
390
487
|
attributeValue: string
|
|
391
488
|
attributeNode: HTMLAttributeNode
|
|
489
|
+
originalAttributeName: string
|
|
392
490
|
parentNode: HTMLOpenTagNode
|
|
393
491
|
}
|
|
394
492
|
|
|
@@ -396,6 +494,7 @@ export interface StaticAttributeDynamicValueParams {
|
|
|
396
494
|
attributeName: string
|
|
397
495
|
valueNodes: Node[]
|
|
398
496
|
attributeNode: HTMLAttributeNode
|
|
497
|
+
originalAttributeName: string
|
|
399
498
|
parentNode: HTMLOpenTagNode
|
|
400
499
|
combinedValue?: string | null
|
|
401
500
|
}
|
|
@@ -535,6 +634,7 @@ export abstract class AttributeVisitorMixin extends BaseRuleVisitor {
|
|
|
535
634
|
private checkAttributesOnNode(node: HTMLOpenTagNode): void {
|
|
536
635
|
forEachAttribute(node, (attributeNode) => {
|
|
537
636
|
const staticAttributeName = getAttributeName(attributeNode)
|
|
637
|
+
const originalAttributeName = getAttributeName(attributeNode, false) || ""
|
|
538
638
|
const isDynamicName = hasDynamicAttributeName(attributeNode)
|
|
539
639
|
const staticAttributeValue = getStaticAttributeValue(attributeNode)
|
|
540
640
|
const valueNodes = getAttributeValueNodes(attributeNode)
|
|
@@ -546,16 +646,17 @@ export abstract class AttributeVisitorMixin extends BaseRuleVisitor {
|
|
|
546
646
|
attributeName: staticAttributeName,
|
|
547
647
|
attributeValue: staticAttributeValue,
|
|
548
648
|
attributeNode,
|
|
649
|
+
originalAttributeName,
|
|
549
650
|
parentNode: node
|
|
550
651
|
})
|
|
551
652
|
} else if (staticAttributeName && isEffectivelyStaticValue && !hasOutputERB) {
|
|
552
653
|
const validatableContent = getValidatableStaticContent(valueNodes) || ""
|
|
553
654
|
|
|
554
|
-
this.checkStaticAttributeStaticValue({ attributeName: staticAttributeName, attributeValue: validatableContent, attributeNode, parentNode: node })
|
|
655
|
+
this.checkStaticAttributeStaticValue({ attributeName: staticAttributeName, attributeValue: validatableContent, attributeNode, originalAttributeName, parentNode: node })
|
|
555
656
|
} else if (staticAttributeName && hasOutputERB) {
|
|
556
657
|
const combinedValue = getAttributeValue(attributeNode)
|
|
557
658
|
|
|
558
|
-
this.checkStaticAttributeDynamicValue({ attributeName: staticAttributeName, valueNodes, attributeNode, parentNode: node, combinedValue })
|
|
659
|
+
this.checkStaticAttributeDynamicValue({ attributeName: staticAttributeName, valueNodes, attributeNode, parentNode: node, originalAttributeName, combinedValue })
|
|
559
660
|
} else if (isDynamicName && staticAttributeValue !== null) {
|
|
560
661
|
const nameNode = attributeNode.name as HTMLAttributeNameNode
|
|
561
662
|
const nameNodes = nameNode.children || []
|
|
@@ -576,28 +677,28 @@ export abstract class AttributeVisitorMixin extends BaseRuleVisitor {
|
|
|
576
677
|
/**
|
|
577
678
|
* Static attribute name with static value: class="container"
|
|
578
679
|
*/
|
|
579
|
-
protected checkStaticAttributeStaticValue(
|
|
680
|
+
protected checkStaticAttributeStaticValue(_params: StaticAttributeStaticValueParams): void {
|
|
580
681
|
// Default implementation does nothing
|
|
581
682
|
}
|
|
582
683
|
|
|
583
684
|
/**
|
|
584
685
|
* Static attribute name with dynamic value: class="<%= css_class %>"
|
|
585
686
|
*/
|
|
586
|
-
protected checkStaticAttributeDynamicValue(
|
|
687
|
+
protected checkStaticAttributeDynamicValue(_params: StaticAttributeDynamicValueParams): void {
|
|
587
688
|
// Default implementation does nothing
|
|
588
689
|
}
|
|
589
690
|
|
|
590
691
|
/**
|
|
591
692
|
* Dynamic attribute name with static value: data-<%= key %>="foo"
|
|
592
693
|
*/
|
|
593
|
-
protected checkDynamicAttributeStaticValue(
|
|
694
|
+
protected checkDynamicAttributeStaticValue(_params: DynamicAttributeStaticValueParams): void {
|
|
594
695
|
// Default implementation does nothing
|
|
595
696
|
}
|
|
596
697
|
|
|
597
698
|
/**
|
|
598
699
|
* Dynamic attribute name with dynamic value: data-<%= key %>="<%= value %>"
|
|
599
700
|
*/
|
|
600
|
-
protected checkDynamicAttributeDynamicValue(
|
|
701
|
+
protected checkDynamicAttributeDynamicValue(_params: DynamicAttributeDynamicValueParams): void {
|
|
601
702
|
// Default implementation does nothing
|
|
602
703
|
}
|
|
603
704
|
}
|
|
@@ -44,8 +44,8 @@ class SVGTagNameCapitalizationVisitor extends BaseRuleVisitor {
|
|
|
44
44
|
if (correctCamelCase && tagName !== correctCamelCase) {
|
|
45
45
|
let type: string = node.type
|
|
46
46
|
|
|
47
|
-
if (node.type
|
|
48
|
-
if (node.type
|
|
47
|
+
if (node.type === "AST_HTML_OPEN_TAG_NODE") type = "Opening"
|
|
48
|
+
if (node.type === "AST_HTML_CLOSE_TAG_NODE") type = "Closing"
|
|
49
49
|
|
|
50
50
|
this.addOffense(
|
|
51
51
|
`${type} SVG tag name \`${tagName}\` should use proper capitalization. Use \`${correctCamelCase}\` instead.`,
|