diff --git a/javascript/packages/linter/src/rules/erb-comment-syntax.ts b/javascript/packages/linter/src/rules/erb-comment-syntax.ts index 4da6eeaf9..60dfcd7af 100644 --- a/javascript/packages/linter/src/rules/erb-comment-syntax.ts +++ b/javascript/packages/linter/src/rules/erb-comment-syntax.ts @@ -1,10 +1,14 @@ import { BaseRuleVisitor } from "./rule-utils.js" -import { ParserRule } from "../types.js" +import { ParserRule, BaseAutofixContext, Mutable } from "../types.js" -import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js" +import type { UnboundLintOffense, LintOffense, LintContext, FullRuleConfig } from "../types.js" import type { ParseResult, ERBContentNode } from "@herb-tools/core" -class ERBCommentSyntaxVisitor extends BaseRuleVisitor { +interface ERBCommentSyntaxAutofixContext extends BaseAutofixContext { + node: Mutable +} + +class ERBCommentSyntaxVisitor extends BaseRuleVisitor { visitERBContentNode(node: ERBContentNode): void { const content = node.content?.value || "" @@ -14,19 +18,22 @@ class ERBCommentSyntaxVisitor extends BaseRuleVisitor { if (content.includes("herb:disable")) { this.addOffense( `Use \`<%#\` instead of \`${openingTag} #\` for \`herb:disable\` directives. Herb directives only work with ERB comment syntax (\`<%# ... %>\`).`, - node.location + node.location, + { node } ) } else { this.addOffense( `Use \`<%#\` instead of \`${openingTag} #\`. Ruby comments immediately after ERB tags can cause parsing issues.`, - node.location + node.location, + { node } ) } } } } -export class ERBCommentSyntax extends ParserRule { +export class ERBCommentSyntax extends ParserRule { + static autocorrectable = true name = "erb-comment-syntax" get defaultConfig(): FullRuleConfig { @@ -36,11 +43,31 @@ export class ERBCommentSyntax extends ParserRule { } } - check(result: ParseResult, context?: Partial): UnboundLintOffense[] { + check(result: ParseResult, context?: Partial): UnboundLintOffense[] { const visitor = new ERBCommentSyntaxVisitor(this.name, context) visitor.visit(result.value) return visitor.offenses } + + autofix(offense: LintOffense, result: ParseResult, _context?: Partial): ParseResult | null { + if (!offense.autofixContext) return null + + const { node } = offense.autofixContext + + if (!node.tag_opening) return null + if (!node.content) return null + + node.tag_opening.value = "<%#" + + const content = node.content.value + const match = content.match(/^ +(#)/) + + if (match) { + node.content.value = content.substring(match[0].length) + } + + return result + } } diff --git a/javascript/packages/linter/test/__snapshots__/cli.test.ts.snap b/javascript/packages/linter/test/__snapshots__/cli.test.ts.snap index d884e59f1..d2e45ab98 100644 --- a/javascript/packages/linter/test/__snapshots__/cli.test.ts.snap +++ b/javascript/packages/linter/test/__snapshots__/cli.test.ts.snap @@ -999,7 +999,7 @@ bad-file.html.erb:1:16 `; exports[`CLI Output Formatting > herb:disable rules 1`] = ` -"[error] Use \`<%#\` instead of \`<% #\` for \`herb:disable\` directives. Herb directives only work with ERB comment syntax (\`<%# ... %>\`). (erb-comment-syntax) +"[error] Use \`<%#\` instead of \`<% #\` for \`herb:disable\` directives. Herb directives only work with ERB comment syntax (\`<%# ... %>\`). [Correctable] (erb-comment-syntax) disabled-1.html.erb:12:19 @@ -1122,7 +1122,7 @@ disabled-1.html.erb:14:19 Summary: Checked 1 file Offenses 2 errors | 6 warnings | 8 ignored (8 offenses across 1 file) - Fixable 8 offenses" + Fixable 8 offenses | 1 autocorrectable using \`--fix\`" `; exports[`CLI Output Formatting > herb:disable rules 2`] = ` diff --git a/javascript/packages/linter/test/autofix/erb-comment-syntax.autofix.test.ts b/javascript/packages/linter/test/autofix/erb-comment-syntax.autofix.test.ts new file mode 100644 index 000000000..a31f39fd7 --- /dev/null +++ b/javascript/packages/linter/test/autofix/erb-comment-syntax.autofix.test.ts @@ -0,0 +1,223 @@ +import { describe, test, expect, beforeAll } from "vitest" +import dedent from "dedent" +import { Herb } from "@herb-tools/node-wasm" +import { Linter } from "../../src/linter.js" +import { ERBCommentSyntax } from "../../src/rules/erb-comment-syntax.js" + +describe("erb-comment-syntax autofix", () => { + beforeAll(async () => { + await Herb.load() + }) + + test("fixes <% # comment %>", () => { + const input = '<% # bad comment %>' + const expected = '<%# bad comment %>' + + const linter = new Linter(Herb, [ERBCommentSyntax]) + const result = linter.autofix(input) + + expect(result.source).toBe(expected) + expect(result.fixed).toHaveLength(1) + expect(result.unfixed).toHaveLength(0) + }) + + test("fixes <%= # comment %>", () => { + const input = '<%= # bad comment %>' + const expected = '<%# bad comment %>' + + const linter = new Linter(Herb, [ERBCommentSyntax]) + const result = linter.autofix(input) + + expect(result.source).toBe(expected) + expect(result.fixed).toHaveLength(1) + expect(result.unfixed).toHaveLength(0) + }) + + test("fixes multiple bad comments in one file", () => { + const input = dedent` + <% # first bad comment %> + <%= # second bad comment %> + ` + + const expected = dedent` + <%# first bad comment %> + <%# second bad comment %> + ` + + const linter = new Linter(Herb, [ERBCommentSyntax]) + const result = linter.autofix(input) + + expect(result.source).toBe(expected) + expect(result.fixed).toHaveLength(2) + expect(result.unfixed).toHaveLength(0) + }) + + test("fixes herb:disable with incorrect syntax", () => { + const input = '
<% # herb:disable html-tag-name-lowercase %>' + const expected = '
<%# herb:disable html-tag-name-lowercase %>' + + const linter = new Linter(Herb, [ERBCommentSyntax]) + const result = linter.autofix(input) + + expect(result.source).toBe(expected) + expect(result.fixed).toHaveLength(1) + expect(result.unfixed).toHaveLength(0) + }) + + test("fixes herb:disable all with incorrect syntax", () => { + const input = '
<% # herb:disable all %>' + const expected = '
<%# herb:disable all %>' + + const linter = new Linter(Herb, [ERBCommentSyntax]) + const result = linter.autofix(input) + + expect(result.source).toBe(expected) + expect(result.fixed).toHaveLength(1) + expect(result.unfixed).toHaveLength(0) + }) + + test("fixes herb:disable with extra whitespace", () => { + const input = '
<% # herb:disable html-tag-name-lowercase %>' + const expected = '
<%# herb:disable html-tag-name-lowercase %>' + + const linter = new Linter(Herb, [ERBCommentSyntax]) + const result = linter.autofix(input) + + expect(result.source).toBe(expected) + expect(result.fixed).toHaveLength(1) + expect(result.unfixed).toHaveLength(0) + }) + + test("preserves already correct ERB comment syntax", () => { + const input = '<%# good comment %>' + const expected = '<%# good comment %>' + + const linter = new Linter(Herb, [ERBCommentSyntax]) + const result = linter.autofix(input) + + expect(result.source).toBe(expected) + expect(result.fixed).toHaveLength(0) + expect(result.unfixed).toHaveLength(0) + }) + + test("preserves multi-line ERB with comment on new line", () => { + const input = dedent` + <% + # good comment + %> + ` + + const expected = dedent` + <% + # good comment + %> + ` + + const linter = new Linter(Herb, [ERBCommentSyntax]) + const result = linter.autofix(input) + + expect(result.source).toBe(expected) + expect(result.fixed).toHaveLength(0) + expect(result.unfixed).toHaveLength(0) + }) + + test("fixes in complex template", () => { + const input = dedent` +
+ <% # bad comment %> + <%= user.name %> + <% # another bad comment %> +
+ ` + + const expected = dedent` +
+ <%# bad comment %> + <%= user.name %> + <%# another bad comment %> +
+ ` + + const linter = new Linter(Herb, [ERBCommentSyntax]) + const result = linter.autofix(input) + + expect(result.source).toBe(expected) + expect(result.fixed).toHaveLength(2) + expect(result.unfixed).toHaveLength(0) + }) + + test("fixes <%== # comment %>", () => { + const input = '<%== # escaped output comment %>' + const expected = '<%# escaped output comment %>' + + const linter = new Linter(Herb, [ERBCommentSyntax]) + const result = linter.autofix(input) + + expect(result.source).toBe(expected) + expect(result.fixed).toHaveLength(1) + expect(result.unfixed).toHaveLength(0) + }) + + test("fixes <%= # with multiple spaces %>", () => { + const input = '<%= # comment with multiple spaces %>' + const expected = '<%# comment with multiple spaces %>' + + const linter = new Linter(Herb, [ERBCommentSyntax]) + const result = linter.autofix(input) + + expect(result.source).toBe(expected) + expect(result.fixed).toHaveLength(1) + expect(result.unfixed).toHaveLength(0) + }) + + test("fixes <%- # trim tag %>", () => { + const input = '<%- # trim tag comment %>' + const expected = '<%# trim tag comment %>' + + const linter = new Linter(Herb, [ERBCommentSyntax]) + const result = linter.autofix(input) + + expect(result.source).toBe(expected) + expect(result.fixed).toHaveLength(1) + expect(result.unfixed).toHaveLength(0) + }) + + test("fixes <% # with many spaces %>", () => { + const input = '<% # comment with many spaces %>' + const expected = '<%# comment with many spaces %>' + + const linter = new Linter(Herb, [ERBCommentSyntax]) + const result = linter.autofix(input) + + expect(result.source).toBe(expected) + expect(result.fixed).toHaveLength(1) + expect(result.unfixed).toHaveLength(0) + }) + + test("fixes all variations in one file", () => { + const input = dedent` + <% # regular %> + <%= # output %> + <%== # escaped output %> + <%= # multiple spaces %> + <%- # trim %> + <% # many spaces %> + ` + + const expected = dedent` + <%# regular %> + <%# output %> + <%# escaped output %> + <%# multiple spaces %> + <%# trim %> + <%# many spaces %> + ` + + const linter = new Linter(Herb, [ERBCommentSyntax]) + const result = linter.autofix(input) + + expect(result.source).toBe(expected) + expect(result.fixed).toHaveLength(6) + expect(result.unfixed).toHaveLength(0) + }) +}) diff --git a/javascript/packages/linter/test/rules/erb-comment-syntax.test.ts b/javascript/packages/linter/test/rules/erb-comment-syntax.test.ts index 7dad3b392..1ff965835 100644 --- a/javascript/packages/linter/test/rules/erb-comment-syntax.test.ts +++ b/javascript/packages/linter/test/rules/erb-comment-syntax.test.ts @@ -78,4 +78,36 @@ describe("ERBCommentSyntax", () => {
<% # herb:disable html-tag-name-lowercase %> `) }) + + test("when ERB escaped output tag is used with incorrect syntax", () => { + expectError("Use `<%#` instead of `<%== #`. Ruby comments immediately after ERB tags can cause parsing issues.") + + assertOffenses(dedent` + <%== # escaped output comment %> + `) + }) + + test("when ERB tag has multiple spaces before #", () => { + expectError("Use `<%#` instead of `<%= #`. Ruby comments immediately after ERB tags can cause parsing issues.") + + assertOffenses(dedent` + <%= # comment with multiple spaces %> + `) + }) + + test("when ERB trim tag is used with incorrect syntax", () => { + expectError("Use `<%#` instead of `<%- #`. Ruby comments immediately after ERB tags can cause parsing issues.") + + assertOffenses(dedent` + <%- # trim tag comment %> + `) + }) + + test("when ERB tag has many spaces before #", () => { + expectError("Use `<%#` instead of `<% #`. Ruby comments immediately after ERB tags can cause parsing issues.") + + assertOffenses(dedent` + <% # comment with many spaces %> + `) + }) })