Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aos
GitHub Repository: aos/grafana-agent
Path: blob/main/pkg/river/diag/printer.go
4096 views
1
package diag
2
3
import (
4
"bufio"
5
"fmt"
6
"io"
7
"strconv"
8
"strings"
9
10
"github.com/fatih/color"
11
"github.com/grafana/agent/pkg/river/token"
12
)
13
14
const tabWidth = 4
15
16
// PrinterConfig controls different settings for the Printer.
17
type PrinterConfig struct {
18
// When Color is true, the printer will output with color and special
19
// formatting characters (such as underlines).
20
//
21
// This should be disabled when not printing to a terminal.
22
Color bool
23
24
// ContextLinesBefore and ContextLinesAfter controls how many context lines
25
// before and after the range of the diagnostic are printed.
26
ContextLinesBefore, ContextLinesAfter int
27
}
28
29
// A Printer pretty-prints Diagnostics.
30
type Printer struct {
31
cfg PrinterConfig
32
}
33
34
// NewPrinter creates a new diagnostics Printer with the provided config.
35
func NewPrinter(cfg PrinterConfig) *Printer {
36
return &Printer{cfg: cfg}
37
}
38
39
// Fprint creates a Printer with default settings and prints diagnostics to the
40
// provided writer. files is used to look up file contents by name for printing
41
// diagnostics context. files may be set to nil to avoid printing context.
42
func Fprint(w io.Writer, files map[string][]byte, diags Diagnostics) error {
43
p := NewPrinter(PrinterConfig{
44
Color: false,
45
ContextLinesBefore: 1,
46
ContextLinesAfter: 1,
47
})
48
return p.Fprint(w, files, diags)
49
}
50
51
// Fprint pretty-prints errors to a writer. files is used to look up file
52
// contents by name when printing context. files may be nil to avoid printing
53
// context.
54
func (p *Printer) Fprint(w io.Writer, files map[string][]byte, diags Diagnostics) error {
55
// Create a buffered writer since we'll have many small calls to Write while
56
// we print errors.
57
//
58
// Buffers writers track the first write error received and will return it
59
// (if any) when flushing, so we can ignore write errors throughout the code
60
// until the very end.
61
bw := bufio.NewWriter(w)
62
63
for i, diag := range diags {
64
p.printDiagnosticHeader(bw, diag)
65
66
// If there's no ending position, set the ending position to be the same as
67
// the start.
68
if !diag.EndPos.Valid() {
69
diag.EndPos = diag.StartPos
70
}
71
72
// We can print the file context if it was found.
73
fileContents, foundFile := files[diag.StartPos.Filename]
74
if foundFile && diag.StartPos.Filename == diag.EndPos.Filename {
75
p.printRange(bw, fileContents, diag)
76
}
77
78
// Print a blank line to separate diagnostics.
79
if i+1 < len(diags) {
80
fmt.Fprintf(bw, "\n")
81
}
82
}
83
84
return bw.Flush()
85
}
86
87
func (p *Printer) printDiagnosticHeader(w io.Writer, diag Diagnostic) {
88
if p.cfg.Color {
89
switch diag.Severity {
90
case SeverityLevelError:
91
cw := color.New(color.FgRed, color.Bold)
92
_, _ = cw.Fprintf(w, "Error: ")
93
case SeverityLevelWarn:
94
cw := color.New(color.FgYellow, color.Bold)
95
_, _ = cw.Fprintf(w, "Warning: ")
96
}
97
98
cw := color.New(color.Bold)
99
_, _ = cw.Fprintf(w, "%s: %s\n", diag.StartPos, diag.Message)
100
return
101
}
102
103
switch diag.Severity {
104
case SeverityLevelError:
105
_, _ = fmt.Fprintf(w, "Error: ")
106
case SeverityLevelWarn:
107
_, _ = fmt.Fprintf(w, "Warning: ")
108
}
109
fmt.Fprintf(w, "%s: %s\n", diag.StartPos, diag.Message)
110
}
111
112
func (p *Printer) printRange(w io.Writer, file []byte, diag Diagnostic) {
113
var (
114
start = diag.StartPos
115
end = diag.EndPos
116
)
117
118
fmt.Fprintf(w, "\n")
119
120
var (
121
lines = strings.Split(string(file), "\n")
122
123
startLine = max(start.Line-p.cfg.ContextLinesBefore, 1)
124
endLine = min(end.Line+p.cfg.ContextLinesAfter, len(lines))
125
126
multiline = end.Line-start.Line > 0
127
)
128
129
prefixWidth := len(strconv.Itoa(endLine))
130
131
for lineNum := startLine; lineNum <= endLine; lineNum++ {
132
line := lines[lineNum-1]
133
134
// Print line number and margin.
135
printPaddedNumber(w, prefixWidth, lineNum)
136
fmt.Fprintf(w, " | ")
137
138
if multiline {
139
// Use 0 for the column number so we never consider the starting line for
140
// showing |.
141
if inRange(lineNum, 0, start, end) {
142
fmt.Fprint(w, "| ")
143
} else {
144
fmt.Fprint(w, " ")
145
}
146
}
147
148
// Print the line, but filter out any \r and replace tabs with spaces.
149
for _, ch := range line {
150
if ch == '\r' {
151
continue
152
}
153
if ch == '\t' || ch == '\v' {
154
printCh(w, tabWidth, ' ')
155
continue
156
}
157
fmt.Fprintf(w, "%c", ch)
158
}
159
160
fmt.Fprintf(w, "\n")
161
162
// Print the focus indicator if we're on a line that needs it.
163
//
164
// The focus indicator line must preserve whitespace present in the line
165
// above it prior to the focus '^' characters. Tab characters are replaced
166
// with spaces for consistent printing.
167
if lineNum == start.Line || (multiline && lineNum == end.Line) {
168
printCh(w, prefixWidth, ' ') // Add empty space where line number would be
169
170
// Print the margin after the blank line number. On multi-line errors,
171
// the arrow is printed all the way to the margin, with straight
172
// lines going down in between the lines.
173
switch {
174
case multiline && lineNum == start.Line:
175
// |_ would look like an incorrect right angle, so the second bar
176
// is dropped.
177
fmt.Fprintf(w, " | _")
178
case multiline && lineNum == end.Line:
179
fmt.Fprintf(w, " | |_")
180
default:
181
fmt.Fprintf(w, " | ")
182
}
183
184
p.printFocus(w, line, lineNum, diag)
185
fmt.Fprintf(w, "\n")
186
}
187
}
188
}
189
190
// printFocus prints the focus indicator for the line number specified by line.
191
// The contents of the line should be represented by data so whitespace can be
192
// retained (injecting spaces where a tab should be, etc.).
193
func (p *Printer) printFocus(w io.Writer, data string, line int, diag Diagnostic) {
194
for i, ch := range data {
195
column := i + 1
196
197
if line == diag.EndPos.Line && column > diag.EndPos.Column {
198
// Stop printing the formatting line after printing all the ^.
199
break
200
}
201
202
blank := byte(' ')
203
if diag.EndPos.Line-diag.StartPos.Line > 0 {
204
blank = byte('_')
205
}
206
207
switch {
208
case ch == '\t' || ch == '\v':
209
printCh(w, tabWidth, blank)
210
case inRange(line, column, diag.StartPos, diag.EndPos):
211
fmt.Fprintf(w, "%c", '^')
212
default:
213
// Print a space.
214
fmt.Fprintf(w, "%c", blank)
215
}
216
}
217
}
218
219
func inRange(line, col int, start, end token.Position) bool {
220
if line < start.Line || line > end.Line {
221
return false
222
}
223
224
switch line {
225
case start.Line:
226
// If the current line is on the starting line, we have to be past the
227
// starting column.
228
return col >= start.Column
229
case end.Line:
230
// If the current line is on the ending line, we have to be before the
231
// final column.
232
return col <= end.Column
233
default:
234
// Otherwise, every column across all the lines in between
235
// is in the range.
236
return true
237
}
238
}
239
240
func printPaddedNumber(w io.Writer, width int, num int) {
241
numStr := strconv.Itoa(num)
242
for i := 0; i < width-len(numStr); i++ {
243
_, _ = w.Write([]byte{' '})
244
}
245
_, _ = w.Write([]byte(numStr))
246
}
247
248
func printCh(w io.Writer, count int, ch byte) {
249
for i := 0; i < count; i++ {
250
_, _ = w.Write([]byte{ch})
251
}
252
}
253
254
func min(a, b int) int {
255
if a < b {
256
return a
257
}
258
return b
259
}
260
261
func max(a, b int) int {
262
if a > b {
263
return a
264
}
265
return b
266
}
267
268