Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aos
GitHub Repository: aos/grafana-agent
Path: blob/main/pkg/river/printer/printer.go
4096 views
1
// Package printer contains utilities for pretty-printing River ASTs.
2
package printer
3
4
import (
5
"fmt"
6
"io"
7
"math"
8
"text/tabwriter"
9
10
"github.com/grafana/agent/pkg/river/ast"
11
"github.com/grafana/agent/pkg/river/token"
12
)
13
14
// Config configures behavior of the printer.
15
type Config struct {
16
Indent int // Indentation to apply to all emitted code. Default 0.
17
}
18
19
// Fprint pretty-prints the specified node to w. The Node type must be an
20
// *ast.File, ast.Body, or a type that implements ast.Stmt or ast.Expr.
21
func (c *Config) Fprint(w io.Writer, node ast.Node) (err error) {
22
var p printer
23
p.Init(c)
24
25
// Pass all of our text through a trimmer to ignore trailing whitespace.
26
w = &trimmer{next: w}
27
28
if err = (&walker{p: &p}).Walk(node); err != nil {
29
return
30
}
31
32
// Call flush one more time to write trailing comments.
33
p.flush(token.Position{
34
Offset: math.MaxInt,
35
Line: math.MaxInt,
36
Column: math.MaxInt,
37
}, token.EOF)
38
39
w = tabwriter.NewWriter(w, 0, 8, 1, ' ', tabwriter.DiscardEmptyColumns|tabwriter.TabIndent)
40
41
if _, err = w.Write(p.output); err != nil {
42
return
43
}
44
if tw, _ := w.(*tabwriter.Writer); tw != nil {
45
// Flush tabwriter if defined
46
err = tw.Flush()
47
}
48
49
return
50
}
51
52
// Fprint pretty-prints the specified node to w. The Node type must be an
53
// *ast.File, ast.Body, or a type that implements ast.Stmt or ast.Expr.
54
func Fprint(w io.Writer, node ast.Node) error {
55
c := &Config{}
56
return c.Fprint(w, node)
57
}
58
59
// The printer writes lexical tokens and whitespace to an internal buffer.
60
// Comments are written by the printer itself, while all other tokens and
61
// formatting characters are sent through calls to Write.
62
//
63
// Internally, printer depends on a tabwriter for formatting text and aligning
64
// runs of characters. Horizontal '\t' and vertical '\v' tab characters are
65
// used to introduce new columns in the row. Runs of characters are stopped
66
// be either introducing a linefeed '\f' or by having a line with a different
67
// number of columns from the previous line. See the text/tabwriter package for
68
// more information on the elastic tabstop algorithm it uses for formatting
69
// text.
70
type printer struct {
71
cfg Config
72
73
// State variables
74
75
output []byte
76
indent int // Current indentation level
77
lastTok token.Token // Last token printed (token.LITERAL if it's whitespace)
78
79
// Whitespace holds a buffer of whitespace characters to print prior to the
80
// next non-whitespace token. Whitespace is held in a buffer to avoid
81
// printing unnecessary whitespace at the end of a file.
82
whitespace []whitespace
83
84
// comments stores comments to be processed as elements are printed.
85
comments commentInfo
86
87
// pos is an approximation of the current position in AST space, and is used
88
// to determine space between AST elements (e.g., if a comment should come
89
// before a token). pos automatically as elements are written and can be manually
90
// set to guarantee an accurate position by passing a token.Pos to Write.
91
pos token.Position
92
last token.Position // Last pos written to output (through writeString)
93
94
// out is an accurate representation of the current position in output space,
95
// used to inject extra formatting like indentation based on the output
96
// position.
97
//
98
// out may differ from pos in terms of whitespace.
99
out token.Position
100
}
101
102
type commentInfo struct {
103
list []ast.CommentGroup
104
idx int
105
cur ast.CommentGroup
106
pos token.Pos
107
}
108
109
func (ci *commentInfo) commentBefore(next token.Position) bool {
110
return ci.pos != token.NoPos && ci.pos.Offset() <= next.Offset
111
}
112
113
// nextComment preloads the next comment.
114
func (ci *commentInfo) nextComment() {
115
for ci.idx < len(ci.list) {
116
c := ci.list[ci.idx]
117
ci.idx++
118
if len(c) > 0 {
119
ci.cur = c
120
ci.pos = ast.StartPos(c[0])
121
return
122
}
123
}
124
ci.pos = token.NoPos
125
}
126
127
// Init initializes the printer for printing. Init is intended to be called
128
// once per printer and doesn't fully reset its state.
129
func (p *printer) Init(cfg *Config) {
130
p.cfg = *cfg
131
p.pos = token.Position{Line: 1, Column: 1}
132
p.out = token.Position{Line: 1, Column: 1}
133
// Capacity is set low since most whitespace sequences are short.
134
p.whitespace = make([]whitespace, 0, 16)
135
}
136
137
// SetComments set the comments to use.
138
func (p *printer) SetComments(comments []ast.CommentGroup) {
139
p.comments = commentInfo{
140
list: comments,
141
idx: 0,
142
pos: token.NoPos,
143
}
144
p.comments.nextComment()
145
}
146
147
// Write writes a list of writable arguments to the printer.
148
//
149
// Arguments can be one of the types described below:
150
//
151
// If arg is a whitespace value, it is accumulated into a buffer and flushed
152
// only after a non-whitespace value is processed. The whitespace buffer will
153
// be forcibly flushed if the buffer becomes full without writing a
154
// non-whitespace token.
155
//
156
// If arg is an *ast.IdentifierExpr, *ast.LiteralExpr, or a token.Token, the
157
// human-readable representation of that value will be written.
158
//
159
// When writing text, comments which need to appear before that text in
160
// AST-space are written first, followed by leftover whitespace and then the
161
// text to write. The written text will update the AST-space position.
162
//
163
// If arg is a token.Pos, the AST-space position of the printer is updated to
164
// the provided Pos. Writing token.Pos values can help make sure the printer's
165
// AST-space position is accurate, as AST-space position is otherwise an
166
// estimation based on written data.
167
func (p *printer) Write(args ...interface{}) {
168
for _, arg := range args {
169
var (
170
data string
171
isLit bool
172
)
173
174
switch arg := arg.(type) {
175
case whitespace:
176
// Whitespace token; add it to our token buffer. Note that a whitespace
177
// token is different than the actual whitespace which will get written
178
// (e.g., wsIndent increases indentation level by one instead of setting
179
// it to one.)
180
if arg == wsIgnore {
181
continue
182
}
183
i := len(p.whitespace)
184
if i == cap(p.whitespace) {
185
// We built up too much whitespace; this can happen if too many calls
186
// to Write happen without appending a non-comment token. We will
187
// force-flush the existing whitespace to avoid a panic.
188
//
189
// Ideally this line is never hit based on how we walk the AST, but
190
// it's kept for safety.
191
p.writeWritespace(i)
192
i = 0
193
}
194
p.whitespace = p.whitespace[0 : i+1]
195
p.whitespace[i] = arg
196
p.lastTok = token.LITERAL
197
continue
198
199
case *ast.Ident:
200
data = arg.Name
201
p.lastTok = token.IDENT
202
203
case *ast.LiteralExpr:
204
data = arg.Value
205
p.lastTok = arg.Kind
206
207
case token.Pos:
208
if arg.Valid() {
209
p.pos = arg.Position()
210
}
211
// Don't write anything; token.Pos is an instruction and doesn't include
212
// any text to write.
213
continue
214
215
case token.Token:
216
s := arg.String()
217
data = s
218
219
// We will need to inject whitespace if the previous token and the
220
// current token would combine into a single token when re-scanned. This
221
// ensures that the sequence of tokens emitted by the output of the
222
// printer match the sequence of tokens from the input.
223
if mayCombine(p.lastTok, s[0]) {
224
if len(p.whitespace) != 0 {
225
// It shouldn't be possible for the whitespace buffer to be not empty
226
// here; p.lastTok would've had to been a non-whitespace token and so
227
// whitespace would've been flushed when it was written to the output
228
// buffer.
229
panic("whitespace buffer not empty")
230
}
231
p.whitespace = p.whitespace[0:1]
232
p.whitespace[0] = ' '
233
}
234
p.lastTok = arg
235
236
default:
237
panic(fmt.Sprintf("printer: unsupported argument %v (%T)\n", arg, arg))
238
}
239
240
next := p.pos
241
242
p.flush(next, p.lastTok)
243
p.writeString(next, data, isLit)
244
}
245
}
246
247
// mayCombine returns true if two tokes must not be combined, because combining
248
// them would format in a different token sequence being generated.
249
func mayCombine(prev token.Token, next byte) (b bool) {
250
switch prev {
251
case token.NUMBER:
252
return next == '.' // 1.
253
case token.DIV:
254
return next == '*' // /*
255
default:
256
return false
257
}
258
}
259
260
// flush prints any pending comments and whitespace occurring textually before
261
// the position of the next token tok. The flush result indicates if a newline
262
// was written or if a formfeed \f character was dropped from the whitespace
263
// buffer.
264
func (p *printer) flush(next token.Position, tok token.Token) {
265
if p.comments.commentBefore(next) {
266
p.injectComments(next, tok)
267
} else if tok != token.EOF {
268
// Write all remaining whitespace.
269
p.writeWritespace(len(p.whitespace))
270
}
271
}
272
273
func (p *printer) injectComments(next token.Position, tok token.Token) {
274
var lastComment *ast.Comment
275
276
for p.comments.commentBefore(next) {
277
for _, c := range p.comments.cur {
278
p.writeCommentPrefix(next, c)
279
p.writeComment(next, c)
280
lastComment = c
281
}
282
p.comments.nextComment()
283
}
284
285
p.writeCommentSuffix(next, tok, lastComment)
286
}
287
288
// writeCommentPrefix writes whitespace that should appear before c.
289
func (p *printer) writeCommentPrefix(next token.Position, c *ast.Comment) {
290
if len(p.output) == 0 {
291
// The comment is the first thing written to the output. Don't write any
292
// whitespace before it.
293
return
294
}
295
296
cPos := c.StartPos.Position()
297
298
if cPos.Line == p.last.Line {
299
// Our comment is on the same line as the last token. Write a separator
300
// between the last token and the comment.
301
separator := byte('\t')
302
if cPos.Line == next.Line {
303
// The comment is on the same line as the next token, which means it has
304
// to be a block comment (since line comments run to the end of the
305
// line.) Use a space as the separator instead since a tab in the middle
306
// of a line between comments would look weird.
307
separator = byte(' ')
308
}
309
p.writeByte(separator, 1)
310
} else {
311
// Our comment is on a different line from the last token. First write
312
// pending whitespace from the last token up to the first newline.
313
var wsCount int
314
315
for i, ws := range p.whitespace {
316
switch ws {
317
case wsBlank, wsVTab:
318
// Drop any whitespace before the comment.
319
p.whitespace[i] = wsIgnore
320
case wsIndent, wsUnindent:
321
// Allow indentation to be applied.
322
continue
323
case wsNewline, wsFormfeed:
324
// Drop the whitespace since we're about to write our own.
325
p.whitespace[i] = wsIgnore
326
}
327
wsCount = i
328
break
329
}
330
p.writeWritespace(wsCount)
331
332
var newlines int
333
if cPos.Valid() && p.last.Valid() {
334
newlines = cPos.Line - p.last.Line
335
}
336
if newlines > 0 {
337
p.writeByte('\f', newlineLimit(newlines))
338
}
339
}
340
}
341
342
func (p *printer) writeComment(_ token.Position, c *ast.Comment) {
343
p.writeString(c.StartPos.Position(), c.Text, true)
344
}
345
346
// writeCommentSuffix writes any whitespace necessary between the last comment
347
// and next. lastComment should be the final comment written.
348
func (p *printer) writeCommentSuffix(next token.Position, tok token.Token, lastComment *ast.Comment) {
349
if tok == token.EOF {
350
// We don't want to add any blank newlines before the end of the file;
351
// return early.
352
return
353
}
354
355
var droppedFF bool
356
357
// If our final comment is a block comment and is on the same line as the
358
// next token, add a space as a suffix to separate them.
359
lastCommentPos := ast.EndPos(lastComment).Position()
360
if lastComment.Text[1] == '*' && next.Line == lastCommentPos.Line {
361
p.writeByte(' ', 1)
362
}
363
364
newlines := next.Line - p.last.Line
365
366
for i, ws := range p.whitespace {
367
switch ws {
368
case wsBlank, wsVTab:
369
p.whitespace[i] = wsIgnore
370
case wsIndent, wsUnindent:
371
continue
372
case wsNewline, wsFormfeed:
373
if ws == wsFormfeed {
374
droppedFF = true
375
}
376
p.whitespace[i] = wsIgnore
377
}
378
}
379
380
p.writeWritespace(len(p.whitespace))
381
382
// Write newlines as long as the next token isn't EOF (so that there's no
383
// blank newlines at the end of the file).
384
if newlines > 0 {
385
ch := byte('\n')
386
if droppedFF {
387
// If we dropped a formfeed while writing comments, we should emit a new
388
// one.
389
ch = byte('\f')
390
}
391
p.writeByte(ch, newlineLimit(newlines))
392
}
393
}
394
395
// writeString writes the literal string s into the printer's output.
396
// Formatting characters in s such as '\t' and '\n' will be interpreted by
397
// underlying tabwriter unless isLit is set.
398
func (p *printer) writeString(pos token.Position, s string, isLit bool) {
399
if p.out.Column == 1 {
400
// We haven't written any text to this line yet; prepend our indentation
401
// for the line.
402
p.writeIndent()
403
}
404
405
if pos.Valid() {
406
// Update p.pos if pos is valid. This is done *after* handling indentation
407
// since we want to interpret pos as the literal position for s (and
408
// writeIndent will update p.pos).
409
p.pos = pos
410
}
411
412
if isLit {
413
// Wrap our literal string in tabwriter.Escape if it's meant to be written
414
// without interpretation by the tabwriter.
415
p.output = append(p.output, tabwriter.Escape)
416
417
defer func() {
418
p.output = append(p.output, tabwriter.Escape)
419
}()
420
}
421
422
p.output = append(p.output, s...)
423
424
var (
425
newlines int
426
lastNewlineIdx int
427
)
428
429
for i := 0; i < len(s); i++ {
430
if ch := s[i]; ch == '\n' || ch == '\f' {
431
newlines++
432
lastNewlineIdx = i
433
}
434
}
435
436
p.pos.Offset += len(s)
437
438
if newlines > 0 {
439
p.pos.Line += newlines
440
p.out.Line += newlines
441
442
newColumn := len(s) - lastNewlineIdx
443
p.pos.Column = newColumn
444
p.out.Column = newColumn
445
} else {
446
p.pos.Column += len(s)
447
p.out.Column += len(s)
448
}
449
450
p.last = p.pos
451
}
452
453
func (p *printer) writeIndent() {
454
depth := p.cfg.Indent + p.indent
455
for i := 0; i < depth; i++ {
456
p.output = append(p.output, '\t')
457
}
458
459
p.pos.Offset += depth
460
p.pos.Column += depth
461
p.out.Column += depth
462
}
463
464
// writeByte writes ch n times to the output, updating the position of the
465
// printer. writeByte is only used for writing whitespace characters.
466
func (p *printer) writeByte(ch byte, n int) {
467
if p.out.Column == 1 {
468
p.writeIndent()
469
}
470
471
for i := 0; i < n; i++ {
472
p.output = append(p.output, ch)
473
}
474
475
// Update positions.
476
p.pos.Offset += n
477
if ch == '\n' || ch == '\f' {
478
p.pos.Line += n
479
p.out.Line += n
480
p.pos.Column = 1
481
p.out.Column = 1
482
return
483
}
484
p.pos.Column += n
485
p.out.Column += n
486
}
487
488
// writeWhitespace writes the first n whitespace entries in the whitespace
489
// buffer.
490
//
491
// writeWritespace is only safe to be called when len(p.whitespace) >= n.
492
func (p *printer) writeWritespace(n int) {
493
for i := 0; i < n; i++ {
494
switch ch := p.whitespace[i]; ch {
495
case wsIgnore: // no-op
496
case wsIndent:
497
p.indent++
498
case wsUnindent:
499
p.indent--
500
if p.indent < 0 {
501
panic("printer: negative indentation")
502
}
503
default:
504
p.writeByte(byte(ch), 1)
505
}
506
}
507
508
// Shift remaining entries down
509
l := copy(p.whitespace, p.whitespace[n:])
510
p.whitespace = p.whitespace[:l]
511
}
512
513
const maxNewlines = 2
514
515
// newlineLimit limits a newline count to maxNewlines.
516
func newlineLimit(count int) int {
517
if count > maxNewlines {
518
count = maxNewlines
519
}
520
return count
521
}
522
523
// whitespace represents a whitespace token to write to the printer's internal
524
// buffer.
525
type whitespace byte
526
527
const (
528
wsIgnore = whitespace(0)
529
wsBlank = whitespace(' ')
530
wsVTab = whitespace('\v')
531
wsNewline = whitespace('\n')
532
wsFormfeed = whitespace('\f')
533
wsIndent = whitespace('>')
534
wsUnindent = whitespace('<')
535
)
536
537
func (ws whitespace) String() string {
538
switch ws {
539
case wsIgnore:
540
return "wsIgnore"
541
case wsBlank:
542
return "wsBlank"
543
case wsVTab:
544
return "wsVTab"
545
case wsNewline:
546
return "wsNewline"
547
case wsFormfeed:
548
return "wsFormfeed"
549
case wsIndent:
550
return "wsIndent"
551
case wsUnindent:
552
return "wsUnindent"
553
default:
554
return fmt.Sprintf("whitespace(%d)", ws)
555
}
556
}
557
558