@herb-tools/linter 0.9.0 → 0.9.1

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.
Files changed (62) hide show
  1. package/README.md +2 -2
  2. package/dist/herb-lint.js +1512 -85
  3. package/dist/herb-lint.js.map +1 -1
  4. package/dist/index.cjs +538 -72
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.js +465 -74
  7. package/dist/index.js.map +1 -1
  8. package/dist/lint-worker.js +1510 -83
  9. package/dist/lint-worker.js.map +1 -1
  10. package/dist/loader.cjs +1065 -81
  11. package/dist/loader.cjs.map +1 -1
  12. package/dist/loader.js +1044 -82
  13. package/dist/loader.js.map +1 -1
  14. package/dist/rules/actionview-no-silent-render.js +31 -0
  15. package/dist/rules/actionview-no-silent-render.js.map +1 -0
  16. package/dist/rules/erb-no-case-node-children.js +3 -1
  17. package/dist/rules/erb-no-case-node-children.js.map +1 -1
  18. package/dist/rules/erb-no-duplicate-branch-elements.js +95 -11
  19. package/dist/rules/erb-no-duplicate-branch-elements.js.map +1 -1
  20. package/dist/rules/erb-no-empty-control-flow.js +190 -0
  21. package/dist/rules/erb-no-empty-control-flow.js.map +1 -0
  22. package/dist/rules/erb-no-silent-statement.js +44 -0
  23. package/dist/rules/erb-no-silent-statement.js.map +1 -0
  24. package/dist/rules/erb-no-unsafe-script-interpolation.js +37 -3
  25. package/dist/rules/erb-no-unsafe-script-interpolation.js.map +1 -1
  26. package/dist/rules/html-allowed-script-type.js +1 -1
  27. package/dist/rules/html-allowed-script-type.js.map +1 -1
  28. package/dist/rules/index.js +20 -16
  29. package/dist/rules/index.js.map +1 -1
  30. package/dist/rules/rule-utils.js +13 -10
  31. package/dist/rules/rule-utils.js.map +1 -1
  32. package/dist/rules.js +8 -2
  33. package/dist/rules.js.map +1 -1
  34. package/dist/types/index.d.ts +1 -0
  35. package/dist/types/rules/actionview-no-silent-render.d.ts +9 -0
  36. package/dist/types/rules/erb-no-duplicate-branch-elements.d.ts +1 -0
  37. package/dist/types/rules/erb-no-empty-control-flow.d.ts +8 -0
  38. package/dist/types/rules/erb-no-silent-statement.d.ts +9 -0
  39. package/dist/types/rules/erb-no-unsafe-script-interpolation.d.ts +2 -1
  40. package/dist/types/rules/index.d.ts +20 -16
  41. package/dist/types/rules/rule-utils.d.ts +7 -6
  42. package/dist/types/types.d.ts +4 -3
  43. package/dist/types.js +6 -3
  44. package/dist/types.js.map +1 -1
  45. package/docs/rules/README.md +3 -0
  46. package/docs/rules/actionview-no-silent-render.md +47 -0
  47. package/docs/rules/erb-no-empty-control-flow.md +83 -0
  48. package/docs/rules/erb-no-silent-statement.md +53 -0
  49. package/docs/rules/erb-no-unsafe-script-interpolation.md +70 -3
  50. package/package.json +8 -8
  51. package/src/index.ts +21 -0
  52. package/src/rules/actionview-no-silent-render.ts +44 -0
  53. package/src/rules/erb-no-case-node-children.ts +3 -1
  54. package/src/rules/erb-no-duplicate-branch-elements.ts +130 -14
  55. package/src/rules/erb-no-empty-control-flow.ts +255 -0
  56. package/src/rules/erb-no-silent-statement.ts +58 -0
  57. package/src/rules/erb-no-unsafe-script-interpolation.ts +51 -5
  58. package/src/rules/html-allowed-script-type.ts +1 -1
  59. package/src/rules/index.ts +21 -16
  60. package/src/rules/rule-utils.ts +14 -10
  61. package/src/rules.ts +8 -2
  62. package/src/types.ts +7 -3
@@ -0,0 +1,255 @@
1
+ import { BaseRuleVisitor } from "./rule-utils.js"
2
+ import { ParserRule } from "../types.js"
3
+ import { isHTMLTextNode, isERBIfNode, isERBElseNode, Location } from "@herb-tools/core"
4
+
5
+ import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
6
+ import type {
7
+ Node,
8
+ ERBIfNode,
9
+ ERBElseNode,
10
+ ERBUnlessNode,
11
+ ERBForNode,
12
+ ERBWhileNode,
13
+ ERBUntilNode,
14
+ ERBWhenNode,
15
+ ERBBeginNode,
16
+ ERBRescueNode,
17
+ ERBEnsureNode,
18
+ ERBBlockNode,
19
+ ERBInNode,
20
+ ParseResult,
21
+ } from "@herb-tools/core"
22
+
23
+ class ERBNoEmptyControlFlowVisitor extends BaseRuleVisitor {
24
+ private processedIfNodes: Set<ERBIfNode> = new Set()
25
+ private processedElseNodes: Set<ERBElseNode> = new Set()
26
+
27
+ visitERBIfNode(node: ERBIfNode): void {
28
+ if (this.processedIfNodes.has(node)) {
29
+ return
30
+ }
31
+
32
+ this.markIfChainAsProcessed(node)
33
+ this.markElseNodesInIfChain(node)
34
+
35
+ const entireChainEmpty = this.isEntireIfChainEmpty(node)
36
+
37
+ if (entireChainEmpty) {
38
+ this.addEmptyBlockOffense(node, node.statements, "if")
39
+ } else {
40
+ this.checkIfChainParts(node)
41
+ }
42
+
43
+ this.visitChildNodes(node)
44
+ }
45
+
46
+ visitERBElseNode(node: ERBElseNode): void {
47
+ if (this.processedElseNodes.has(node)) {
48
+ this.visitChildNodes(node)
49
+ return
50
+ }
51
+
52
+ this.addEmptyBlockOffense(node, node.statements, "else")
53
+ this.visitChildNodes(node)
54
+ }
55
+
56
+ visitERBUnlessNode(node: ERBUnlessNode): void {
57
+ const unlessHasContent = this.statementsHaveContent(node.statements)
58
+ const elseHasContent = node.else_clause && this.statementsHaveContent(node.else_clause.statements)
59
+
60
+ if (node.else_clause) {
61
+ this.processedElseNodes.add(node.else_clause)
62
+ }
63
+
64
+ const entireBlockEmpty = !unlessHasContent && !elseHasContent
65
+
66
+ if (entireBlockEmpty) {
67
+ this.addEmptyBlockOffense(node, node.statements, "unless")
68
+ } else {
69
+ if (!unlessHasContent) {
70
+ this.addEmptyBlockOffenseWithEnd(node, node.statements, "unless", node.else_clause)
71
+ }
72
+
73
+ if (node.else_clause && !elseHasContent) {
74
+ this.addEmptyBlockOffense(node.else_clause, node.else_clause.statements, "else")
75
+ }
76
+ }
77
+
78
+ this.visitChildNodes(node)
79
+ }
80
+
81
+ visitERBForNode(node: ERBForNode): void {
82
+ this.addEmptyBlockOffense(node, node.statements, "for")
83
+ this.visitChildNodes(node)
84
+ }
85
+
86
+ visitERBWhileNode(node: ERBWhileNode): void {
87
+ this.addEmptyBlockOffense(node, node.statements, "while")
88
+ this.visitChildNodes(node)
89
+ }
90
+
91
+ visitERBUntilNode(node: ERBUntilNode): void {
92
+ this.addEmptyBlockOffense(node, node.statements, "until")
93
+ this.visitChildNodes(node)
94
+ }
95
+
96
+ visitERBWhenNode(node: ERBWhenNode): void {
97
+ if (!node.then_keyword) {
98
+ this.addEmptyBlockOffense(node, node.statements, "when")
99
+ }
100
+
101
+ this.visitChildNodes(node)
102
+ }
103
+
104
+ visitERBInNode(node: ERBInNode): void {
105
+ if (!node.then_keyword) {
106
+ this.addEmptyBlockOffense(node, node.statements, "in")
107
+ }
108
+
109
+ this.visitChildNodes(node)
110
+ }
111
+
112
+ visitERBBeginNode(node: ERBBeginNode): void {
113
+ this.addEmptyBlockOffense(node, node.statements, "begin")
114
+ this.visitChildNodes(node)
115
+ }
116
+
117
+ visitERBRescueNode(node: ERBRescueNode): void {
118
+ this.addEmptyBlockOffense(node, node.statements, "rescue")
119
+ this.visitChildNodes(node)
120
+ }
121
+
122
+ visitERBEnsureNode(node: ERBEnsureNode): void {
123
+ this.addEmptyBlockOffense(node, node.statements, "ensure")
124
+ this.visitChildNodes(node)
125
+ }
126
+
127
+ visitERBBlockNode(node: ERBBlockNode): void {
128
+ this.addEmptyBlockOffense(node, node.body, "do")
129
+ this.visitChildNodes(node)
130
+ }
131
+
132
+ private addEmptyBlockOffense(node: Node, statements: Node[], blockType: string): void {
133
+ this.addEmptyBlockOffenseWithEnd(node, statements, blockType, null)
134
+ }
135
+
136
+ private addEmptyBlockOffenseWithEnd(node: Node, statements: Node[], blockType: string, subsequentNode: Node | null): void {
137
+ if (this.statementsHaveContent(statements)) {
138
+ return
139
+ }
140
+
141
+ const startLocation = node.location.start
142
+ const endLocation = subsequentNode
143
+ ? subsequentNode.location.start
144
+ : node.location.end
145
+
146
+ const location = Location.from(
147
+ startLocation.line, startLocation.column,
148
+ endLocation.line, endLocation.column,
149
+ )
150
+
151
+ const offense = this.createOffense(
152
+ `Empty ${blockType} block: this control flow statement has no content`,
153
+ location,
154
+ )
155
+ offense.tags = ["unnecessary"]
156
+ this.offenses.push(offense)
157
+ }
158
+
159
+ private statementsHaveContent(statements: Node[]): boolean {
160
+ return statements.some(statement => {
161
+ if (isHTMLTextNode(statement)) {
162
+ return statement.content.trim() !== ""
163
+ }
164
+
165
+ return true
166
+ })
167
+ }
168
+
169
+ private markIfChainAsProcessed(node: ERBIfNode): void {
170
+ this.processedIfNodes.add(node)
171
+ this.traverseSubsequentNodes(node.subsequent, (current) => {
172
+ if (isERBIfNode(current)) {
173
+ this.processedIfNodes.add(current)
174
+ }
175
+ })
176
+ }
177
+
178
+ private markElseNodesInIfChain(node: ERBIfNode): void {
179
+ this.traverseSubsequentNodes(node.subsequent, (current) => {
180
+ if (isERBElseNode(current)) {
181
+ this.processedElseNodes.add(current)
182
+ }
183
+ })
184
+ }
185
+
186
+ private traverseSubsequentNodes(startNode: Node | null, callback: (node: ERBIfNode | ERBElseNode) => void): void {
187
+ let current: Node | null = startNode
188
+ while (current) {
189
+ if (isERBIfNode(current)) {
190
+ callback(current)
191
+ current = current.subsequent
192
+ } else if (isERBElseNode(current)) {
193
+ callback(current)
194
+ break
195
+ } else {
196
+ break
197
+ }
198
+ }
199
+ }
200
+
201
+ private checkIfChainParts(node: ERBIfNode): void {
202
+ if (!this.statementsHaveContent(node.statements)) {
203
+ this.addEmptyBlockOffenseWithEnd(node, node.statements, "if", node.subsequent)
204
+ }
205
+
206
+ this.traverseSubsequentNodes(node.subsequent, (current) => {
207
+ if (this.statementsHaveContent(current.statements)) {
208
+ return
209
+ }
210
+
211
+ const blockType = isERBIfNode(current) ? "elsif" : "else"
212
+ const nextSubsequent = isERBIfNode(current) ? current.subsequent : null
213
+
214
+ if (nextSubsequent) {
215
+ this.addEmptyBlockOffenseWithEnd(current, current.statements, blockType, nextSubsequent)
216
+ } else {
217
+ this.addEmptyBlockOffense(current, current.statements, blockType)
218
+ }
219
+ })
220
+ }
221
+
222
+ private isEntireIfChainEmpty(node: ERBIfNode): boolean {
223
+ if (this.statementsHaveContent(node.statements)) {
224
+ return false
225
+ }
226
+
227
+ let hasContent = false
228
+ this.traverseSubsequentNodes(node.subsequent, (current) => {
229
+ if (this.statementsHaveContent(current.statements)) {
230
+ hasContent = true
231
+ }
232
+ })
233
+
234
+ return !hasContent
235
+ }
236
+ }
237
+
238
+ export class ERBNoEmptyControlFlowRule extends ParserRule {
239
+ static ruleName = "erb-no-empty-control-flow"
240
+
241
+ get defaultConfig(): FullRuleConfig {
242
+ return {
243
+ enabled: true,
244
+ severity: "hint"
245
+ }
246
+ }
247
+
248
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
249
+ const visitor = new ERBNoEmptyControlFlowVisitor(this.ruleName, context)
250
+
251
+ visitor.visit(result.value)
252
+
253
+ return visitor.offenses
254
+ }
255
+ }
@@ -0,0 +1,58 @@
1
+ import { BaseRuleVisitor } from "./rule-utils.js"
2
+ import { ParserRule } from "../types.js"
3
+
4
+ import { isERBOutputNode, PrismNode } from "@herb-tools/core"
5
+
6
+ import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
7
+ import type { ParseResult, ERBContentNode, ParserOptions } from "@herb-tools/core"
8
+
9
+ function isAssignmentNode(prismNode: PrismNode): boolean {
10
+ const type: string = prismNode?.constructor?.name
11
+ if (!type) return false
12
+
13
+ return type.endsWith("WriteNode")
14
+ }
15
+
16
+ class ERBNoSilentStatementVisitor extends BaseRuleVisitor {
17
+ visitERBContentNode(node: ERBContentNode): void {
18
+ if (isERBOutputNode(node)) return
19
+
20
+ const prismNode = node.prismNode
21
+ if (!prismNode) return
22
+
23
+ if (isAssignmentNode(prismNode)) return
24
+
25
+ const content = node.content?.value?.trim()
26
+ if (!content) return
27
+
28
+ this.addOffense(
29
+ `Avoid using silent ERB tags for statements. Move \`${content}\` to a controller, helper, or presenter.`,
30
+ node.location,
31
+ )
32
+ }
33
+ }
34
+
35
+ export class ERBNoSilentStatementRule extends ParserRule {
36
+ static ruleName = "erb-no-silent-statement"
37
+
38
+ get defaultConfig(): FullRuleConfig {
39
+ return {
40
+ enabled: false,
41
+ severity: "warning"
42
+ }
43
+ }
44
+
45
+ get parserOptions(): Partial<ParserOptions> {
46
+ return {
47
+ prism_nodes: true,
48
+ }
49
+ }
50
+
51
+ check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
52
+ const visitor = new ERBNoSilentStatementVisitor(this.ruleName, context)
53
+
54
+ visitor.visit(result.value)
55
+
56
+ return visitor.offenses
57
+ }
58
+ }
@@ -1,5 +1,7 @@
1
1
  import { ParserRule } from "../types.js"
2
2
  import { BaseRuleVisitor } from "./rule-utils.js"
3
+ import { PrismVisitor } from "@herb-tools/core"
4
+
3
5
  import {
4
6
  getTagLocalName,
5
7
  getAttribute,
@@ -10,9 +12,34 @@ import {
10
12
  } from "@herb-tools/core"
11
13
 
12
14
  import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
13
- import type { ParseResult, HTMLElementNode, Node } from "@herb-tools/core"
15
+ import type { ParseResult, HTMLElementNode, Node, ERBContentNode, ParserOptions, PrismNode } from "@herb-tools/core"
16
+
17
+ const SAFE_METHOD_NAMES = new Set([
18
+ "to_json",
19
+ "json_escape",
20
+ ])
21
+
22
+ const ESCAPE_JAVASCRIPT_METHOD_NAMES = new Set([
23
+ "j",
24
+ "escape_javascript",
25
+ ])
26
+
27
+ class SafeCallDetector extends PrismVisitor {
28
+ public hasSafeCall = false
29
+ public hasEscapeJavascriptCall = false
30
+
31
+ visitCallNode(node: PrismNode): void {
32
+ if (SAFE_METHOD_NAMES.has(node.name)) {
33
+ this.hasSafeCall = true
34
+ }
35
+
36
+ if (ESCAPE_JAVASCRIPT_METHOD_NAMES.has(node.name)) {
37
+ this.hasEscapeJavascriptCall = true
38
+ }
14
39
 
15
- const SAFE_PATTERN = /\.to_json\b/
40
+ this.visitChildNodes(node)
41
+ }
42
+ }
16
43
 
17
44
  class ERBNoUnsafeScriptInterpolationVisitor extends BaseRuleVisitor {
18
45
  visitHTMLElementNode(node: HTMLElementNode): void {
@@ -35,7 +62,6 @@ class ERBNoUnsafeScriptInterpolationVisitor extends BaseRuleVisitor {
35
62
  const typeValue = typeAttribute ? getStaticAttributeValue(typeAttribute) : null
36
63
 
37
64
  if (typeValue === "text/html") return
38
-
39
65
  if (!node.body || node.body.length === 0) return
40
66
 
41
67
  this.checkNodesForUnsafeOutput(node.body)
@@ -46,9 +72,21 @@ class ERBNoUnsafeScriptInterpolationVisitor extends BaseRuleVisitor {
46
72
  if (!isERBNode(child)) continue
47
73
  if (!isERBOutputNode(child)) continue
48
74
 
49
- const content = child.content?.value?.trim() || ""
75
+ const erbContent = child as ERBContentNode
76
+ const prismNode = erbContent.prismNode
77
+ const detector = new SafeCallDetector()
78
+
79
+ if (prismNode) detector.visit(prismNode)
80
+ if (detector.hasSafeCall) continue
81
+
82
+ if (detector.hasEscapeJavascriptCall) {
83
+ this.addOffense(
84
+ "Avoid `j()` / `escape_javascript()` in `<script>` tags. It is only safe inside quoted string literals. Use `.to_json` instead, which is safe in any position.",
85
+ child.location,
86
+ )
50
87
 
51
- if (SAFE_PATTERN.test(content)) continue
88
+ continue
89
+ }
52
90
 
53
91
  this.addOffense(
54
92
  "Unsafe ERB output in `<script>` tag. Use `.to_json` to safely serialize values into JavaScript.",
@@ -68,9 +106,17 @@ export class ERBNoUnsafeScriptInterpolationRule extends ParserRule {
68
106
  }
69
107
  }
70
108
 
109
+ get parserOptions(): Partial<ParserOptions> {
110
+ return {
111
+ prism_nodes: true,
112
+ }
113
+ }
114
+
71
115
  check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
72
116
  const visitor = new ERBNoUnsafeScriptInterpolationVisitor(this.ruleName, context)
117
+
73
118
  visitor.visit(result.value)
119
+
74
120
  return visitor.offenses
75
121
  }
76
122
  }
@@ -5,10 +5,10 @@ import { getTagLocalName, getAttribute, getStaticAttributeValue, hasAttributeVal
5
5
  import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
6
6
  import type { HTMLAttributeNode, HTMLOpenTagNode, ParseResult } from "@herb-tools/core"
7
7
 
8
- const ALLOWED_TYPES = ["text/javascript"]
9
8
  // NOTE: Rules are not configurable for now, keep some sane defaults
10
9
  // See https://github.com/marcoroth/herb/issues/1204
11
10
  const ALLOW_BLANK = true
11
+ const ALLOWED_TYPES = ["text/javascript", "module", "importmap", "speculationrules"]
12
12
 
13
13
  class AllowedScriptTypeVisitor extends BaseRuleVisitor {
14
14
  visitHTMLOpenTagNode(node: HTMLOpenTagNode): void {
@@ -3,40 +3,45 @@ export * from "./file-utils.js"
3
3
  export * from "./string-utils.js"
4
4
  export * from "./herb-disable-comment-base.js"
5
5
 
6
+ export * from "./actionview-no-silent-helper.js"
7
+ export * from "./actionview-no-silent-render.js"
8
+
6
9
  export * from "./erb-comment-syntax.js"
7
10
  export * from "./erb-no-case-node-children.js"
8
- export * from "./erb-no-inline-case-conditions.js"
11
+ export * from "./erb-no-empty-control-flow.js"
9
12
  export * from "./erb-no-conditional-open-tag.js"
10
13
  export * from "./erb-no-duplicate-branch-elements.js"
11
14
  export * from "./erb-no-empty-tags.js"
12
15
  export * from "./erb-no-extra-newline.js"
13
16
  export * from "./erb-no-extra-whitespace-inside-tags.js"
14
- export * from "./erb-no-output-control-flow.js"
15
- export * from "./erb-no-then-in-control-flow.js"
16
- export * from "./erb-no-silent-tag-in-attribute-name.js"
17
- export * from "./erb-no-trailing-whitespace.js"
18
- export * from "./erb-prefer-image-tag-helper.js"
19
- export * from "./erb-require-trailing-newline.js"
20
- export * from "./erb-require-whitespace-inside-tags.js"
17
+ export * from "./erb-no-inline-case-conditions.js"
18
+ export * from "./erb-no-instance-variables-in-partials.js"
21
19
  export * from "./erb-no-javascript-tag-helper.js"
20
+ export * from "./erb-no-output-control-flow.js"
21
+ export * from "./erb-no-output-in-attribute-name.js"
22
+ export * from "./erb-no-output-in-attribute-position.js"
22
23
  export * from "./erb-no-raw-output-in-attribute-value.js"
24
+ export * from "./erb-no-silent-statement.js"
25
+ export * from "./erb-no-silent-tag-in-attribute-name.js"
23
26
  export * from "./erb-no-statement-in-script.js"
27
+ export * from "./erb-no-then-in-control-flow.js"
28
+ export * from "./erb-no-trailing-whitespace.js"
24
29
  export * from "./erb-no-unsafe-js-attribute.js"
25
30
  export * from "./erb-no-unsafe-raw.js"
26
31
  export * from "./erb-no-unsafe-script-interpolation.js"
32
+ export * from "./erb-prefer-image-tag-helper.js"
33
+ export * from "./erb-require-trailing-newline.js"
34
+ export * from "./erb-require-whitespace-inside-tags.js"
27
35
  export * from "./erb-right-trim.js"
28
- export * from "./erb-no-output-in-attribute-position.js"
29
- export * from "./erb-no-output-in-attribute-name.js"
30
- export * from "./erb-no-instance-variables-in-partials.js"
31
36
  export * from "./erb-strict-locals-comment-syntax.js"
32
37
  export * from "./erb-strict-locals-required.js"
33
38
 
34
- export * from "./herb-disable-comment-valid-rule-name.js"
35
- export * from "./herb-disable-comment-no-redundant-all.js"
36
- export * from "./herb-disable-comment-no-duplicate-rules.js"
37
- export * from "./herb-disable-comment-missing-rules.js"
38
39
  export * from "./herb-disable-comment-malformed.js"
40
+ export * from "./herb-disable-comment-missing-rules.js"
41
+ export * from "./herb-disable-comment-no-duplicate-rules.js"
42
+ export * from "./herb-disable-comment-no-redundant-all.js"
39
43
  export * from "./herb-disable-comment-unnecessary.js"
44
+ export * from "./herb-disable-comment-valid-rule-name.js"
40
45
 
41
46
  export * from "./html-allowed-script-type.js"
42
47
  export * from "./html-anchor-require-href.js"
@@ -49,8 +54,8 @@ export * from "./html-attribute-equals-spacing.js"
49
54
  export * from "./html-attribute-values-require-quotes.js"
50
55
  export * from "./html-avoid-both-disabled-and-aria-disabled.js"
51
56
  export * from "./html-body-only-elements.js"
52
- export * from "./html-details-has-summary.js"
53
57
  export * from "./html-boolean-attributes-no-value.js"
58
+ export * from "./html-details-has-summary.js"
54
59
  export * from "./html-head-only-elements.js"
55
60
  export * from "./html-iframe-has-title.js"
56
61
  export * from "./html-img-require-alt.js"
@@ -7,7 +7,7 @@ import {
7
7
  getValidatableStaticContent,
8
8
  getAttributeName,
9
9
  getStaticAttributeValue,
10
- hasDynamicAttributeNameOnAttribute as hasDynamicAttributeName,
10
+ hasDynamicAttributeName,
11
11
  getCombinedAttributeNameString,
12
12
  getAttributeValueNodes,
13
13
  getAttributeValue,
@@ -27,6 +27,7 @@ import type {
27
27
  import { DEFAULT_LINT_CONTEXT } from "../types.js"
28
28
 
29
29
  import type * as Nodes from "@herb-tools/core"
30
+ import type { DiagnosticTag } from "@herb-tools/core"
30
31
  import type { UnboundLintOffense, LintContext, LintSeverity, BaseAutofixContext } from "../types.js"
31
32
 
32
33
  export enum ControlFlowType {
@@ -53,7 +54,7 @@ export abstract class BaseRuleVisitor<TAutofixContext extends BaseAutofixContext
53
54
  * Helper method to create an unbound lint offense (without severity).
54
55
  * The Linter will bind severity based on the rule's config.
55
56
  */
56
- protected createOffense(message: string, location: Location, autofixContext?: TAutofixContext, severity?: LintSeverity): UnboundLintOffense<TAutofixContext> {
57
+ protected createOffense(message: string, location: Location, autofixContext?: TAutofixContext, severity?: LintSeverity, tags?: DiagnosticTag[]): UnboundLintOffense<TAutofixContext> {
57
58
  return {
58
59
  rule: this.ruleName,
59
60
  code: this.ruleName,
@@ -62,14 +63,15 @@ export abstract class BaseRuleVisitor<TAutofixContext extends BaseAutofixContext
62
63
  location,
63
64
  autofixContext,
64
65
  severity,
66
+ tags,
65
67
  }
66
68
  }
67
69
 
68
70
  /**
69
71
  * Helper method to add an offense to the offenses array
70
72
  */
71
- protected addOffense(message: string, location: Location, autofixContext?: TAutofixContext, severity?: LintSeverity): void {
72
- this.offenses.push(this.createOffense(message, location, autofixContext, severity))
73
+ protected addOffense(message: string, location: Location, autofixContext?: TAutofixContext, severity?: LintSeverity, tags?: DiagnosticTag[]): void {
74
+ this.offenses.push(this.createOffense(message, location, autofixContext, severity, tags))
73
75
  }
74
76
  }
75
77
 
@@ -515,7 +517,7 @@ export abstract class BaseLexerRuleVisitor<TAutofixContext extends BaseAutofixCo
515
517
  * Helper method to create an unbound lint offense (without severity).
516
518
  * The Linter will bind severity based on the rule's config.
517
519
  */
518
- protected createOffense(message: string, location: Location, autofixContext?: TAutofixContext, severity?: LintSeverity): UnboundLintOffense<TAutofixContext> {
520
+ protected createOffense(message: string, location: Location, autofixContext?: TAutofixContext, severity?: LintSeverity, tags?: DiagnosticTag[]): UnboundLintOffense<TAutofixContext> {
519
521
  return {
520
522
  rule: this.ruleName,
521
523
  code: this.ruleName,
@@ -524,14 +526,15 @@ export abstract class BaseLexerRuleVisitor<TAutofixContext extends BaseAutofixCo
524
526
  location,
525
527
  autofixContext,
526
528
  severity,
529
+ tags,
527
530
  }
528
531
  }
529
532
 
530
533
  /**
531
534
  * Helper method to add an offense to the offenses array
532
535
  */
533
- protected addOffense(message: string, location: Location, autofixContext?: TAutofixContext, severity?: LintSeverity): void {
534
- this.offenses.push(this.createOffense(message, location, autofixContext, severity))
536
+ protected addOffense(message: string, location: Location, autofixContext?: TAutofixContext, severity?: LintSeverity, tags?: DiagnosticTag[]): void {
537
+ this.offenses.push(this.createOffense(message, location, autofixContext, severity, tags))
535
538
  }
536
539
 
537
540
  /**
@@ -578,7 +581,7 @@ export abstract class BaseSourceRuleVisitor<TAutofixContext extends BaseAutofixC
578
581
  * Helper method to create an unbound lint offense (without severity).
579
582
  * The Linter will bind severity based on the rule's config.
580
583
  */
581
- protected createOffense(message: string, location: Location, autofixContext?: TAutofixContext, severity?: LintSeverity): UnboundLintOffense<TAutofixContext> {
584
+ protected createOffense(message: string, location: Location, autofixContext?: TAutofixContext, severity?: LintSeverity, tags?: DiagnosticTag[]): UnboundLintOffense<TAutofixContext> {
582
585
  return {
583
586
  rule: this.ruleName,
584
587
  code: this.ruleName,
@@ -587,14 +590,15 @@ export abstract class BaseSourceRuleVisitor<TAutofixContext extends BaseAutofixC
587
590
  location,
588
591
  autofixContext,
589
592
  severity,
593
+ tags,
590
594
  }
591
595
  }
592
596
 
593
597
  /**
594
598
  * Helper method to add an offense to the offenses array
595
599
  */
596
- protected addOffense(message: string, location: Location, autofixContext?: TAutofixContext, severity?: LintSeverity): void {
597
- this.offenses.push(this.createOffense(message, location, autofixContext, severity))
600
+ protected addOffense(message: string, location: Location, autofixContext?: TAutofixContext, severity?: LintSeverity, tags?: DiagnosticTag[]): void {
601
+ this.offenses.push(this.createOffense(message, location, autofixContext, severity, tags))
598
602
  }
599
603
 
600
604
  /**
package/src/rules.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  import type { RuleClass } from "./types.js"
2
2
 
3
3
  import { ActionViewNoSilentHelperRule } from "./rules/actionview-no-silent-helper.js"
4
+ import { ActionViewNoSilentRenderRule } from "./rules/actionview-no-silent-render.js"
4
5
 
5
6
  import { ERBCommentSyntax } from "./rules/erb-comment-syntax.js";
6
7
  import { ERBNoCaseNodeChildrenRule } from "./rules/erb-no-case-node-children.js"
8
+ import { ERBNoEmptyControlFlowRule } from "./rules/erb-no-empty-control-flow.js"
7
9
  import { ERBNoConditionalHTMLElementRule } from "./rules/erb-no-conditional-html-element.js"
8
10
  import { ERBNoConditionalOpenTagRule } from "./rules/erb-no-conditional-open-tag.js"
9
11
  import { ERBNoDuplicateBranchElementsRule } from "./rules/erb-no-duplicate-branch-elements.js"
@@ -18,6 +20,7 @@ import { ERBNoOutputControlFlowRule } from "./rules/erb-no-output-control-flow.j
18
20
  import { ERBNoOutputInAttributeNameRule } from "./rules/erb-no-output-in-attribute-name.js"
19
21
  import { ERBNoOutputInAttributePositionRule } from "./rules/erb-no-output-in-attribute-position.js"
20
22
  import { ERBNoRawOutputInAttributeValueRule } from "./rules/erb-no-raw-output-in-attribute-value.js"
23
+ import { ERBNoSilentStatementRule } from "./rules/erb-no-silent-statement.js"
21
24
  import { ERBNoSilentTagInAttributeNameRule } from "./rules/erb-no-silent-tag-in-attribute-name.js"
22
25
  import { ERBNoStatementInScriptRule } from "./rules/erb-no-statement-in-script.js"
23
26
  import { ERBNoThenInControlFlowRule } from "./rules/erb-no-then-in-control-flow.js"
@@ -51,8 +54,8 @@ import { HTMLAttributeEqualsSpacingRule } from "./rules/html-attribute-equals-sp
51
54
  import { HTMLAttributeValuesRequireQuotesRule } from "./rules/html-attribute-values-require-quotes.js"
52
55
  import { HTMLAvoidBothDisabledAndAriaDisabledRule } from "./rules/html-avoid-both-disabled-and-aria-disabled.js"
53
56
  import { HTMLBodyOnlyElementsRule } from "./rules/html-body-only-elements.js"
54
- import { HTMLDetailsHasSummaryRule } from "./rules/html-details-has-summary.js"
55
57
  import { HTMLBooleanAttributesNoValueRule } from "./rules/html-boolean-attributes-no-value.js"
58
+ import { HTMLDetailsHasSummaryRule } from "./rules/html-details-has-summary.js"
56
59
  import { HTMLHeadOnlyElementsRule } from "./rules/html-head-only-elements.js"
57
60
  import { HTMLIframeHasTitleRule } from "./rules/html-iframe-has-title.js"
58
61
  import { HTMLImgRequireAltRule } from "./rules/html-img-require-alt.js"
@@ -84,9 +87,11 @@ import { TurboPermanentRequireIdRule } from "./rules/turbo-permanent-require-id.
84
87
 
85
88
  export const rules: RuleClass[] = [
86
89
  ActionViewNoSilentHelperRule,
90
+ ActionViewNoSilentRenderRule,
87
91
 
88
92
  ERBCommentSyntax,
89
93
  ERBNoCaseNodeChildrenRule,
94
+ ERBNoEmptyControlFlowRule,
90
95
  ERBNoConditionalHTMLElementRule,
91
96
  ERBNoConditionalOpenTagRule,
92
97
  ERBNoDuplicateBranchElementsRule,
@@ -101,6 +106,7 @@ export const rules: RuleClass[] = [
101
106
  ERBNoOutputInAttributeNameRule,
102
107
  ERBNoOutputInAttributePositionRule,
103
108
  ERBNoRawOutputInAttributeValueRule,
109
+ ERBNoSilentStatementRule,
104
110
  ERBNoSilentTagInAttributeNameRule,
105
111
  ERBNoStatementInScriptRule,
106
112
  ERBNoThenInControlFlowRule,
@@ -134,8 +140,8 @@ export const rules: RuleClass[] = [
134
140
  HTMLAttributeValuesRequireQuotesRule,
135
141
  HTMLAvoidBothDisabledAndAriaDisabledRule,
136
142
  HTMLBodyOnlyElementsRule,
137
- HTMLDetailsHasSummaryRule,
138
143
  HTMLBooleanAttributesNoValueRule,
144
+ HTMLDetailsHasSummaryRule,
139
145
  HTMLHeadOnlyElementsRule,
140
146
  HTMLIframeHasTitleRule,
141
147
  HTMLImgRequireAltRule,