diff --git a/javascript/packages/highlighter/src/diagnostic-renderer.ts b/javascript/packages/highlighter/src/diagnostic-renderer.ts index bdcdcf610..9ec29788a 100644 --- a/javascript/packages/highlighter/src/diagnostic-renderer.ts +++ b/javascript/packages/highlighter/src/diagnostic-renderer.ts @@ -14,6 +14,7 @@ export interface DiagnosticRenderOptions { maxWidth?: number truncateLines?: boolean codeUrl?: string + suffix?: string } export class DiagnosticRenderer { @@ -145,9 +146,9 @@ export class DiagnosticRenderer { const { codeUrl } = options - const text = diagnostic.severity const color = severityColor(diagnostic.severity) - const diagnosticIdText = colorize(diagnostic.code || "-", "gray") + const text = colorize(colorize(diagnostic.severity, color), "bold") + const diagnosticIdText = diagnostic.code || "-" const diagnosticId = codeUrl ? hyperlink(diagnosticIdText, codeUrl) : diagnosticIdText const originalLines = content.split("\n") @@ -249,8 +250,10 @@ export class DiagnosticRenderer { } const highlightedMessage = this.highlightBackticks(diagnostic.message) + const { suffix } = options + const suffixText = suffix ? ` ${suffix}` : "" - return `[${text}] ${highlightedMessage} (${diagnosticId}) + return `[${text}] ${highlightedMessage} (${diagnosticId})${suffixText} ${fileHeader} diff --git a/javascript/packages/highlighter/src/highlighter.ts b/javascript/packages/highlighter/src/highlighter.ts index aedd0c21d..26bf9ec7b 100644 --- a/javascript/packages/highlighter/src/highlighter.ts +++ b/javascript/packages/highlighter/src/highlighter.ts @@ -21,6 +21,7 @@ export interface HighlightOptions { maxWidth?: number truncateLines?: boolean codeUrlBuilder?: (code: string) => string + suffixBuilder?: (diagnostic: Diagnostic) => string | undefined } export interface HighlightDiagnosticOptions { @@ -31,6 +32,7 @@ export interface HighlightDiagnosticOptions { maxWidth?: number truncateLines?: boolean codeUrl?: string + suffix?: string } export class Highlighter { @@ -100,6 +102,7 @@ export class Highlighter { maxWidth = LineWrapper.getTerminalWidth(), truncateLines = false, codeUrlBuilder, + suffixBuilder, } = options // Case 1: Split diagnostics - render each diagnostic individually @@ -108,6 +111,7 @@ export class Highlighter { for (let i = 0; i < diagnostics.length; i++) { const diagnostic = diagnostics[i] const codeUrl = codeUrlBuilder && diagnostic.code ? codeUrlBuilder(diagnostic.code) : undefined + const suffix = suffixBuilder ? suffixBuilder(diagnostic) : undefined const result = this.highlightDiagnostic(path, diagnostic, content, { contextLines, showLineNumbers, @@ -115,6 +119,7 @@ export class Highlighter { maxWidth, truncateLines, codeUrl, + suffix, }) results.push(result) diff --git a/javascript/packages/highlighter/src/index.ts b/javascript/packages/highlighter/src/index.ts index c83c214df..faddfe9ed 100644 --- a/javascript/packages/highlighter/src/index.ts +++ b/javascript/packages/highlighter/src/index.ts @@ -4,4 +4,5 @@ export * from "./file-renderer.js" export * from "./highlighter.js" export * from "./line-wrapper.js" export * from "./syntax-renderer.js" +export * from "./text-formatter.js" export * from "./themes.js" diff --git a/javascript/packages/highlighter/src/inline-diagnostic-renderer.ts b/javascript/packages/highlighter/src/inline-diagnostic-renderer.ts index e34658c0e..868148f73 100644 --- a/javascript/packages/highlighter/src/inline-diagnostic-renderer.ts +++ b/javascript/packages/highlighter/src/inline-diagnostic-renderer.ts @@ -13,7 +13,7 @@ export class InlineDiagnosticRenderer { } private getSeverityText(severity: DiagnosticSeverity): string { - return colorize(severity, severityColor(severity)) + return colorize(colorize(severity, severityColor(severity)), "bold") } private getHighestSeverity(diagnostics: Diagnostic[]): DiagnosticSeverity { @@ -167,7 +167,7 @@ export class InlineDiagnosticRenderer { output += `${pointerPrefix}${pointerSpacing}${pointer}\n` const severityText = this.getSeverityText(diagnostic.severity) - const diagnosticIdText = colorize(diagnostic.code || "-", "gray") + const diagnosticIdText = diagnostic.code || "-" const diagnosticId = codeUrlBuilder && diagnostic.code ? hyperlink(diagnosticIdText, codeUrlBuilder(diagnostic.code)) : diagnosticIdText const highlightedMessage = TextFormatter.highlightBackticks(diagnostic.message) const diagnosticText = `[${severityText}] ${highlightedMessage} (${diagnosticId})` @@ -185,7 +185,7 @@ export class InlineDiagnosticRenderer { output += `${pointerSpacing}${pointer}\n` const severityText = this.getSeverityText(diagnostic.severity) - const diagnosticIdText = colorize(diagnostic.code || "-", "gray") + const diagnosticIdText = diagnostic.code || "-" const diagnosticId = codeUrlBuilder && diagnostic.code ? hyperlink(diagnosticIdText, codeUrlBuilder(diagnostic.code)) : diagnosticIdText const highlightedMessage = TextFormatter.highlightBackticks(diagnostic.message) const diagnosticText = `[${severityText}] ${highlightedMessage} (${diagnosticId})` diff --git a/javascript/packages/linter/src/cli/formatters/detailed-formatter.ts b/javascript/packages/linter/src/cli/formatters/detailed-formatter.ts index 8d01fe034..48d95efae 100644 --- a/javascript/packages/linter/src/cli/formatters/detailed-formatter.ts +++ b/javascript/packages/linter/src/cli/formatters/detailed-formatter.ts @@ -28,17 +28,14 @@ export class DetailedFormatter extends BaseFormatter { await this.highlighter.initialize() } + const correctableTag = colorize(colorize("[Correctable]", "green"), "bold") + const autocorrectableSet = new Set( + allOffenses.filter(item => item.autocorrectable).map(item => item.offense) + ) + if (isSingleFile) { const { filename, content } = allOffenses[0] - const diagnostics = allOffenses.map(item => { - if (item.autocorrectable && item.offense.code) { - return { - ...item.offense, - message: `${item.offense.message} ${colorize(colorize("[Correctable]", "green"), "bold")}` - } - } - return item.offense - }) + const diagnostics = allOffenses.map(item => item.offense) const highlighted = this.highlighter.highlight(filename, content, { diagnostics: diagnostics, @@ -47,6 +44,7 @@ export class DetailedFormatter extends BaseFormatter { wrapLines: this.wrapLines, truncateLines: this.truncateLines, codeUrlBuilder: ruleDocumentationUrl, + suffixBuilder: (diagnostic) => autocorrectableSet.has(diagnostic) ? correctableTag : undefined, }) console.log(`\n${highlighted}`) @@ -56,21 +54,14 @@ export class DetailedFormatter extends BaseFormatter { for (let i = 0; i < allOffenses.length; i++) { const { filename, offense, content, autocorrectable } = allOffenses[i] - let modifiedOffense = offense - - if (autocorrectable && offense.code) { - modifiedOffense = { - ...offense, - message: `${offense.message} ${colorize(colorize("[Correctable]", "green"), "bold")}` - } - } - - const codeUrl = modifiedOffense.code ? ruleDocumentationUrl(modifiedOffense.code) : undefined - const formatted = this.highlighter.highlightDiagnostic(filename, modifiedOffense, content, { + const codeUrl = offense.code ? ruleDocumentationUrl(offense.code) : undefined + const suffix = autocorrectable ? correctableTag : undefined + const formatted = this.highlighter.highlightDiagnostic(filename, offense, content, { contextLines: 2, wrapLines: this.wrapLines, truncateLines: this.truncateLines, codeUrl, + suffix, }) console.log(`\n${formatted}`) diff --git a/javascript/packages/linter/src/cli/formatters/simple-formatter.ts b/javascript/packages/linter/src/cli/formatters/simple-formatter.ts index 776c9309e..644dfa375 100644 --- a/javascript/packages/linter/src/cli/formatters/simple-formatter.ts +++ b/javascript/packages/linter/src/cli/formatters/simple-formatter.ts @@ -1,4 +1,4 @@ -import { colorize, hyperlink } from "@herb-tools/highlighter" +import { colorize, hyperlink, TextFormatter } from "@herb-tools/highlighter" import { BaseFormatter } from "./base-formatter.js" import { ruleDocumentationUrl } from "../../urls.js" @@ -30,14 +30,14 @@ export class SimpleFormatter extends BaseFormatter { for (const offense of offenses) { const isError = offense.severity === "error" const severity = isError ? colorize("✗", "brightRed") : colorize("⚠", "brightYellow") - const ruleText = colorize(`(${offense.code})`, "blue") + const ruleText = `(${offense.code})` const rule = offense.code ? hyperlink(ruleText, ruleDocumentationUrl(offense.code)) : ruleText const locationString = `${offense.location.start.line}:${offense.location.start.column}` const paddedLocation = locationString.padEnd(4) - console.log(` ${colorize(paddedLocation, "gray")} ${severity} ${offense.message} ${rule}`) + const message = TextFormatter.highlightBackticks(offense.message) + console.log(` ${colorize(paddedLocation, "gray")} ${severity} ${message} ${rule}`) } - console.log("") } formatFileProcessed(filename: string, processedFiles: ProcessedFile[]): void { @@ -46,14 +46,14 @@ export class SimpleFormatter extends BaseFormatter { for (const { offense, autocorrectable } of processedFiles) { const isError = offense.severity === "error" const severity = isError ? colorize("✗", "brightRed") : colorize("⚠", "brightYellow") - const ruleText = colorize(`(${offense.code})`, "blue") + const ruleText = `(${offense.code})` const rule = offense.code ? hyperlink(ruleText, ruleDocumentationUrl(offense.code)) : ruleText const locationString = `${offense.location.start.line}:${offense.location.start.column}` const paddedLocation = locationString.padEnd(4) const correctable = autocorrectable ? colorize(colorize(" [Correctable]", "green"), "bold") : "" - console.log(` ${colorize(paddedLocation, "gray")} ${severity} ${offense.message} ${rule}${correctable}`) + const message = TextFormatter.highlightBackticks(offense.message) + console.log(` ${colorize(paddedLocation, "gray")} ${severity} ${message} ${rule}${correctable}`) } - console.log("") } } diff --git a/javascript/packages/linter/src/cli/summary-reporter.ts b/javascript/packages/linter/src/cli/summary-reporter.ts index 98ebabd05..fa3edfb29 100644 --- a/javascript/packages/linter/src/cli/summary-reporter.ts +++ b/javascript/packages/linter/src/cli/summary-reporter.ts @@ -41,20 +41,14 @@ export class SummaryReporter { const filesClean = filesChecked - filesWithOffenses let filesSummary = "" - let shouldDim = false if (filesWithOffenses > 0) { - filesSummary = `${colorize(colorize(`${filesWithOffenses} with offenses`, "brightRed"), "bold")} | ${colorize(colorize(`${filesClean} clean`, "green"), "bold")} ${colorize(colorize(`(${filesChecked} total)`, "gray"), "dim")}` + filesSummary = `${colorize(colorize(`${filesWithOffenses} with offenses`, "brightRed"), "bold")} | ${colorize(colorize(`${filesClean} clean`, "green"), "bold")} ${colorize(`(${filesChecked} total)`, "gray")}` } else { - filesSummary = `${colorize(colorize(`${filesChecked} clean`, "green"), "bold")} ${colorize(colorize(`(${filesChecked} total)`, "gray"), "dim")}` - shouldDim = true + filesSummary = `${colorize(colorize(`${filesChecked} clean`, "green"), "bold")} ${colorize(`(${filesChecked} total)`, "gray")}` } - if (shouldDim) { - console.log(colorize(` ${colorize(pad("Files"), "gray")} ${filesSummary}`, "dim")) - } else { - console.log(` ${colorize(pad("Files"), "gray")} ${filesSummary}`) - } + console.log(` ${colorize(pad("Files"), "gray")} ${filesSummary}`) } let offensesSummary = "" @@ -96,7 +90,7 @@ export class SummaryReporter { } if (detailText) { - offensesSummary += ` ${colorize(colorize(`(${detailText})`, "gray"), "dim")}` + offensesSummary += ` ${colorize(`(${detailText})`, "gray")}` } } @@ -124,7 +118,7 @@ export class SummaryReporter { const timeString = startDate.toTimeString().split(' ')[0] console.log(` ${colorize(pad("Start at"), "gray")} ${colorize(timeString, "cyan")}`) - console.log(` ${colorize(pad("Duration"), "gray")} ${colorize(`${duration}ms`, "cyan")} ${colorize(colorize(`(${ruleCount} ${this.pluralize(ruleCount, "rule")})`, "gray"), "dim")}`) + console.log(` ${colorize(pad("Duration"), "gray")} ${colorize(`${duration}ms`, "cyan")} ${colorize(`(${ruleCount} ${this.pluralize(ruleCount, "rule")})`, "gray")}`) } if (filesWithOffenses === 0 && files.length > 1) { @@ -141,20 +135,21 @@ export class SummaryReporter { const remainingRules = allRules.slice(limit) const title = ruleOffenses.size <= limit ? "Rule offenses:" : "Most frequent rule offenses:" + console.log("\n") console.log(` ${colorize(title, "bold")}`) for (const [rule, data] of displayedRules) { const fileCount = data.files.size const countText = `(${data.count} ${this.pluralize(data.count, "offense")} in ${fileCount} ${this.pluralize(fileCount, "file")})` - const ruleText = colorize(rule, "gray") + const ruleText = colorize(rule, "white") const ruleLink = hyperlink(ruleText, ruleDocumentationUrl(rule)) - console.log(` ${ruleLink} ${colorize(colorize(countText, "gray"), "dim")}`) + console.log(` ${ruleLink} ${colorize(countText, "gray")}`) } if (remainingRules.length > 0) { const remainingOffenseCount = remainingRules.reduce((sum, [_, data]) => sum + data.count, 0) const remainingRuleCount = remainingRules.length - console.log(colorize(colorize(`\n ...and ${remainingRuleCount} more ${this.pluralize(remainingRuleCount, "rule")} with ${remainingOffenseCount} ${this.pluralize(remainingOffenseCount, "offense")}`, "gray"), "dim")) + console.log(colorize(`\n ...and ${remainingRuleCount} more ${this.pluralize(remainingRuleCount, "rule")} with ${remainingOffenseCount} ${this.pluralize(remainingOffenseCount, "offense")}`, "gray")) } } } diff --git a/javascript/packages/linter/test/__snapshots__/cli.test.ts.snap b/javascript/packages/linter/test/__snapshots__/cli.test.ts.snap index 310a473b7..30fdf699a 100644 --- a/javascript/packages/linter/test/__snapshots__/cli.test.ts.snap +++ b/javascript/packages/linter/test/__snapshots__/cli.test.ts.snap @@ -15,7 +15,7 @@ test/fixtures/ignored.html.erb:8:2 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ [1/7] ⎯⎯⎯⎯ -[error] Opening tag name \`
4 │
+
+
Rule offenses:
html-tag-name-lowercase (2 offenses in 1 file)
html-img-require-alt (1 offense in 1 file)
@@ -167,13 +171,15 @@ test/fixtures/test-file-with-errors.html.erb:2:22
exports[`CLI Output Formatting > GitHub Actions format includes rule codes 1`] = `
"::error file=test/fixtures/no-trailing-newline.html.erb,line=1,col=29,title=erb-require-trailing-newline • @herb-tools/linter@0.8.10::File must end with trailing newline. [erb-require-trailing-newline]%0A%0A%0Atest/fixtures/no-trailing-newline.html.erb:1:29%0A%0A → 1 │
4 │
+
+
Rule offenses:
html-tag-name-lowercase (2 offenses in 1 file)
html-img-require-alt (1 offense in 1 file)
@@ -688,7 +708,7 @@ test/fixtures/test-file-with-errors.html.erb:2:22
`;
exports[`CLI Output Formatting > formats GitHub Actions output with --format=github option 1`] = `
-"[error] Opening tag name \`\` should be lowercase. Use \`\` instead. [Correctable] (html-tag-name-lowercase)
+"[error] Opening tag name \`\` should be lowercase. Use \`\` instead. (html-tag-name-lowercase) [Correctable]
test/fixtures/test-file-simple.html.erb:2:3
@@ -701,7 +721,7 @@ test/fixtures/test-file-simple.html.erb:2:3
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ [1/2] ⎯⎯⎯⎯
-[error] Closing tag name \`\` should be lowercase. Use \`\` instead. [Correctable] (html-tag-name-lowercase)
+[error] Closing tag name \`\` should be lowercase. Use \`\` instead. (html-tag-name-lowercase) [Correctable]
test/fixtures/test-file-simple.html.erb:2:22
@@ -711,6 +731,8 @@ test/fixtures/test-file-simple.html.erb:2:22
3 │
4 │
+
+
Rule offenses:
html-tag-name-lowercase (2 offenses in 1 file)
@@ -886,7 +908,7 @@ test/fixtures/test-file-with-errors.html.erb:3:3
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ [1/3] ⎯⎯⎯⎯
-[error] Opening tag name \`\` should be lowercase. Use \`\` instead. [Correctable] (html-tag-name-lowercase)
+[error] Opening tag name \`\` should be lowercase. Use \`\` instead. (html-tag-name-lowercase) [Correctable]
test/fixtures/test-file-with-errors.html.erb:2:3
@@ -899,7 +921,7 @@ test/fixtures/test-file-with-errors.html.erb:2:3
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ [2/3] ⎯⎯⎯⎯
-[error] Closing tag name \`\` should be lowercase. Use \`\` instead. [Correctable] (html-tag-name-lowercase)
+[error] Closing tag name \`\` should be lowercase. Use \`\` instead. (html-tag-name-lowercase) [Correctable]
test/fixtures/test-file-with-errors.html.erb:2:22
@@ -909,6 +931,8 @@ test/fixtures/test-file-with-errors.html.erb:2:22
3 │
4 │
+
+
Rule offenses:
html-tag-name-lowercase (2 offenses in 1 file)
html-img-require-alt (1 offense in 1 file)
@@ -925,6 +949,7 @@ exports[`CLI Output Formatting > formats simple output correctly 1`] = `
2:3 ✗ Opening tag name \`\` should be lowercase. Use \`\` instead. (html-tag-name-lowercase) [Correctable]
2:22 ✗ Closing tag name \`\` should be lowercase. Use \`\` instead. (html-tag-name-lowercase) [Correctable]
+
Rule offenses:
html-tag-name-lowercase (2 offenses in 1 file)
@@ -940,6 +965,7 @@ exports[`CLI Output Formatting > formats simple output for bad-file correctly 1`
1:1 ✗ Opening tag name \`\` should be lowercase. Use \`\` instead. (html-tag-name-lowercase) [Correctable]
1:16 ✗ Closing tag name \`\` should be lowercase. Use \`\` instead. (html-tag-name-lowercase) [Correctable]
+
Rule offenses:
html-tag-name-lowercase (2 offenses in 1 file)
@@ -969,7 +995,7 @@ exports[`CLI Output Formatting > handles boolean attributes 1`] = `
`;
exports[`CLI Output Formatting > handles multiple errors correctly 1`] = `
-"[error] Opening tag name \`\` should be lowercase. Use \`\` instead. [Correctable] (html-tag-name-lowercase)
+"[error] Opening tag name \`\` should be lowercase. Use \`\` instead. (html-tag-name-lowercase) [Correctable]
test/fixtures/bad-file.html.erb:1:1
@@ -980,7 +1006,7 @@ test/fixtures/bad-file.html.erb:1:1
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ [1/2] ⎯⎯⎯⎯
-[error] Closing tag name \`\` should be lowercase. Use \`\` instead. [Correctable] (html-tag-name-lowercase)
+[error] Closing tag name \`\` should be lowercase. Use \`\` instead. (html-tag-name-lowercase) [Correctable]
test/fixtures/bad-file.html.erb:1:16
@@ -988,6 +1014,8 @@ test/fixtures/bad-file.html.erb:1:16
│ ~~~~
2 │
+
+
Rule offenses:
html-tag-name-lowercase (2 offenses in 1 file)
@@ -999,7 +1027,7 @@ test/fixtures/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 (\`<%# ... %>\`). [Correctable] (erb-comment-syntax)
+"[error] Use \`<%#\` instead of \`<% #\` for \`herb:disable\` directives. Herb directives only work with ERB comment syntax (\`<%# ... %>\`). (erb-comment-syntax) [Correctable]
test/fixtures/disabled-1.html.erb:12:19
@@ -1111,6 +1139,8 @@ test/fixtures/disabled-1.html.erb:14:19
15 │
16 │
+
+
Rule offenses:
herb-disable-comment-no-redundant-all (3 offenses in 1 file)
herb-disable-comment-no-duplicate-rules (2 offenses in 1 file)
@@ -1214,7 +1244,7 @@ test/fixtures/disabled-2.html.erb:6:30
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ [6/13] ⎯⎯⎯⎯
-[error] Opening tag name \`
4 │
+
+
Rule offenses:
html-tag-name-lowercase (2 offenses in 1 file)
html-img-require-alt (1 offense in 1 file)