Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 34 additions & 7 deletions javascript/packages/linter/src/rules/erb-comment-syntax.ts
Original file line number Diff line number Diff line change
@@ -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<ERBContentNode>
}

class ERBCommentSyntaxVisitor extends BaseRuleVisitor<ERBCommentSyntaxAutofixContext> {
visitERBContentNode(node: ERBContentNode): void {
const content = node.content?.value || ""

Expand All @@ -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<ERBCommentSyntaxAutofixContext> {
static autocorrectable = true
name = "erb-comment-syntax"

get defaultConfig(): FullRuleConfig {
Expand All @@ -36,11 +43,31 @@ export class ERBCommentSyntax extends ParserRule {
}
}

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

visitor.visit(result.value)

return visitor.offenses
}

autofix(offense: LintOffense<ERBCommentSyntaxAutofixContext>, result: ParseResult, _context?: Partial<LintContext>): 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
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -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 = '<DIV></DIV><% # herb:disable html-tag-name-lowercase %>'
const expected = '<DIV></DIV><%# 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 = '<DIV></DIV><% # herb:disable all %>'
const expected = '<DIV></DIV><%# 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 = '<DIV></DIV><% # herb:disable html-tag-name-lowercase %>'
const expected = '<DIV></DIV><%# 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`
<div>
<% # bad comment %>
<%= user.name %>
<% # another bad comment %>
</div>
`

const expected = dedent`
<div>
<%# bad comment %>
<%= user.name %>
<%# another bad comment %>
</div>
`

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)
})
})
32 changes: 32 additions & 0 deletions javascript/packages/linter/test/rules/erb-comment-syntax.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,36 @@ describe("ERBCommentSyntax", () => {
<DIV></DIV><% # 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 %>
`)
})
})
Loading