diff --git a/javascript/packages/core/src/ast-utils.ts b/javascript/packages/core/src/ast-utils.ts index f158cd956..7cecc3d58 100644 --- a/javascript/packages/core/src/ast-utils.ts +++ b/javascript/packages/core/src/ast-utils.ts @@ -760,7 +760,7 @@ export function isEquivalentElement(first: HTMLElementNode, second: HTMLElementN // --- AST Mutation Utilities --- const CHILD_ARRAY_PROPS = ["children", "body", "statements", "conditions"] -const LINKED_NODE_PROPS = ["subsequent", "else_clause"] +const LINKED_NODE_PROPS = ["subsequent", "else_clause", "open_tag", "close_tag", "value", "name"] /** * Finds the array containing a target node in the AST, along with its index. diff --git a/javascript/packages/linter/src/cli/diff.ts b/javascript/packages/linter/src/cli/diff.ts new file mode 100644 index 000000000..537f0c216 --- /dev/null +++ b/javascript/packages/linter/src/cli/diff.ts @@ -0,0 +1,157 @@ +import { colorize } from "@herb-tools/highlighter" + +export interface DiffLine { + type: "context" | "removed" | "added" + content: string + lineNumber: number +} + +export interface DiffHunk { + lines: DiffLine[] +} + +export function computeDiff(original: string, fixed: string, contextLines: number = 1): DiffHunk[] { + if (original === fixed) return [] + + const originalLines = original.split("\n") + const fixedLines = fixed.split("\n") + + if (originalLines.length === fixedLines.length) { + return computeParallelDiff(originalLines, fixedLines, contextLines) + } + + return computeBlockDiff(originalLines, fixedLines, contextLines) +} + +function computeParallelDiff(originalLines: string[], fixedLines: string[], contextLines: number): DiffHunk[] { + const changeIndices: number[] = [] + + for (let index = 0; index < originalLines.length; index++) { + if (originalLines[index] !== fixedLines[index]) { + changeIndices.push(index) + } + } + + if (changeIndices.length === 0) return [] + + const changeGroups: number[][] = [] + let currentGroup: number[] = [changeIndices[0]] + + for (let index = 1; index < changeIndices.length; index++) { + const gapBetweenChanges = changeIndices[index] - changeIndices[index - 1] - 1 + + if (gapBetweenChanges <= contextLines * 2) { + currentGroup.push(changeIndices[index]) + } else { + changeGroups.push(currentGroup) + currentGroup = [changeIndices[index]] + } + } + + changeGroups.push(currentGroup) + + const hunks: DiffHunk[] = [] + + for (const group of changeGroups) { + const hunk: DiffHunk = { lines: [] } + const firstChange = group[0] + const lastChange = group[group.length - 1] + const contextStart = Math.max(0, firstChange - contextLines) + const contextEnd = Math.min(originalLines.length - 1, lastChange + contextLines) + + for (let index = contextStart; index <= contextEnd; index++) { + if (group.includes(index)) { + hunk.lines.push({ type: "removed", content: originalLines[index], lineNumber: index + 1 }) + hunk.lines.push({ type: "added", content: fixedLines[index], lineNumber: index + 1 }) + } else { + hunk.lines.push({ type: "context", content: originalLines[index], lineNumber: index + 1 }) + } + } + + hunks.push(hunk) + } + + return hunks +} + +function computeBlockDiff(originalLines: string[], fixedLines: string[], contextLines: number): DiffHunk[] { + let prefixLength = 0 + + while ( + prefixLength < originalLines.length && + prefixLength < fixedLines.length && + originalLines[prefixLength] === fixedLines[prefixLength] + ) { + prefixLength++ + } + + let originalEnd = originalLines.length - 1 + let fixedEnd = fixedLines.length - 1 + + while ( + originalEnd > prefixLength && + fixedEnd > prefixLength && + originalLines[originalEnd] === fixedLines[fixedEnd] + ) { + originalEnd-- + fixedEnd-- + } + + if (prefixLength > originalEnd && prefixLength > fixedEnd) { + return [] + } + + const hunk: DiffHunk = { lines: [] } + + const contextStart = Math.max(0, prefixLength - contextLines) + + for (let index = contextStart; index < prefixLength; index++) { + hunk.lines.push({ type: "context", content: originalLines[index], lineNumber: index + 1 }) + } + + for (let index = prefixLength; index <= originalEnd; index++) { + hunk.lines.push({ type: "removed", content: originalLines[index], lineNumber: index + 1 }) + } + + for (let index = prefixLength; index <= fixedEnd; index++) { + hunk.lines.push({ type: "added", content: fixedLines[index], lineNumber: index + 1 }) + } + + const contextEnd = Math.min(originalLines.length - 1, originalEnd + contextLines) + + for (let index = originalEnd + 1; index <= contextEnd; index++) { + hunk.lines.push({ type: "context", content: originalLines[index], lineNumber: index + 1 }) + } + + return hunk.lines.length > 0 ? [hunk] : [] +} + +export function formatDiff(hunks: DiffHunk[], indent: string = " "): string { + const lines: string[] = [] + + for (let hunkIndex = 0; hunkIndex < hunks.length; hunkIndex++) { + const hunk = hunks[hunkIndex] + + if (hunkIndex > 0) { + lines.push(indent + colorize(" ...", "gray")) + } + + for (const line of hunk.lines) { + switch (line.type) { + case "removed": + lines.push(indent + colorize(`- ${line.content}`, "red")) + break + + case "added": + lines.push(indent + colorize(`+ ${line.content}`, "green")) + break + + case "context": + lines.push(indent + colorize(` ${line.content}`, "gray")) + break + } + } + } + + return lines.join("\n") +} diff --git a/javascript/packages/linter/src/cli/file-processor.ts b/javascript/packages/linter/src/cli/file-processor.ts index 55da3a402..c3c941892 100644 --- a/javascript/packages/linter/src/cli/file-processor.ts +++ b/javascript/packages/linter/src/cli/file-processor.ts @@ -11,6 +11,9 @@ import { fileURLToPath } from "node:url" import { availableParallelism } from "node:os" import { colorize } from "@herb-tools/highlighter" +import { computeDiff, formatDiff } from "./diff.js" + +import type { DiffHunk } from "./diff.js" import type { Diagnostic } from "@herb-tools/core" import type { FormatOption } from "./argument-parser.js" import type { HerbConfigOptions } from "@herb-tools/config" @@ -22,6 +25,7 @@ export interface ProcessedFile { offense: Diagnostic content: string autocorrectable?: boolean + autofixDiff?: DiffHunk[] } export interface ProcessingContext { @@ -208,11 +212,26 @@ export class FileProcessor { } } else { for (const offense of lintResult.offenses) { + const autocorrectable = this.isRuleAutocorrectable(offense.rule) + let autofixDiff: DiffHunk[] | undefined + + if (autocorrectable && formatOption !== "json") { + const previewResult = this.linter.previewAutofix(content, { + fileName: filename, + ignoreDisableComments: context?.ignoreDisableComments + }, [offense], { includeUnsafe: true }) + + if (previewResult.fixed.length > 0) { + autofixDiff = computeDiff(content, previewResult.source) + } + } + allOffenses.push({ filename, offense: offense, content, - autocorrectable: this.isRuleAutocorrectable(offense.rule) + autocorrectable, + autofixDiff, }) const ruleData = ruleOffenses.get(offense.rule) || { count: 0, files: new Set() } diff --git a/javascript/packages/linter/src/cli/formatters/detailed-formatter.ts b/javascript/packages/linter/src/cli/formatters/detailed-formatter.ts index b31fbce29..03a107fdd 100644 --- a/javascript/packages/linter/src/cli/formatters/detailed-formatter.ts +++ b/javascript/packages/linter/src/cli/formatters/detailed-formatter.ts @@ -4,6 +4,7 @@ import { BaseFormatter } from "./base-formatter.js" import { LineWrapper } from "@herb-tools/highlighter" import { ruleDocumentationUrl } from "../../urls.js" import { fileUrl } from "../file-url.js" +import { formatDiff } from "../diff.js" import type { Diagnostic } from "@herb-tools/core" import type { ProcessedFile } from "../file-processor.js" @@ -34,7 +35,9 @@ export class DetailedFormatter extends BaseFormatter { allOffenses.filter(item => item.autocorrectable).map(item => item.offense) ) - if (isSingleFile) { + const hasDiffs = allOffenses.some(item => item.autofixDiff && item.autofixDiff.length > 0) + + if (isSingleFile && !hasDiffs) { const { filename, content } = allOffenses[0] const diagnostics = allOffenses.map(item => item.offense) @@ -53,8 +56,8 @@ export class DetailedFormatter extends BaseFormatter { } else { const totalMessageCount = allOffenses.length - for (let i = 0; i < allOffenses.length; i++) { - const { filename, offense, content, autocorrectable } = allOffenses[i] + for (let index = 0; index < allOffenses.length; index++) { + const { filename, offense, content, autocorrectable, autofixDiff } = allOffenses[index] const codeUrl = offense.code ? ruleDocumentationUrl(offense.code) : undefined const suffix = autocorrectable ? correctableTag : undefined @@ -68,8 +71,15 @@ export class DetailedFormatter extends BaseFormatter { }) console.log(`\n${formatted}`) + if (autofixDiff && autofixDiff.length > 0) { + const diffHeader = colorize(" Suggested fix:", "cyan") + const diffOutput = formatDiff(autofixDiff) + + console.log(`${diffHeader}\n${diffOutput}`) + } + const width = LineWrapper.getTerminalWidth() - const progressText = `[${i + 1}/${totalMessageCount}]` + const progressText = `[${index + 1}/${totalMessageCount}]` const rightPadding = 16 const separatorLength = Math.max(0, width - progressText.length - 1 - rightPadding) const separator = '⎯' diff --git a/javascript/packages/linter/src/cli/formatters/simple-formatter.ts b/javascript/packages/linter/src/cli/formatters/simple-formatter.ts index 0898f7693..9d64affe0 100644 --- a/javascript/packages/linter/src/cli/formatters/simple-formatter.ts +++ b/javascript/packages/linter/src/cli/formatters/simple-formatter.ts @@ -3,6 +3,7 @@ import { colorize, hyperlink, TextFormatter } from "@herb-tools/highlighter" import { BaseFormatter } from "./base-formatter.js" import { ruleDocumentationUrl } from "../../urls.js" import { fileUrl } from "../file-url.js" +import { formatDiff } from "../diff.js" import type { Diagnostic } from "@herb-tools/core" import type { ProcessedFile } from "../file-processor.js" @@ -49,7 +50,7 @@ export class SimpleFormatter extends BaseFormatter { const filenameLink = hyperlink(filenameText, fileUrl(filename)) console.log(`${filenameLink}:`) - for (const { offense, autocorrectable } of processedFiles) { + for (const { offense, autocorrectable, autofixDiff } of processedFiles) { const isError = offense.severity === "error" const severity = isError ? colorize("✗", "brightRed") : colorize("⚠", "brightYellow") const ruleText = `(${offense.code})` @@ -61,6 +62,12 @@ export class SimpleFormatter extends BaseFormatter { const message = TextFormatter.highlightBackticks(offense.message) console.log(` ${paddedLocation} ${severity} ${message} ${rule}${correctable}`) + + if (autofixDiff && autofixDiff.length > 0) { + const diffOutput = formatDiff(autofixDiff, " ") + + console.log(diffOutput) + } } } } diff --git a/javascript/packages/linter/src/linter.ts b/javascript/packages/linter/src/linter.ts index d0dad7381..8ae080828 100644 --- a/javascript/packages/linter/src/linter.ts +++ b/javascript/packages/linter/src/linter.ts @@ -571,8 +571,8 @@ export class Linter { const unfixed: LintOffense[] = [] if (parserOffenses.length > 0) { - const parseResult = this.parseCache.get(currentSource) let needsReindent = false + let lastParseResult: ParseResult | null = null for (const offense of parserOffenses) { const ruleClass = this.findRuleClass(offense.rule) @@ -598,6 +598,9 @@ export class Linter { continue } + const parserOptions = rule.parserOptions || {} + const parseResult = this.parseCache.get(currentSource, parserOptions) + if (offense.autofixContext) { const originalNodeType = offense.autofixContext.node.type const location: Location = offense.autofixContext.node.location ? Location.from(offense.autofixContext.node.location) : offense.location @@ -621,6 +624,7 @@ export class Linter { if (fixedResult) { fixed.push(offense) + lastParseResult = parseResult if (this.isParserRuleClass(ruleClass) && ruleClass.reindentAfterAutofix === true) { needsReindent = true @@ -630,11 +634,11 @@ export class Linter { } } - if (fixed.length > 0) { + if (fixed.length > 0 && lastParseResult) { if (needsReindent) { - currentSource = new IndentPrinter().print(parseResult.value) + currentSource = new IndentPrinter().print(lastParseResult.value) } else { - currentSource = new IdentityPrinter().print(parseResult.value) + currentSource = new IdentityPrinter().print(lastParseResult.value) } } } @@ -686,4 +690,14 @@ export class Linter { unfixed } } + + previewAutofix(source: string, context?: Partial, offensesToFix?: LintOffense[], options?: { includeUnsafe?: boolean }): AutofixResult { + this.parseCache.clear() + + const result = this.autofix(source, context, offensesToFix, options) + + this.parseCache.clear() + + 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 21a92ae51..fbfd8f3e7 100644 --- a/javascript/packages/linter/test/__snapshots__/cli.test.ts.snap +++ b/javascript/packages/linter/test/__snapshots__/cli.test.ts.snap @@ -12,9 +12,9 @@ test/fixtures/ignored.html.erb:8:2 9 │ 10 │ - ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ [1/7] ⎯⎯⎯⎯ + [error] Opening tag name \`
\` should be lowercase. Use \`
\` instead. (html-tag-name-lowercase) [Correctable] test/fixtures/ignored.html.erb:6:3 @@ -26,9 +26,14 @@ test/fixtures/ignored.html.erb:6:3 7 │
hello
<%# herb:disable html-tag-name-lowercase %> 8 │ <% %> <%# herb:disable erb-no-empty-tags %> - + Suggested fix: + + -
hello
+ +
hello
+
hello
<%# herb:disable html-tag-name-lowercase %> ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ [2/7] ⎯⎯⎯⎯ + [error] Closing tag name \`
\` should be lowercase. Use \`
\` instead. (html-tag-name-lowercase) [Correctable] test/fixtures/ignored.html.erb:6:14 @@ -40,9 +45,14 @@ test/fixtures/ignored.html.erb:6:14 7 │
hello
<%# herb:disable html-tag-name-lowercase %> 8 │ <% %> <%# herb:disable erb-no-empty-tags %> - + Suggested fix: + + -
hello
+ +
hello
+
hello
<%# herb:disable html-tag-name-lowercase %> ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ [3/7] ⎯⎯⎯⎯ + [error] Opening tag name \`
\` should be lowercase. Use \`
\` instead. (html-tag-name-lowercase) [Correctable] test/fixtures/ignored.html.erb:7:3 @@ -54,9 +64,14 @@ test/fixtures/ignored.html.erb:7:3 8 │ <% %> <%# herb:disable erb-no-empty-tags %> 9 │
- + Suggested fix: +
hello
+ -
hello
<%# herb:disable html-tag-name-lowercase %> + +
hello
<%# herb:disable html-tag-name-lowercase %> + <% %> <%# herb:disable erb-no-empty-tags %> ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ [4/7] ⎯⎯⎯⎯ + [error] Closing tag name \`
\` should be lowercase. Use \`\` instead. (html-tag-name-lowercase) [Correctable] test/fixtures/ignored.html.erb:7:14 @@ -68,9 +83,14 @@ test/fixtures/ignored.html.erb:7:14 8 │ <% %> <%# herb:disable erb-no-empty-tags %> 9 │ - + Suggested fix: +
hello
+ -
hello
<%# herb:disable html-tag-name-lowercase %> + +
hello
<%# herb:disable html-tag-name-lowercase %> + <% %> <%# herb:disable erb-no-empty-tags %> ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ [5/7] ⎯⎯⎯⎯ + [warning] No offenses from \`html-tag-name-lowercase\` on this line. Remove the \`herb:disable\` comment. (herb-disable-comment-unnecessary) test/fixtures/ignored.html.erb:7:19 @@ -82,9 +102,9 @@ test/fixtures/ignored.html.erb:7:19 8 │ <% %> <%# herb:disable erb-no-empty-tags %> 9 │ - ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ [6/7] ⎯⎯⎯⎯ + [warning] No offenses from \`erb-no-empty-tags\` on this line. Remove the \`herb:disable\` comment. (herb-disable-comment-unnecessary) test/fixtures/ignored.html.erb:8:8 @@ -96,6 +116,8 @@ test/fixtures/ignored.html.erb:8:8 9 │ 10 │ +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ [7/7] ⎯⎯⎯⎯ + Rule offenses: @@ -133,9 +155,9 @@ test/fixtures/test-file-with-errors.html.erb:3:3 4 │ 5 │ - ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ [1/3] ⎯⎯⎯⎯ + [error] Opening tag name \`\` should be lowercase. Use \`\` instead. (html-tag-name-lowercase) [Correctable] test/fixtures/test-file-with-errors.html.erb:2:3 @@ -146,9 +168,14 @@ test/fixtures/test-file-with-errors.html.erb:2:3 3 │ 4 │ - + Suggested fix: +
+ - Test content + + Test content + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ [2/3] ⎯⎯⎯⎯ + [error] Closing tag name \`\` should be lowercase. Use \`\` instead. (html-tag-name-lowercase) [Correctable] test/fixtures/test-file-with-errors.html.erb:2:22 @@ -159,6 +186,13 @@ test/fixtures/test-file-with-errors.html.erb:2:22 3 │ 4 │
+ Suggested fix: +
+ - Test content + + Test content + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ [3/3] ⎯⎯⎯⎯ + Rule offenses: @@ -183,6 +217,11 @@ test/fixtures/no-trailing-newline.html.erb:1:29 → 1 │
No trailing newline
│ ~ + Suggested fix: +
No trailing newline
+ + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ [1/1] ⎯⎯⎯⎯ + Rule offenses: @@ -206,9 +245,13 @@ test/fixtures/erb-no-extra-whitespace-inside-tags.html.erb:1:2 2 │ 3 │ <% - + Suggested fix: + - <% extra_whitespace %> + + <% extra_whitespace %> + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ [1/4] ⎯⎯⎯⎯ + [error] Remove extra whitespace before \`%>\`. (erb-no-extra-whitespace-inside-tags) [Correctable] test/fixtures/erb-no-extra-whitespace-inside-tags.html.erb:1:20 @@ -218,9 +261,13 @@ test/fixtures/erb-no-extra-whitespace-inside-tags.html.erb:1:20 2 │ 3 │ <% - + Suggested fix: + - <% extra_whitespace %> + + <% extra_whitespace %> + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ [2/4] ⎯⎯⎯⎯ + [error] Remove extra whitespace after \`<%=\`. (erb-no-extra-whitespace-inside-tags) [Correctable] test/fixtures/erb-no-extra-whitespace-inside-tags.html.erb:9:3 @@ -232,9 +279,14 @@ test/fixtures/erb-no-extra-whitespace-inside-tags.html.erb:9:3 10 │ as: :post 11 │ %> - + Suggested fix: + + - <%= render partial: "post", + + <%= render partial: "post", + as: :post ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ [3/4] ⎯⎯⎯⎯ + [error] Avoid unused expressions in silent ERB tags. \`extra_whitespace\` is evaluated but its return value is discarded. Use \`<%= ... %>\` to output the value or remove the expression. (erb-no-unused-expressions) test/fixtures/erb-no-extra-whitespace-inside-tags.html.erb:1:4 @@ -244,6 +296,8 @@ test/fixtures/erb-no-extra-whitespace-inside-tags.html.erb:1:4 2 │ 3 │ <% +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ [4/4] ⎯⎯⎯⎯ + Rule offenses: @@ -273,9 +327,14 @@ test/fixtures/ignored.html.erb:6:3 7 │
hello
<%# herb:disable html-tag-name-lowercase %> 8 │ <% %> <%# herb:disable erb-no-empty-tags %> - + Suggested fix: + + -
hello
+ +
hello
+
hello
<%# herb:disable html-tag-name-lowercase %> ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ [1/2] ⎯⎯⎯⎯ + [error] Closing tag name \`
\` should be lowercase. Use \`\` instead. (html-tag-name-lowercase) [Correctable] test/fixtures/ignored.html.erb:6:14 @@ -287,6 +346,13 @@ test/fixtures/ignored.html.erb:6:14 7 │
hello
<%# herb:disable html-tag-name-lowercase %> 8 │ <% %> <%# herb:disable erb-no-empty-tags %> + Suggested fix: + + -
hello
+ +
hello
+
hello
<%# herb:disable html-tag-name-lowercase %> +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ [2/2] ⎯⎯⎯⎯ + Rule offenses: @@ -342,9 +408,9 @@ test/fixtures/multiple-rule-offenses.html.erb:5:3 6 │ 7 │ - ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ [1/14] ⎯⎯⎯⎯ + [error] Add an \`href\` attribute to \`\` to ensure it is focusable and accessible. Links should navigate somewhere. If you need a clickable element without navigation, use a \`