Skip to content

Commit 1c90bed

Browse files
authored
Linter: Implement aufofix for erb-comment-syntax rule (#775)
This pull request implements the auto-fix functionality for the `erb-comment-syntax` linter rule. For example it will correct this: ```erb <% # bad comment %> <%= user.name %> <% # another bad comment %> ``` to this: ```erb <%# bad comment %> <%= user.name %> <%# another bad comment %> ```
1 parent 0bbd01b commit 1c90bed

File tree

4 files changed

+291
-9
lines changed

4 files changed

+291
-9
lines changed
Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import { BaseRuleVisitor } from "./rule-utils.js"
2-
import { ParserRule } from "../types.js"
2+
import { ParserRule, BaseAutofixContext, Mutable } from "../types.js"
33

4-
import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
4+
import type { UnboundLintOffense, LintOffense, LintContext, FullRuleConfig } from "../types.js"
55
import type { ParseResult, ERBContentNode } from "@herb-tools/core"
66

7-
class ERBCommentSyntaxVisitor extends BaseRuleVisitor {
7+
interface ERBCommentSyntaxAutofixContext extends BaseAutofixContext {
8+
node: Mutable<ERBContentNode>
9+
}
10+
11+
class ERBCommentSyntaxVisitor extends BaseRuleVisitor<ERBCommentSyntaxAutofixContext> {
812
visitERBContentNode(node: ERBContentNode): void {
913
const content = node.content?.value || ""
1014

@@ -14,19 +18,22 @@ class ERBCommentSyntaxVisitor extends BaseRuleVisitor {
1418
if (content.includes("herb:disable")) {
1519
this.addOffense(
1620
`Use \`<%#\` instead of \`${openingTag} #\` for \`herb:disable\` directives. Herb directives only work with ERB comment syntax (\`<%# ... %>\`).`,
17-
node.location
21+
node.location,
22+
{ node }
1823
)
1924
} else {
2025
this.addOffense(
2126
`Use \`<%#\` instead of \`${openingTag} #\`. Ruby comments immediately after ERB tags can cause parsing issues.`,
22-
node.location
27+
node.location,
28+
{ node }
2329
)
2430
}
2531
}
2632
}
2733
}
2834

29-
export class ERBCommentSyntax extends ParserRule {
35+
export class ERBCommentSyntax extends ParserRule<ERBCommentSyntaxAutofixContext> {
36+
static autocorrectable = true
3037
name = "erb-comment-syntax"
3138

3239
get defaultConfig(): FullRuleConfig {
@@ -36,11 +43,31 @@ export class ERBCommentSyntax extends ParserRule {
3643
}
3744
}
3845

39-
check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
46+
check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense<ERBCommentSyntaxAutofixContext>[] {
4047
const visitor = new ERBCommentSyntaxVisitor(this.name, context)
4148

4249
visitor.visit(result.value)
4350

4451
return visitor.offenses
4552
}
53+
54+
autofix(offense: LintOffense<ERBCommentSyntaxAutofixContext>, result: ParseResult, _context?: Partial<LintContext>): ParseResult | null {
55+
if (!offense.autofixContext) return null
56+
57+
const { node } = offense.autofixContext
58+
59+
if (!node.tag_opening) return null
60+
if (!node.content) return null
61+
62+
node.tag_opening.value = "<%#"
63+
64+
const content = node.content.value
65+
const match = content.match(/^ +(#)/)
66+
67+
if (match) {
68+
node.content.value = content.substring(match[0].length)
69+
}
70+
71+
return result
72+
}
4673
}

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

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
import { describe, test, expect, beforeAll } from "vitest"
2+
import dedent from "dedent"
3+
import { Herb } from "@herb-tools/node-wasm"
4+
import { Linter } from "../../src/linter.js"
5+
import { ERBCommentSyntax } from "../../src/rules/erb-comment-syntax.js"
6+
7+
describe("erb-comment-syntax autofix", () => {
8+
beforeAll(async () => {
9+
await Herb.load()
10+
})
11+
12+
test("fixes <% # comment %>", () => {
13+
const input = '<% # bad comment %>'
14+
const expected = '<%# bad comment %>'
15+
16+
const linter = new Linter(Herb, [ERBCommentSyntax])
17+
const result = linter.autofix(input)
18+
19+
expect(result.source).toBe(expected)
20+
expect(result.fixed).toHaveLength(1)
21+
expect(result.unfixed).toHaveLength(0)
22+
})
23+
24+
test("fixes <%= # comment %>", () => {
25+
const input = '<%= # bad comment %>'
26+
const expected = '<%# bad comment %>'
27+
28+
const linter = new Linter(Herb, [ERBCommentSyntax])
29+
const result = linter.autofix(input)
30+
31+
expect(result.source).toBe(expected)
32+
expect(result.fixed).toHaveLength(1)
33+
expect(result.unfixed).toHaveLength(0)
34+
})
35+
36+
test("fixes multiple bad comments in one file", () => {
37+
const input = dedent`
38+
<% # first bad comment %>
39+
<%= # second bad comment %>
40+
`
41+
42+
const expected = dedent`
43+
<%# first bad comment %>
44+
<%# second bad comment %>
45+
`
46+
47+
const linter = new Linter(Herb, [ERBCommentSyntax])
48+
const result = linter.autofix(input)
49+
50+
expect(result.source).toBe(expected)
51+
expect(result.fixed).toHaveLength(2)
52+
expect(result.unfixed).toHaveLength(0)
53+
})
54+
55+
test("fixes herb:disable with incorrect syntax", () => {
56+
const input = '<DIV></DIV><% # herb:disable html-tag-name-lowercase %>'
57+
const expected = '<DIV></DIV><%# herb:disable html-tag-name-lowercase %>'
58+
59+
const linter = new Linter(Herb, [ERBCommentSyntax])
60+
const result = linter.autofix(input)
61+
62+
expect(result.source).toBe(expected)
63+
expect(result.fixed).toHaveLength(1)
64+
expect(result.unfixed).toHaveLength(0)
65+
})
66+
67+
test("fixes herb:disable all with incorrect syntax", () => {
68+
const input = '<DIV></DIV><% # herb:disable all %>'
69+
const expected = '<DIV></DIV><%# herb:disable all %>'
70+
71+
const linter = new Linter(Herb, [ERBCommentSyntax])
72+
const result = linter.autofix(input)
73+
74+
expect(result.source).toBe(expected)
75+
expect(result.fixed).toHaveLength(1)
76+
expect(result.unfixed).toHaveLength(0)
77+
})
78+
79+
test("fixes herb:disable with extra whitespace", () => {
80+
const input = '<DIV></DIV><% # herb:disable html-tag-name-lowercase %>'
81+
const expected = '<DIV></DIV><%# herb:disable html-tag-name-lowercase %>'
82+
83+
const linter = new Linter(Herb, [ERBCommentSyntax])
84+
const result = linter.autofix(input)
85+
86+
expect(result.source).toBe(expected)
87+
expect(result.fixed).toHaveLength(1)
88+
expect(result.unfixed).toHaveLength(0)
89+
})
90+
91+
test("preserves already correct ERB comment syntax", () => {
92+
const input = '<%# good comment %>'
93+
const expected = '<%# good comment %>'
94+
95+
const linter = new Linter(Herb, [ERBCommentSyntax])
96+
const result = linter.autofix(input)
97+
98+
expect(result.source).toBe(expected)
99+
expect(result.fixed).toHaveLength(0)
100+
expect(result.unfixed).toHaveLength(0)
101+
})
102+
103+
test("preserves multi-line ERB with comment on new line", () => {
104+
const input = dedent`
105+
<%
106+
# good comment
107+
%>
108+
`
109+
110+
const expected = dedent`
111+
<%
112+
# good comment
113+
%>
114+
`
115+
116+
const linter = new Linter(Herb, [ERBCommentSyntax])
117+
const result = linter.autofix(input)
118+
119+
expect(result.source).toBe(expected)
120+
expect(result.fixed).toHaveLength(0)
121+
expect(result.unfixed).toHaveLength(0)
122+
})
123+
124+
test("fixes in complex template", () => {
125+
const input = dedent`
126+
<div>
127+
<% # bad comment %>
128+
<%= user.name %>
129+
<% # another bad comment %>
130+
</div>
131+
`
132+
133+
const expected = dedent`
134+
<div>
135+
<%# bad comment %>
136+
<%= user.name %>
137+
<%# another bad comment %>
138+
</div>
139+
`
140+
141+
const linter = new Linter(Herb, [ERBCommentSyntax])
142+
const result = linter.autofix(input)
143+
144+
expect(result.source).toBe(expected)
145+
expect(result.fixed).toHaveLength(2)
146+
expect(result.unfixed).toHaveLength(0)
147+
})
148+
149+
test("fixes <%== # comment %>", () => {
150+
const input = '<%== # escaped output comment %>'
151+
const expected = '<%# escaped output comment %>'
152+
153+
const linter = new Linter(Herb, [ERBCommentSyntax])
154+
const result = linter.autofix(input)
155+
156+
expect(result.source).toBe(expected)
157+
expect(result.fixed).toHaveLength(1)
158+
expect(result.unfixed).toHaveLength(0)
159+
})
160+
161+
test("fixes <%= # with multiple spaces %>", () => {
162+
const input = '<%= # comment with multiple spaces %>'
163+
const expected = '<%# comment with multiple spaces %>'
164+
165+
const linter = new Linter(Herb, [ERBCommentSyntax])
166+
const result = linter.autofix(input)
167+
168+
expect(result.source).toBe(expected)
169+
expect(result.fixed).toHaveLength(1)
170+
expect(result.unfixed).toHaveLength(0)
171+
})
172+
173+
test("fixes <%- # trim tag %>", () => {
174+
const input = '<%- # trim tag comment %>'
175+
const expected = '<%# trim tag comment %>'
176+
177+
const linter = new Linter(Herb, [ERBCommentSyntax])
178+
const result = linter.autofix(input)
179+
180+
expect(result.source).toBe(expected)
181+
expect(result.fixed).toHaveLength(1)
182+
expect(result.unfixed).toHaveLength(0)
183+
})
184+
185+
test("fixes <% # with many spaces %>", () => {
186+
const input = '<% # comment with many spaces %>'
187+
const expected = '<%# comment with many spaces %>'
188+
189+
const linter = new Linter(Herb, [ERBCommentSyntax])
190+
const result = linter.autofix(input)
191+
192+
expect(result.source).toBe(expected)
193+
expect(result.fixed).toHaveLength(1)
194+
expect(result.unfixed).toHaveLength(0)
195+
})
196+
197+
test("fixes all variations in one file", () => {
198+
const input = dedent`
199+
<% # regular %>
200+
<%= # output %>
201+
<%== # escaped output %>
202+
<%= # multiple spaces %>
203+
<%- # trim %>
204+
<% # many spaces %>
205+
`
206+
207+
const expected = dedent`
208+
<%# regular %>
209+
<%# output %>
210+
<%# escaped output %>
211+
<%# multiple spaces %>
212+
<%# trim %>
213+
<%# many spaces %>
214+
`
215+
216+
const linter = new Linter(Herb, [ERBCommentSyntax])
217+
const result = linter.autofix(input)
218+
219+
expect(result.source).toBe(expected)
220+
expect(result.fixed).toHaveLength(6)
221+
expect(result.unfixed).toHaveLength(0)
222+
})
223+
})

javascript/packages/linter/test/rules/erb-comment-syntax.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,36 @@ describe("ERBCommentSyntax", () => {
7878
<DIV></DIV><% # herb:disable html-tag-name-lowercase %>
7979
`)
8080
})
81+
82+
test("when ERB escaped output tag is used with incorrect syntax", () => {
83+
expectError("Use `<%#` instead of `<%== #`. Ruby comments immediately after ERB tags can cause parsing issues.")
84+
85+
assertOffenses(dedent`
86+
<%== # escaped output comment %>
87+
`)
88+
})
89+
90+
test("when ERB tag has multiple spaces before #", () => {
91+
expectError("Use `<%#` instead of `<%= #`. Ruby comments immediately after ERB tags can cause parsing issues.")
92+
93+
assertOffenses(dedent`
94+
<%= # comment with multiple spaces %>
95+
`)
96+
})
97+
98+
test("when ERB trim tag is used with incorrect syntax", () => {
99+
expectError("Use `<%#` instead of `<%- #`. Ruby comments immediately after ERB tags can cause parsing issues.")
100+
101+
assertOffenses(dedent`
102+
<%- # trim tag comment %>
103+
`)
104+
})
105+
106+
test("when ERB tag has many spaces before #", () => {
107+
expectError("Use `<%#` instead of `<% #`. Ruby comments immediately after ERB tags can cause parsing issues.")
108+
109+
assertOffenses(dedent`
110+
<% # comment with many spaces %>
111+
`)
112+
})
81113
})

0 commit comments

Comments
 (0)