// Package printer contains utilities for pretty-printing River ASTs.1package printer23import (4"fmt"5"io"6"math"7"text/tabwriter"89"github.com/grafana/agent/pkg/river/ast"10"github.com/grafana/agent/pkg/river/token"11)1213// Config configures behavior of the printer.14type Config struct {15Indent int // Indentation to apply to all emitted code. Default 0.16}1718// Fprint pretty-prints the specified node to w. The Node type must be an19// *ast.File, ast.Body, or a type that implements ast.Stmt or ast.Expr.20func (c *Config) Fprint(w io.Writer, node ast.Node) (err error) {21var p printer22p.Init(c)2324// Pass all of our text through a trimmer to ignore trailing whitespace.25w = &trimmer{next: w}2627if err = (&walker{p: &p}).Walk(node); err != nil {28return29}3031// Call flush one more time to write trailing comments.32p.flush(token.Position{33Offset: math.MaxInt,34Line: math.MaxInt,35Column: math.MaxInt,36}, token.EOF)3738w = tabwriter.NewWriter(w, 0, 8, 1, ' ', tabwriter.DiscardEmptyColumns|tabwriter.TabIndent)3940if _, err = w.Write(p.output); err != nil {41return42}43if tw, _ := w.(*tabwriter.Writer); tw != nil {44// Flush tabwriter if defined45err = tw.Flush()46}4748return49}5051// Fprint pretty-prints the specified node to w. The Node type must be an52// *ast.File, ast.Body, or a type that implements ast.Stmt or ast.Expr.53func Fprint(w io.Writer, node ast.Node) error {54c := &Config{}55return c.Fprint(w, node)56}5758// The printer writes lexical tokens and whitespace to an internal buffer.59// Comments are written by the printer itself, while all other tokens and60// formatting characters are sent through calls to Write.61//62// Internally, printer depends on a tabwriter for formatting text and aligning63// runs of characters. Horizontal '\t' and vertical '\v' tab characters are64// used to introduce new columns in the row. Runs of characters are stopped65// be either introducing a linefeed '\f' or by having a line with a different66// number of columns from the previous line. See the text/tabwriter package for67// more information on the elastic tabstop algorithm it uses for formatting68// text.69type printer struct {70cfg Config7172// State variables7374output []byte75indent int // Current indentation level76lastTok token.Token // Last token printed (token.LITERAL if it's whitespace)7778// Whitespace holds a buffer of whitespace characters to print prior to the79// next non-whitespace token. Whitespace is held in a buffer to avoid80// printing unnecessary whitespace at the end of a file.81whitespace []whitespace8283// comments stores comments to be processed as elements are printed.84comments commentInfo8586// pos is an approximation of the current position in AST space, and is used87// to determine space between AST elements (e.g., if a comment should come88// before a token). pos automatically as elements are written and can be manually89// set to guarantee an accurate position by passing a token.Pos to Write.90pos token.Position91last token.Position // Last pos written to output (through writeString)9293// out is an accurate representation of the current position in output space,94// used to inject extra formatting like indentation based on the output95// position.96//97// out may differ from pos in terms of whitespace.98out token.Position99}100101type commentInfo struct {102list []ast.CommentGroup103idx int104cur ast.CommentGroup105pos token.Pos106}107108func (ci *commentInfo) commentBefore(next token.Position) bool {109return ci.pos != token.NoPos && ci.pos.Offset() <= next.Offset110}111112// nextComment preloads the next comment.113func (ci *commentInfo) nextComment() {114for ci.idx < len(ci.list) {115c := ci.list[ci.idx]116ci.idx++117if len(c) > 0 {118ci.cur = c119ci.pos = ast.StartPos(c[0])120return121}122}123ci.pos = token.NoPos124}125126// Init initializes the printer for printing. Init is intended to be called127// once per printer and doesn't fully reset its state.128func (p *printer) Init(cfg *Config) {129p.cfg = *cfg130p.pos = token.Position{Line: 1, Column: 1}131p.out = token.Position{Line: 1, Column: 1}132// Capacity is set low since most whitespace sequences are short.133p.whitespace = make([]whitespace, 0, 16)134}135136// SetComments set the comments to use.137func (p *printer) SetComments(comments []ast.CommentGroup) {138p.comments = commentInfo{139list: comments,140idx: 0,141pos: token.NoPos,142}143p.comments.nextComment()144}145146// Write writes a list of writable arguments to the printer.147//148// Arguments can be one of the types described below:149//150// If arg is a whitespace value, it is accumulated into a buffer and flushed151// only after a non-whitespace value is processed. The whitespace buffer will152// be forcibly flushed if the buffer becomes full without writing a153// non-whitespace token.154//155// If arg is an *ast.IdentifierExpr, *ast.LiteralExpr, or a token.Token, the156// human-readable representation of that value will be written.157//158// When writing text, comments which need to appear before that text in159// AST-space are written first, followed by leftover whitespace and then the160// text to write. The written text will update the AST-space position.161//162// If arg is a token.Pos, the AST-space position of the printer is updated to163// the provided Pos. Writing token.Pos values can help make sure the printer's164// AST-space position is accurate, as AST-space position is otherwise an165// estimation based on written data.166func (p *printer) Write(args ...interface{}) {167for _, arg := range args {168var (169data string170isLit bool171)172173switch arg := arg.(type) {174case whitespace:175// Whitespace token; add it to our token buffer. Note that a whitespace176// token is different than the actual whitespace which will get written177// (e.g., wsIndent increases indentation level by one instead of setting178// it to one.)179if arg == wsIgnore {180continue181}182i := len(p.whitespace)183if i == cap(p.whitespace) {184// We built up too much whitespace; this can happen if too many calls185// to Write happen without appending a non-comment token. We will186// force-flush the existing whitespace to avoid a panic.187//188// Ideally this line is never hit based on how we walk the AST, but189// it's kept for safety.190p.writeWritespace(i)191i = 0192}193p.whitespace = p.whitespace[0 : i+1]194p.whitespace[i] = arg195p.lastTok = token.LITERAL196continue197198case *ast.Ident:199data = arg.Name200p.lastTok = token.IDENT201202case *ast.LiteralExpr:203data = arg.Value204p.lastTok = arg.Kind205206case token.Pos:207if arg.Valid() {208p.pos = arg.Position()209}210// Don't write anything; token.Pos is an instruction and doesn't include211// any text to write.212continue213214case token.Token:215s := arg.String()216data = s217218// We will need to inject whitespace if the previous token and the219// current token would combine into a single token when re-scanned. This220// ensures that the sequence of tokens emitted by the output of the221// printer match the sequence of tokens from the input.222if mayCombine(p.lastTok, s[0]) {223if len(p.whitespace) != 0 {224// It shouldn't be possible for the whitespace buffer to be not empty225// here; p.lastTok would've had to been a non-whitespace token and so226// whitespace would've been flushed when it was written to the output227// buffer.228panic("whitespace buffer not empty")229}230p.whitespace = p.whitespace[0:1]231p.whitespace[0] = ' '232}233p.lastTok = arg234235default:236panic(fmt.Sprintf("printer: unsupported argument %v (%T)\n", arg, arg))237}238239next := p.pos240241p.flush(next, p.lastTok)242p.writeString(next, data, isLit)243}244}245246// mayCombine returns true if two tokes must not be combined, because combining247// them would format in a different token sequence being generated.248func mayCombine(prev token.Token, next byte) (b bool) {249switch prev {250case token.NUMBER:251return next == '.' // 1.252case token.DIV:253return next == '*' // /*254default:255return false256}257}258259// flush prints any pending comments and whitespace occurring textually before260// the position of the next token tok. The flush result indicates if a newline261// was written or if a formfeed \f character was dropped from the whitespace262// buffer.263func (p *printer) flush(next token.Position, tok token.Token) {264if p.comments.commentBefore(next) {265p.injectComments(next, tok)266} else if tok != token.EOF {267// Write all remaining whitespace.268p.writeWritespace(len(p.whitespace))269}270}271272func (p *printer) injectComments(next token.Position, tok token.Token) {273var lastComment *ast.Comment274275for p.comments.commentBefore(next) {276for _, c := range p.comments.cur {277p.writeCommentPrefix(next, c)278p.writeComment(next, c)279lastComment = c280}281p.comments.nextComment()282}283284p.writeCommentSuffix(next, tok, lastComment)285}286287// writeCommentPrefix writes whitespace that should appear before c.288func (p *printer) writeCommentPrefix(next token.Position, c *ast.Comment) {289if len(p.output) == 0 {290// The comment is the first thing written to the output. Don't write any291// whitespace before it.292return293}294295cPos := c.StartPos.Position()296297if cPos.Line == p.last.Line {298// Our comment is on the same line as the last token. Write a separator299// between the last token and the comment.300separator := byte('\t')301if cPos.Line == next.Line {302// The comment is on the same line as the next token, which means it has303// to be a block comment (since line comments run to the end of the304// line.) Use a space as the separator instead since a tab in the middle305// of a line between comments would look weird.306separator = byte(' ')307}308p.writeByte(separator, 1)309} else {310// Our comment is on a different line from the last token. First write311// pending whitespace from the last token up to the first newline.312var wsCount int313314for i, ws := range p.whitespace {315switch ws {316case wsBlank, wsVTab:317// Drop any whitespace before the comment.318p.whitespace[i] = wsIgnore319case wsIndent, wsUnindent:320// Allow indentation to be applied.321continue322case wsNewline, wsFormfeed:323// Drop the whitespace since we're about to write our own.324p.whitespace[i] = wsIgnore325}326wsCount = i327break328}329p.writeWritespace(wsCount)330331var newlines int332if cPos.Valid() && p.last.Valid() {333newlines = cPos.Line - p.last.Line334}335if newlines > 0 {336p.writeByte('\f', newlineLimit(newlines))337}338}339}340341func (p *printer) writeComment(_ token.Position, c *ast.Comment) {342p.writeString(c.StartPos.Position(), c.Text, true)343}344345// writeCommentSuffix writes any whitespace necessary between the last comment346// and next. lastComment should be the final comment written.347func (p *printer) writeCommentSuffix(next token.Position, tok token.Token, lastComment *ast.Comment) {348if tok == token.EOF {349// We don't want to add any blank newlines before the end of the file;350// return early.351return352}353354var droppedFF bool355356// If our final comment is a block comment and is on the same line as the357// next token, add a space as a suffix to separate them.358lastCommentPos := ast.EndPos(lastComment).Position()359if lastComment.Text[1] == '*' && next.Line == lastCommentPos.Line {360p.writeByte(' ', 1)361}362363newlines := next.Line - p.last.Line364365for i, ws := range p.whitespace {366switch ws {367case wsBlank, wsVTab:368p.whitespace[i] = wsIgnore369case wsIndent, wsUnindent:370continue371case wsNewline, wsFormfeed:372if ws == wsFormfeed {373droppedFF = true374}375p.whitespace[i] = wsIgnore376}377}378379p.writeWritespace(len(p.whitespace))380381// Write newlines as long as the next token isn't EOF (so that there's no382// blank newlines at the end of the file).383if newlines > 0 {384ch := byte('\n')385if droppedFF {386// If we dropped a formfeed while writing comments, we should emit a new387// one.388ch = byte('\f')389}390p.writeByte(ch, newlineLimit(newlines))391}392}393394// writeString writes the literal string s into the printer's output.395// Formatting characters in s such as '\t' and '\n' will be interpreted by396// underlying tabwriter unless isLit is set.397func (p *printer) writeString(pos token.Position, s string, isLit bool) {398if p.out.Column == 1 {399// We haven't written any text to this line yet; prepend our indentation400// for the line.401p.writeIndent()402}403404if pos.Valid() {405// Update p.pos if pos is valid. This is done *after* handling indentation406// since we want to interpret pos as the literal position for s (and407// writeIndent will update p.pos).408p.pos = pos409}410411if isLit {412// Wrap our literal string in tabwriter.Escape if it's meant to be written413// without interpretation by the tabwriter.414p.output = append(p.output, tabwriter.Escape)415416defer func() {417p.output = append(p.output, tabwriter.Escape)418}()419}420421p.output = append(p.output, s...)422423var (424newlines int425lastNewlineIdx int426)427428for i := 0; i < len(s); i++ {429if ch := s[i]; ch == '\n' || ch == '\f' {430newlines++431lastNewlineIdx = i432}433}434435p.pos.Offset += len(s)436437if newlines > 0 {438p.pos.Line += newlines439p.out.Line += newlines440441newColumn := len(s) - lastNewlineIdx442p.pos.Column = newColumn443p.out.Column = newColumn444} else {445p.pos.Column += len(s)446p.out.Column += len(s)447}448449p.last = p.pos450}451452func (p *printer) writeIndent() {453depth := p.cfg.Indent + p.indent454for i := 0; i < depth; i++ {455p.output = append(p.output, '\t')456}457458p.pos.Offset += depth459p.pos.Column += depth460p.out.Column += depth461}462463// writeByte writes ch n times to the output, updating the position of the464// printer. writeByte is only used for writing whitespace characters.465func (p *printer) writeByte(ch byte, n int) {466if p.out.Column == 1 {467p.writeIndent()468}469470for i := 0; i < n; i++ {471p.output = append(p.output, ch)472}473474// Update positions.475p.pos.Offset += n476if ch == '\n' || ch == '\f' {477p.pos.Line += n478p.out.Line += n479p.pos.Column = 1480p.out.Column = 1481return482}483p.pos.Column += n484p.out.Column += n485}486487// writeWhitespace writes the first n whitespace entries in the whitespace488// buffer.489//490// writeWritespace is only safe to be called when len(p.whitespace) >= n.491func (p *printer) writeWritespace(n int) {492for i := 0; i < n; i++ {493switch ch := p.whitespace[i]; ch {494case wsIgnore: // no-op495case wsIndent:496p.indent++497case wsUnindent:498p.indent--499if p.indent < 0 {500panic("printer: negative indentation")501}502default:503p.writeByte(byte(ch), 1)504}505}506507// Shift remaining entries down508l := copy(p.whitespace, p.whitespace[n:])509p.whitespace = p.whitespace[:l]510}511512const maxNewlines = 2513514// newlineLimit limits a newline count to maxNewlines.515func newlineLimit(count int) int {516if count > maxNewlines {517count = maxNewlines518}519return count520}521522// whitespace represents a whitespace token to write to the printer's internal523// buffer.524type whitespace byte525526const (527wsIgnore = whitespace(0)528wsBlank = whitespace(' ')529wsVTab = whitespace('\v')530wsNewline = whitespace('\n')531wsFormfeed = whitespace('\f')532wsIndent = whitespace('>')533wsUnindent = whitespace('<')534)535536func (ws whitespace) String() string {537switch ws {538case wsIgnore:539return "wsIgnore"540case wsBlank:541return "wsBlank"542case wsVTab:543return "wsVTab"544case wsNewline:545return "wsNewline"546case wsFormfeed:547return "wsFormfeed"548case wsIndent:549return "wsIndent"550case wsUnindent:551return "wsUnindent"552default:553return fmt.Sprintf("whitespace(%d)", ws)554}555}556557558