@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.
- package/README.md +2 -2
- package/dist/herb-lint.js +1512 -85
- package/dist/herb-lint.js.map +1 -1
- package/dist/index.cjs +538 -72
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +465 -74
- package/dist/index.js.map +1 -1
- package/dist/lint-worker.js +1510 -83
- package/dist/lint-worker.js.map +1 -1
- package/dist/loader.cjs +1065 -81
- package/dist/loader.cjs.map +1 -1
- package/dist/loader.js +1044 -82
- package/dist/loader.js.map +1 -1
- package/dist/rules/actionview-no-silent-render.js +31 -0
- package/dist/rules/actionview-no-silent-render.js.map +1 -0
- package/dist/rules/erb-no-case-node-children.js +3 -1
- package/dist/rules/erb-no-case-node-children.js.map +1 -1
- package/dist/rules/erb-no-duplicate-branch-elements.js +95 -11
- package/dist/rules/erb-no-duplicate-branch-elements.js.map +1 -1
- package/dist/rules/erb-no-empty-control-flow.js +190 -0
- package/dist/rules/erb-no-empty-control-flow.js.map +1 -0
- package/dist/rules/erb-no-silent-statement.js +44 -0
- package/dist/rules/erb-no-silent-statement.js.map +1 -0
- package/dist/rules/erb-no-unsafe-script-interpolation.js +37 -3
- package/dist/rules/erb-no-unsafe-script-interpolation.js.map +1 -1
- package/dist/rules/html-allowed-script-type.js +1 -1
- package/dist/rules/html-allowed-script-type.js.map +1 -1
- package/dist/rules/index.js +20 -16
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/rule-utils.js +13 -10
- package/dist/rules/rule-utils.js.map +1 -1
- package/dist/rules.js +8 -2
- package/dist/rules.js.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/rules/actionview-no-silent-render.d.ts +9 -0
- package/dist/types/rules/erb-no-duplicate-branch-elements.d.ts +1 -0
- package/dist/types/rules/erb-no-empty-control-flow.d.ts +8 -0
- package/dist/types/rules/erb-no-silent-statement.d.ts +9 -0
- package/dist/types/rules/erb-no-unsafe-script-interpolation.d.ts +2 -1
- package/dist/types/rules/index.d.ts +20 -16
- package/dist/types/rules/rule-utils.d.ts +7 -6
- package/dist/types/types.d.ts +4 -3
- package/dist/types.js +6 -3
- package/dist/types.js.map +1 -1
- package/docs/rules/README.md +3 -0
- package/docs/rules/actionview-no-silent-render.md +47 -0
- package/docs/rules/erb-no-empty-control-flow.md +83 -0
- package/docs/rules/erb-no-silent-statement.md +53 -0
- package/docs/rules/erb-no-unsafe-script-interpolation.md +70 -3
- package/package.json +8 -8
- package/src/index.ts +21 -0
- package/src/rules/actionview-no-silent-render.ts +44 -0
- package/src/rules/erb-no-case-node-children.ts +3 -1
- package/src/rules/erb-no-duplicate-branch-elements.ts +130 -14
- package/src/rules/erb-no-empty-control-flow.ts +255 -0
- package/src/rules/erb-no-silent-statement.ts +58 -0
- package/src/rules/erb-no-unsafe-script-interpolation.ts +51 -5
- package/src/rules/html-allowed-script-type.ts +1 -1
- package/src/rules/index.ts +21 -16
- package/src/rules/rule-utils.ts +14 -10
- package/src/rules.ts +8 -2
- 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
|
-
|
|
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
|
|
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
|
-
|
|
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 {
|
package/src/rules/index.ts
CHANGED
|
@@ -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-
|
|
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-
|
|
15
|
-
export * from "./erb-no-
|
|
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"
|
package/src/rules/rule-utils.ts
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
getValidatableStaticContent,
|
|
8
8
|
getAttributeName,
|
|
9
9
|
getStaticAttributeValue,
|
|
10
|
-
|
|
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,
|