@herb-tools/linter 0.8.6 → 0.8.7

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.
@@ -1,48 +1,163 @@
1
- import { ParserRule } from "../types.js"
2
- import { AttributeVisitorMixin, StaticAttributeStaticValueParams, StaticAttributeDynamicValueParams } from "./rule-utils.js"
1
+ import { ParserRule, BaseAutofixContext } from "../types.js"
2
+ import { ControlFlowTrackingVisitor, ControlFlowType, getAttributeName } from "./rule-utils.js"
3
3
 
4
4
  import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
5
- import type { HTMLOpenTagNode, HTMLAttributeNode, ParseResult } from "@herb-tools/core"
5
+ import type { HTMLOpenTagNode, HTMLAttributeNode, ParseResult, Location } from "@herb-tools/core"
6
6
 
7
- class NoDuplicateAttributesVisitor extends AttributeVisitorMixin {
8
- private attributeNames = new Map<string, HTMLAttributeNode[]>()
7
+ interface ControlFlowState {
8
+ previousBranchAttributes: Set<string>
9
+ previousControlFlowAttributes: Set<string>
10
+ }
11
+
12
+ interface BranchState {
13
+ previousBranchAttributes: Set<string>
14
+ }
15
+
16
+ class NoDuplicateAttributesVisitor extends ControlFlowTrackingVisitor<
17
+ BaseAutofixContext,
18
+ ControlFlowState,
19
+ BranchState
20
+ > {
21
+ private tagAttributes = new Set<string>()
22
+ private currentBranchAttributes = new Set<string>()
23
+ private controlFlowAttributes = new Set<string>()
9
24
 
10
25
  visitHTMLOpenTagNode(node: HTMLOpenTagNode): void {
11
- this.attributeNames.clear()
26
+ this.tagAttributes = new Set()
27
+ this.currentBranchAttributes = new Set()
28
+ this.controlFlowAttributes = new Set()
12
29
  super.visitHTMLOpenTagNode(node)
13
- this.reportDuplicates()
14
30
  }
15
31
 
32
+ visitHTMLAttributeNode(node: HTMLAttributeNode): void {
33
+ this.checkAttribute(node)
34
+ }
35
+
36
+ protected onEnterControlFlow(_controlFlowType: ControlFlowType, wasAlreadyInControlFlow: boolean): ControlFlowState {
37
+ const stateToRestore: ControlFlowState = {
38
+ previousBranchAttributes: this.currentBranchAttributes,
39
+ previousControlFlowAttributes: this.controlFlowAttributes,
40
+ }
41
+
42
+ this.currentBranchAttributes = new Set()
43
+
44
+ if (!wasAlreadyInControlFlow) {
45
+ this.controlFlowAttributes = new Set()
46
+ }
47
+
48
+ return stateToRestore
49
+ }
50
+
51
+ protected onExitControlFlow(
52
+ controlFlowType: ControlFlowType,
53
+ wasAlreadyInControlFlow: boolean,
54
+ stateToRestore: ControlFlowState,
55
+ ): void {
56
+ if (controlFlowType === ControlFlowType.CONDITIONAL && !wasAlreadyInControlFlow) {
57
+ this.controlFlowAttributes.forEach((attr) => this.tagAttributes.add(attr))
58
+ }
59
+
60
+ this.currentBranchAttributes = stateToRestore.previousBranchAttributes
61
+ this.controlFlowAttributes = stateToRestore.previousControlFlowAttributes
62
+ }
63
+
64
+ protected onEnterBranch(): BranchState {
65
+ const stateToRestore: BranchState = {
66
+ previousBranchAttributes: this.currentBranchAttributes,
67
+ }
68
+
69
+ if (this.isInControlFlow) {
70
+ this.currentBranchAttributes = new Set()
71
+ }
72
+
73
+ return stateToRestore
74
+ }
75
+
76
+ protected onExitBranch(_stateToRestore: BranchState): void {}
77
+
78
+ private checkAttribute(attributeNode: HTMLAttributeNode): void {
79
+ const identifier = getAttributeName(attributeNode)
80
+ if (!identifier) return
16
81
 
17
- protected checkStaticAttributeStaticValue({ attributeName, attributeNode }: StaticAttributeStaticValueParams): void {
18
- this.trackAttributeName(attributeName, attributeNode)
82
+ this.processAttributeDuplicate(identifier, attributeNode)
19
83
  }
20
84
 
21
- protected checkStaticAttributeDynamicValue({ attributeName, attributeNode }: StaticAttributeDynamicValueParams): void {
22
- this.trackAttributeName(attributeName, attributeNode)
85
+ private processAttributeDuplicate(identifier: string, attributeNode: HTMLAttributeNode): void {
86
+ if (!this.isInControlFlow) {
87
+ this.handleHTMLAttribute(identifier, attributeNode)
88
+ return
89
+ }
90
+
91
+ if (this.currentControlFlowType === ControlFlowType.LOOP) {
92
+ this.handleLoopAttribute(identifier, attributeNode)
93
+ } else {
94
+ this.handleConditionalAttribute(identifier, attributeNode)
95
+ }
96
+
97
+ this.currentBranchAttributes.add(identifier)
23
98
  }
24
99
 
25
- private trackAttributeName(attributeName: string, attributeNode: HTMLAttributeNode): void {
26
- if (!this.attributeNames.has(attributeName)) {
27
- this.attributeNames.set(attributeName, [])
100
+ private handleHTMLAttribute(identifier: string, attributeNode: HTMLAttributeNode): void {
101
+ if (this.tagAttributes.has(identifier)) {
102
+ this.addDuplicateAttributeOffense(identifier, attributeNode.name!.location)
103
+ }
104
+
105
+ this.tagAttributes.add(identifier)
106
+ }
107
+
108
+ private handleLoopAttribute(identifier: string, attributeNode: HTMLAttributeNode): void {
109
+ if (this.currentBranchAttributes.has(identifier)) {
110
+ this.addSameLoopIterationOffense(identifier, attributeNode.name!.location)
111
+ return
112
+ }
113
+
114
+ if (this.tagAttributes.has(identifier)) {
115
+ this.addDuplicateAttributeOffense(identifier, attributeNode.name!.location)
116
+ return
28
117
  }
29
118
 
30
- this.attributeNames.get(attributeName)!.push(attributeNode)
119
+ this.addLoopWillDuplicateOffense(identifier, attributeNode.name!.location)
31
120
  }
32
121
 
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]
122
+ private handleConditionalAttribute(identifier: string, attributeNode: HTMLAttributeNode): void {
123
+ if (this.currentBranchAttributes.has(identifier)) {
124
+ this.addSameBranchOffense(identifier, attributeNode.name!.location)
125
+ return
126
+ }
38
127
 
39
- this.addOffense(
40
- `Duplicate attribute \`${attributeName}\` found on tag. Remove the duplicate occurrence.`,
41
- attributeNode.name!.location,
42
- )
43
- }
44
- }
128
+ if (this.tagAttributes.has(identifier)) {
129
+ this.addDuplicateAttributeOffense(identifier, attributeNode.name!.location)
45
130
  }
131
+
132
+ this.controlFlowAttributes.add(identifier)
133
+ }
134
+
135
+ private addDuplicateAttributeOffense(identifier: string, location: Location): void {
136
+ this.addOffense(
137
+ `Duplicate attribute \`${identifier}\`. Browsers only use the first occurrence and ignore duplicate attributes. Remove the duplicate or merge the values.`,
138
+ location,
139
+ )
140
+ }
141
+
142
+ private addSameLoopIterationOffense(identifier: string, location: Location): void {
143
+ this.addOffense(
144
+ `Duplicate attribute \`${identifier}\` in same loop iteration. Each iteration will produce an element with duplicate attributes. Remove one or merge the values.`,
145
+ location,
146
+ )
147
+ }
148
+
149
+ private addLoopWillDuplicateOffense(identifier: string, location: Location): void {
150
+ this.addOffense(
151
+ `Attribute \`${identifier}\` inside loop will appear multiple times on this element. Use a dynamic attribute name like \`${identifier}-<%= index %>\` or move the attribute outside the loop.`,
152
+ location,
153
+ )
154
+ }
155
+
156
+ private addSameBranchOffense(identifier: string, location: Location): void {
157
+ this.addOffense(
158
+ `Duplicate attribute \`${identifier}\` in same branch. This branch will produce an element with duplicate attributes. Remove one or merge the values.`,
159
+ location,
160
+ )
46
161
  }
47
162
  }
48
163