Skip to content

Commit f25021e

Browse files
authored
Linter: Implement erb-no-inline-case-conditions rule (#1265)
1 parent ebeeb5c commit f25021e

File tree

7 files changed

+222
-3
lines changed

7 files changed

+222
-3
lines changed

javascript/packages/linter/docs/rules/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ This page contains documentation for all Herb Linter rules.
66

77
- [`erb-comment-syntax`](./erb-comment-syntax.md) - Disallow Ruby comments immediately after ERB tags
88
- [`erb-no-case-node-children`](./erb-no-case-node-children.md) - Don't use `children` for `case/when` and `case/in` nodes
9+
- [`erb-no-inline-case-conditions`](./erb-no-inline-case-conditions.md) - Disallow inline `case`/`when` and `case`/`in` conditions in a single ERB tag
910
- [`erb-no-conditional-html-element`](./erb-no-conditional-html-element.md) - Disallow conditional HTML elements
1011
- [`erb-no-empty-tags`](./erb-no-empty-tags.md) - Disallow empty ERB tags
1112
- [`erb-no-extra-newline`](./erb-no-extra-newline.md) - Disallow extra newlines.
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# Linter Rule: Disallow inline case conditions
2+
3+
**Rule:** `erb-no-inline-case-conditions`
4+
5+
## Description
6+
7+
Disallow placing `case` and its first `when`/`in` condition in the same ERB tag. When a `case` statement and its condition appear in a single ERB tag (e.g., `<% case x when y %>`), the parser cannot reliably process, compile, or format the template. This rule flags such patterns and guides users toward separate ERB tags.
8+
9+
## Rationale
10+
11+
ERB templates that combine `case` with a `when` or `in` condition in a single tag create parsing ambiguity. The parser creates synthetic condition nodes to handle this pattern in non-strict mode, but the resulting AST cannot be reliably formatted or compiled.
12+
13+
Using separate ERB tags for `case` and its conditions:
14+
15+
- Makes the template structure unambiguous for the parser
16+
- Enables proper formatting and compilation
17+
- Improves readability by clearly separating the case expression from its branches
18+
- Follows the conventional ERB style used across the Ruby on Rails ecosystem
19+
20+
## Examples
21+
22+
### ✅ Good
23+
24+
`case`/`when` in separate ERB tags:
25+
26+
```erb
27+
<% case variable %>
28+
<% when "a" %>
29+
A
30+
<% when "b" %>
31+
B
32+
<% else %>
33+
Other
34+
<% end %>
35+
```
36+
37+
`case`/`in` (pattern matching) in separate ERB tags:
38+
39+
```erb
40+
<% case value %>
41+
<% in 1 %>
42+
One
43+
<% in 2 %>
44+
Two
45+
<% else %>
46+
Other
47+
<% end %>
48+
```
49+
50+
### 🚫 Bad
51+
52+
Inline `case`/`when` in a single ERB tag:
53+
54+
```erb
55+
<% case variable when "a" %>
56+
A
57+
<% when "b" %>
58+
B
59+
<% end %>
60+
```
61+
62+
Inline `case`/`in` in a single ERB tag:
63+
64+
```erb
65+
<% case value in 1 %>
66+
One
67+
<% in 2 %>
68+
Two
69+
<% end %>
70+
```
71+
72+
`case`/`when` on separate lines but still in the same ERB tag:
73+
74+
```erb
75+
<% case variable
76+
when "a" %>
77+
A
78+
<% when "b" %>
79+
B
80+
<% end %>
81+
```
82+
83+
## References
84+
85+
\-

javascript/packages/linter/src/rules.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { RuleClass } from "./types.js"
22

33
import { ERBCommentSyntax } from "./rules/erb-comment-syntax.js";
44
import { ERBNoCaseNodeChildrenRule } from "./rules/erb-no-case-node-children.js"
5+
import { ERBNoInlineCaseConditionsRule } from "./rules/erb-no-inline-case-conditions.js"
56
import { ERBNoConditionalHTMLElementRule } from "./rules/erb-no-conditional-html-element.js"
67
import { ERBNoConditionalOpenTagRule } from "./rules/erb-no-conditional-open-tag.js"
78
import { ERBNoEmptyTagsRule } from "./rules/erb-no-empty-tags.js"
@@ -68,6 +69,7 @@ import { ParserNoErrorsRule } from "./rules/parser-no-errors.js"
6869
export const rules: RuleClass[] = [
6970
ERBCommentSyntax,
7071
ERBNoCaseNodeChildrenRule,
72+
ERBNoInlineCaseConditionsRule,
7173
ERBNoConditionalHTMLElementRule,
7274
ERBNoConditionalOpenTagRule,
7375
ERBNoEmptyTagsRule,
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { ParserRule } from "../types.js"
2+
import { BaseRuleVisitor } from "./rule-utils.js"
3+
4+
import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
5+
import type { ERBCaseNode, ERBCaseMatchNode, ERBWhenNode, ERBInNode, ParseResult, ParserOptions } from "@herb-tools/core"
6+
7+
class ERBNoInlineCaseConditionsVisitor extends BaseRuleVisitor {
8+
visitERBCaseNode(node: ERBCaseNode): void {
9+
this.checkConditions(node, "when")
10+
this.visitChildNodes(node)
11+
}
12+
13+
visitERBCaseMatchNode(node: ERBCaseMatchNode): void {
14+
this.checkConditions(node, "in")
15+
this.visitChildNodes(node)
16+
}
17+
18+
private checkConditions(node: ERBCaseNode | ERBCaseMatchNode, type: string): void {
19+
if (!node.conditions || node.conditions.length === 0) return
20+
21+
for (const condition of node.conditions as (ERBWhenNode | ERBInNode)[]) {
22+
if (condition.tag_opening === null) {
23+
this.addOffense(
24+
`A \`case\` statement with \`${type}\` conditions in a single ERB tag cannot be reliably parsed, compiled, and formatted. Use separate ERB tags for \`case\` and its conditions (e.g., \`<% case x %>\` followed by \`<% ${type} y %>\`).`,
25+
node.location,
26+
)
27+
break
28+
}
29+
}
30+
}
31+
}
32+
33+
export class ERBNoInlineCaseConditionsRule extends ParserRule {
34+
name = "erb-no-inline-case-conditions"
35+
36+
get defaultConfig(): FullRuleConfig {
37+
return {
38+
enabled: true,
39+
severity: "warning",
40+
}
41+
}
42+
43+
get parserOptions(): Partial<ParserOptions> {
44+
return { strict: false }
45+
}
46+
47+
check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
48+
const visitor = new ERBNoInlineCaseConditionsVisitor(this.name, context)
49+
50+
visitor.visit(result.value)
51+
52+
return visitor.offenses
53+
}
54+
}

javascript/packages/linter/src/rules/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export * from "./herb-disable-comment-base.js"
55

66
export * from "./erb-comment-syntax.js"
77
export * from "./erb-no-case-node-children.js"
8+
export * from "./erb-no-inline-case-conditions.js"
89
export * from "./erb-no-conditional-open-tag.js"
910
export * from "./erb-no-empty-tags.js"
1011
export * from "./erb-no-extra-newline.js"

javascript/packages/linter/test/__snapshots__/cli.test.ts.snap

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import dedent from "dedent"
2+
3+
import { describe, test } from "vitest"
4+
5+
import { ERBNoInlineCaseConditionsRule } from "../../src/rules/erb-no-inline-case-conditions.js"
6+
import { createLinterTest } from "../helpers/linter-test-helper.js"
7+
8+
const { expectNoOffenses, expectWarning, assertOffenses } = createLinterTest(ERBNoInlineCaseConditionsRule)
9+
10+
describe("ERBNoInlineCaseConditionsRule", () => {
11+
describe("case/when", () => {
12+
test("valid case/when in separate ERB tags", () => {
13+
expectNoOffenses(dedent`
14+
<% case variable %>
15+
<% when "a" %>
16+
A
17+
<% when "b" %>
18+
B
19+
<% else %>
20+
Other
21+
<% end %>
22+
`)
23+
})
24+
25+
test("invalid inline case/when", () => {
26+
expectWarning('A `case` statement with `when` conditions in a single ERB tag cannot be reliably parsed, compiled, and formatted. Use separate ERB tags for `case` and its conditions (e.g., `<% case x %>` followed by `<% when y %>`).')
27+
28+
assertOffenses(dedent`
29+
<% case variable when "a" %>
30+
A
31+
<% when "b" %>
32+
B
33+
<% end %>
34+
`)
35+
})
36+
37+
test("invalid case/when on newline in same tag", () => {
38+
expectWarning('A `case` statement with `when` conditions in a single ERB tag cannot be reliably parsed, compiled, and formatted. Use separate ERB tags for `case` and its conditions (e.g., `<% case x %>` followed by `<% when y %>`).')
39+
40+
assertOffenses(`<% case variable\n when "a" %>\n A\n<% when "b" %>\n B\n<% end %>`)
41+
})
42+
})
43+
44+
describe("case/in", () => {
45+
test("valid case/in in separate ERB tags", () => {
46+
expectNoOffenses(dedent`
47+
<% case value %>
48+
<% in 1 %>
49+
One
50+
<% in 2 %>
51+
Two
52+
<% else %>
53+
Other
54+
<% end %>
55+
`)
56+
})
57+
58+
test("invalid inline case/in", () => {
59+
expectWarning('A `case` statement with `in` conditions in a single ERB tag cannot be reliably parsed, compiled, and formatted. Use separate ERB tags for `case` and its conditions (e.g., `<% case x %>` followed by `<% in y %>`).')
60+
61+
assertOffenses(dedent`
62+
<% case value in 1 %>
63+
One
64+
<% in 2 %>
65+
Two
66+
<% end %>
67+
`)
68+
})
69+
70+
test("invalid case/in on newline in same tag", () => {
71+
expectWarning('A `case` statement with `in` conditions in a single ERB tag cannot be reliably parsed, compiled, and formatted. Use separate ERB tags for `case` and its conditions (e.g., `<% case x %>` followed by `<% in y %>`).')
72+
73+
assertOffenses(`<% case value\n in 1 %>\n One\n<% in 2 %>\n Two\n<% end %>`)
74+
})
75+
})
76+
})

0 commit comments

Comments
 (0)