Skip to content

Commit bc72326

Browse files
authored
Linter: Link file paths in Linter CLI output (#1243)
<img width="3108" height="1484" alt="CleanShot 2026-02-24 at 22 05 43@2x" src="https://github.com/user-attachments/assets/7c126918-ac23-49c9-9757-88c88753182c" />
1 parent 20dfedd commit bc72326

File tree

6 files changed

+37
-16
lines changed

6 files changed

+37
-16
lines changed

javascript/packages/highlighter/src/diagnostic-renderer.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export interface DiagnosticRenderOptions {
1414
maxWidth?: number
1515
truncateLines?: boolean
1616
codeUrl?: string
17+
fileUrl?: string
1718
suffix?: string
1819
}
1920

@@ -142,9 +143,9 @@ export class DiagnosticRenderer {
142143

143144
const shouldWrap = wrapLines && !truncateLines
144145
const shouldTruncate = truncateLines
145-
const fileHeader = `${colorize(path, "cyan")}:${colorize(`${diagnostic.location.start.line}:${diagnostic.location.start.column}`, "cyan")}`
146-
147-
const { codeUrl } = options
146+
const fileHeaderText = `${colorize(path, "cyan")}:${colorize(`${diagnostic.location.start.line}:${diagnostic.location.start.column}`, "cyan")}`
147+
const { codeUrl, fileUrl: fileUrlOption } = options
148+
const fileHeader = fileUrlOption ? hyperlink(fileHeaderText, fileUrlOption) : fileHeaderText
148149

149150
const color = severityColor(diagnostic.severity)
150151
const text = colorize(colorize(diagnostic.severity, color), "bold")

javascript/packages/highlighter/src/highlighter.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export interface HighlightOptions {
2121
maxWidth?: number
2222
truncateLines?: boolean
2323
codeUrlBuilder?: (code: string) => string
24+
fileUrlBuilder?: (path: string, diagnostic: Diagnostic) => string
2425
suffixBuilder?: (diagnostic: Diagnostic) => string | undefined
2526
}
2627

@@ -32,6 +33,7 @@ export interface HighlightDiagnosticOptions {
3233
maxWidth?: number
3334
truncateLines?: boolean
3435
codeUrl?: string
36+
fileUrl?: string
3537
suffix?: string
3638
}
3739

@@ -102,6 +104,7 @@ export class Highlighter {
102104
maxWidth = LineWrapper.getTerminalWidth(),
103105
truncateLines = false,
104106
codeUrlBuilder,
107+
fileUrlBuilder,
105108
suffixBuilder,
106109
} = options
107110

@@ -111,6 +114,7 @@ export class Highlighter {
111114
for (let i = 0; i < diagnostics.length; i++) {
112115
const diagnostic = diagnostics[i]
113116
const codeUrl = codeUrlBuilder && diagnostic.code ? codeUrlBuilder(diagnostic.code) : undefined
117+
const fileUrl = fileUrlBuilder ? fileUrlBuilder(path, diagnostic) : undefined
114118
const suffix = suffixBuilder ? suffixBuilder(diagnostic) : undefined
115119
const result = this.highlightDiagnostic(path, diagnostic, content, {
116120
contextLines,
@@ -119,6 +123,7 @@ export class Highlighter {
119123
maxWidth,
120124
truncateLines,
121125
codeUrl,
126+
fileUrl,
122127
suffix,
123128
})
124129

javascript/packages/linter/src/cli/formatters/detailed-formatter.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { colorize, Highlighter, type ThemeInput, DEFAULT_THEME } from "@herb-too
22

33
import { BaseFormatter } from "./base-formatter.js"
44
import { LineWrapper } from "@herb-tools/highlighter"
5-
import { ruleDocumentationUrl } from "../../urls.js"
5+
import { ruleDocumentationUrl, fileUrl } from "../../urls.js"
66

77
import type { Diagnostic } from "@herb-tools/core"
88
import type { ProcessedFile } from "../file-processor.js"
@@ -44,6 +44,7 @@ export class DetailedFormatter extends BaseFormatter {
4444
wrapLines: this.wrapLines,
4545
truncateLines: this.truncateLines,
4646
codeUrlBuilder: ruleDocumentationUrl,
47+
fileUrlBuilder: (path) => fileUrl(path),
4748
suffixBuilder: (diagnostic) => autocorrectableSet.has(diagnostic) ? correctableTag : undefined,
4849
})
4950

@@ -61,6 +62,7 @@ export class DetailedFormatter extends BaseFormatter {
6162
wrapLines: this.wrapLines,
6263
truncateLines: this.truncateLines,
6364
codeUrl,
65+
fileUrl: fileUrl(filename),
6466
suffix,
6567
})
6668
console.log(`\n${formatted}`)

javascript/packages/linter/src/cli/formatters/simple-formatter.ts

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { colorize, hyperlink, TextFormatter } from "@herb-tools/highlighter"
22

33
import { BaseFormatter } from "./base-formatter.js"
4-
import { ruleDocumentationUrl } from "../../urls.js"
4+
import { ruleDocumentationUrl, fileUrl } from "../../urls.js"
55

66
import type { Diagnostic } from "@herb-tools/core"
77
import type { ProcessedFile } from "../file-processor.js"
@@ -25,35 +25,41 @@ export class SimpleFormatter extends BaseFormatter {
2525
}
2626

2727
formatFile(filename: string, offenses: Diagnostic[]): void {
28-
console.log(`${colorize(filename, "cyan")}:`)
28+
const filenameText = colorize(filename, "cyan")
29+
const filenameLink = hyperlink(filenameText, fileUrl(filename))
30+
console.log(`${filenameLink}:`)
2931

3032
for (const offense of offenses) {
3133
const isError = offense.severity === "error"
3234
const severity = isError ? colorize("✗", "brightRed") : colorize("⚠", "brightYellow")
3335
const ruleText = `(${offense.code})`
3436
const rule = offense.code ? hyperlink(ruleText, ruleDocumentationUrl(offense.code)) : ruleText
35-
const locationString = `${offense.location.start.line}:${offense.location.start.column}`
36-
const paddedLocation = locationString.padEnd(4)
37-
37+
const { line, column } = offense.location.start
38+
const locationString = `${line}:${column}`
39+
const paddedLocation = colorize(locationString.padEnd(4), "gray")
3840
const message = TextFormatter.highlightBackticks(offense.message)
39-
console.log(` ${colorize(paddedLocation, "gray")} ${severity} ${message} ${rule}`)
41+
42+
console.log(` ${paddedLocation} ${severity} ${message} ${rule}`)
4043
}
4144
}
4245

4346
formatFileProcessed(filename: string, processedFiles: ProcessedFile[]): void {
44-
console.log(`${colorize(filename, "cyan")}:`)
47+
const filenameText = colorize(filename, "cyan")
48+
const filenameLink = hyperlink(filenameText, fileUrl(filename))
49+
console.log(`${filenameLink}:`)
4550

4651
for (const { offense, autocorrectable } of processedFiles) {
4752
const isError = offense.severity === "error"
4853
const severity = isError ? colorize("✗", "brightRed") : colorize("⚠", "brightYellow")
4954
const ruleText = `(${offense.code})`
5055
const rule = offense.code ? hyperlink(ruleText, ruleDocumentationUrl(offense.code)) : ruleText
51-
const locationString = `${offense.location.start.line}:${offense.location.start.column}`
52-
const paddedLocation = locationString.padEnd(4)
56+
const { line, column } = offense.location.start
57+
const locationString = `${line}:${column}`
58+
const paddedLocation = colorize(locationString.padEnd(4), "gray")
5359
const correctable = autocorrectable ? colorize(colorize(" [Correctable]", "green"), "bold") : ""
54-
5560
const message = TextFormatter.highlightBackticks(offense.message)
56-
console.log(` ${colorize(paddedLocation, "gray")} ${severity} ${message} ${rule}${correctable}`)
61+
62+
console.log(` ${paddedLocation} ${severity} ${message} ${rule}${correctable}`)
5763
}
5864
}
5965
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export * from "./linter.js"
22
export * from "./rules/index.js"
33
export * from "./types.js"
4-
export * from "./urls.js"
54

5+
export { ruleDocumentationUrl } from "./urls.js"
66
export { rules } from "./rules.js"
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
1+
import { resolve } from "node:path"
2+
13
const DOCS_BASE_URL = "https://herb-tools.dev/linter/rules"
24

35
export function ruleDocumentationUrl(ruleId: string): string {
46
return `${DOCS_BASE_URL}/${ruleId}`
57
}
8+
9+
export function fileUrl(filePath: string): string {
10+
const absolutePath = resolve(filePath)
11+
return `file://${absolutePath}`
12+
}

0 commit comments

Comments
 (0)