1 // Copyright 2009 The Go Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file.
5 // Package printer implements printing of AST nodes.
21 const debug = false // enable for debugging
22 const infinity = 1 << 30
27 ignore = whiteSpace(0)
28 blank = whiteSpace(' ')
29 vtab = whiteSpace('\v')
30 newline = whiteSpace('\n')
31 formfeed = whiteSpace('\f')
32 indent = whiteSpace('>')
33 unindent = whiteSpace('<')
36 // Use ignoreMultiLine if the multiLine information is not important.
37 var ignoreMultiLine = new(bool)
39 // A pmode value represents the current printer mode.
43 noExtraLinebreak pmode = 1 << iota
47 // Configuration (does not change after initialization)
52 output bytes.Buffer // raw printer result
53 indent int // current indentation
54 mode pmode // current printer mode
55 lastTok token.Token // the last token printed (token.ILLEGAL if it's whitespace)
56 wsbuf []whiteSpace // delayed white space
58 // The (possibly estimated) position in the generated output;
59 // in AST space (i.e., pos is set whenever a token position is
60 // known accurately, and updated dependending on what has been
64 // The value of pos immediately after the last item has been
65 // written using writeItem.
68 // The list of all source comments, in order of appearance.
69 comments []*ast.CommentGroup // may be nil
70 cindex int // current comment index
71 useNodeComments bool // if not set, ignore lead and line comments of nodes
73 // Cache of already computed node sizes.
74 nodeSizes map[ast.Node]int
77 func (p *printer) init(cfg *Config, fset *token.FileSet, nodeSizes map[ast.Node]int) {
80 p.wsbuf = make([]whiteSpace, 0, 16) // whitespace sequences are short
81 p.nodeSizes = nodeSizes
84 func (p *printer) internalError(msg ...interface{}) {
86 fmt.Print(p.pos.String() + ": ")
92 // nlines returns the adjusted number of linebreaks given the desired number
93 // of breaks n such that min <= result <= max.
95 func (p *printer) nlines(n, min int) int {
96 const max = 2 // max. number of newlines
106 // writeByte writes a single byte to p.output and updates p.pos.
107 func (p *printer) writeByte(ch byte) {
108 p.output.WriteByte(ch)
112 if ch == '\n' || ch == '\f' {
114 // use "hard" htabs - indentation columns
115 // must not be discarded by the tabwriter
116 const htabs = "\t\t\t\t\t\t\t\t"
119 p.output.WriteString(htabs)
122 p.output.WriteString(htabs[0:j])
126 p.pos.Offset += p.indent
127 p.pos.Column = 1 + p.indent
131 // writeNewlines writes up to n newlines to p.output and updates p.pos.
132 // The actual number of newlines written is limited by nlines.
133 // nl must be one of '\n' or '\f'.
135 func (p *printer) writeNewlines(n int, nl byte) {
136 for n = p.nlines(n, 0); n > 0; n-- {
141 // writeString writes the string s to p.output and updates p.pos.
142 // If isLit is set, s is escaped w/ tabwriter.Escape characters
143 // to protect s from being interpreted by the tabwriter.
145 // Note: writeString is only used to write Go tokens, literals, and
146 // comments, all of which must be written literally. Thus, it is correct
147 // to always set isLit = true. However, setting it explicitly only when
148 // needed (i.e., when we don't know that s contains no tabs or line breaks)
149 // avoids processing extra escape characters and reduces run time of the
150 // printer benchmark by up to 10%.
152 func (p *printer) writeString(s string, isLit bool) {
154 // Protect s such that is passes through the tabwriter
155 // unchanged. Note that valid Go programs cannot contain
156 // tabwriter.Escape bytes since they do not appear in legal
158 p.output.WriteByte(tabwriter.Escape)
161 p.output.WriteString(s)
165 column := p.pos.Column + len(s)
166 for i := 0; i < len(s); i++ {
172 p.pos.Offset += len(s)
174 p.pos.Column = column
177 p.output.WriteByte(tabwriter.Escape)
181 // writeItem writes data at position pos. data is the text corresponding to
182 // a single lexical token, but may also be comment text. pos is the actual
183 // (or at least very accurately estimated) position of the data in the original
184 // source text. writeItem updates p.last to the position immediately following
187 func (p *printer) writeItem(pos token.Position, data string, isLit bool) {
189 // continue with previous position if we don't have a valid pos
190 if p.last.IsValid() && p.last.Filename != pos.Filename {
191 // the file has changed - reset state
192 // (used when printing merged ASTs of different files
193 // e.g., the result of ast.MergePackageFiles)
196 p.wsbuf = p.wsbuf[0:0]
201 // do not update p.pos - use write0
202 _, filename := filepath.Split(pos.Filename)
203 fmt.Fprintf(&p.output, "[%s:%d:%d]", filename, pos.Line, pos.Column)
205 p.writeString(data, isLit)
209 const linePrefix = "//line "
211 // writeCommentPrefix writes the whitespace before a comment.
212 // If there is any pending whitespace, it consumes as much of
213 // it as is likely to help position the comment nicely.
214 // pos is the comment position, next the position of the item
215 // after all pending comments, prev is the previous comment in
216 // a group of comments (or nil), and isKeyword indicates if the
217 // next item is a keyword.
219 func (p *printer) writeCommentPrefix(pos, next token.Position, prev, comment *ast.Comment, isKeyword bool) {
220 if p.output.Len() == 0 {
221 // the comment is the first item to be printed - don't write any whitespace
225 if pos.IsValid() && pos.Filename != p.last.Filename {
226 // comment in a different file - separate with newlines (writeNewlines will limit the number)
227 p.writeNewlines(10, '\f')
231 if pos.Line == p.last.Line && (prev == nil || prev.Text[1] != '/') {
232 // comment on the same line as last item:
233 // separate with at least one separator
236 // first comment of a comment group
238 for i, ch := range p.wsbuf {
241 // ignore any blanks before a comment
245 // respect existing tabs - important
246 // for proper formatting of commented structs
250 // apply pending indentation
258 // make sure there is at least one separator
261 if pos.Line == next.Line {
262 // next item is on the same line as the comment
263 // (which must be a /*-style comment): separate
264 // with a blank instead of a tab
271 // comment on a different line:
272 // separate with at least one line break
274 // first comment of a comment group
276 for i, ch := range p.wsbuf {
279 // ignore any horizontal whitespace before line breaks
283 // apply pending indentation
286 // if the next token is a keyword, apply the outdent
287 // if it appears that the comment is aligned with the
288 // keyword; otherwise assume the outdent is part of a
289 // closing block and stop (this scenario appears with
290 // comments before a case label where the comments
291 // apply to the next case instead of the current one)
292 if isKeyword && pos.Column == next.Column {
295 case newline, formfeed:
296 // TODO(gri): may want to keep formfeed info in some cases
305 // turn off indent if we're about to print a line directive.
307 if strings.HasPrefix(comment.Text, linePrefix) {
311 // use formfeeds to break columns before a comment;
312 // this is analogous to using formfeeds to separate
313 // individual lines of /*-style comments - but make
314 // sure there is at least one line break if the previous
315 // comment was a line comment
316 n := pos.Line - p.last.Line // if !pos.IsValid(), pos.Line == 0, and n will be 0
317 if n <= 0 && prev != nil && prev.Text[1] == '/' {
321 p.writeNewlines(n, '\f')
327 // Split comment text into lines
328 // (using strings.Split(text, "\n") is significantly slower for
329 // this specific purpose, as measured with: gotest -bench=Print)
330 func split(text string) []string {
331 // count lines (comment text never ends in a newline)
333 for i := 0; i < len(text); i++ {
340 lines := make([]string, n)
343 for j := 0; j < len(text); j++ {
345 lines[n] = text[i:j] // exclude newline
346 i = j + 1 // discard newline
355 // Returns true if s contains only white space
356 // (only tabs and blanks can appear in the printer's context).
357 func isBlank(s string) bool {
358 for i := 0; i < len(s); i++ {
366 func commonPrefix(a, b string) string {
368 for i < len(a) && i < len(b) && a[i] == b[i] && (a[i] <= ' ' || a[i] == '*') {
374 func stripCommonPrefix(lines []string) {
376 return // at most one line - nothing to do
380 // The heuristic in this function tries to handle a few
381 // common patterns of /*-style comments: Comments where
382 // the opening /* and closing */ are aligned and the
383 // rest of the comment text is aligned and indented with
384 // blanks or tabs, cases with a vertical "line of stars"
385 // on the left, and cases where the closing */ is on the
386 // same line as the last comment text.
388 // Compute maximum common white prefix of all but the first,
389 // last, and blank lines, and replace blank lines with empty
390 // lines (the first line starts with /* and has no prefix).
391 // In case of two-line comments, consider the last line for
392 // the prefix computation since otherwise the prefix would
395 // Note that the first and last line are never empty (they
396 // contain the opening /* and closing */ respectively) and
397 // thus they can be ignored by the blank line check.
401 for i, line := range lines[1 : len(lines)-1] {
404 lines[1+i] = "" // range starts at line 1
406 prefix = commonPrefix(line, line)
409 prefix = commonPrefix(prefix, line)
412 } else { // len(lines) == 2, lines cannot be blank (contain /* and */)
414 prefix = commonPrefix(line, line)
418 * Check for vertical "line of stars" and correct prefix accordingly.
421 if i := strings.Index(prefix, "*"); i >= 0 {
422 // Line of stars present.
423 if i > 0 && prefix[i-1] == ' ' {
424 i-- // remove trailing blank from prefix so stars remain aligned
429 // No line of stars present.
430 // Determine the white space on the first line after the /*
431 // and before the beginning of the comment text, assume two
432 // blanks instead of the /* unless the first character after
433 // the /* is a tab. If the first comment line is empty but
434 // for the opening /*, assume up to 3 blanks or a tab. This
435 // whitespace may be found as suffix in the common prefix.
437 if isBlank(first[2:]) {
438 // no comment text on the first line:
439 // reduce prefix by up to 3 blanks or a tab
440 // if present - this keeps comment text indented
441 // relative to the /* and */'s if it was indented
442 // in the first place
444 for n := 0; n < 3 && i > 0 && prefix[i-1] == ' '; n++ {
447 if i == len(prefix) && i > 0 && prefix[i-1] == '\t' {
452 // comment text on the first line
453 suffix := make([]byte, len(first))
454 n := 2 // start after opening /*
455 for n < len(first) && first[n] <= ' ' {
459 if n > 2 && suffix[2] == '\t' {
460 // assume the '\t' compensates for the /*
463 // otherwise assume two blanks
464 suffix[0], suffix[1] = ' ', ' '
467 // Shorten the computed common prefix by the length of
468 // suffix, if it is found as suffix of the prefix.
469 if strings.HasSuffix(prefix, string(suffix)) {
470 prefix = prefix[0 : len(prefix)-len(suffix)]
475 // Handle last line: If it only contains a closing */, align it
476 // with the opening /*, otherwise align the text with the other
478 last := lines[len(lines)-1]
480 i := strings.Index(last, closing) // i >= 0 (closing is always present)
481 if isBlank(last[0:i]) {
482 // last line only contains closing */
484 closing = " */" // add blank to align final star
486 lines[len(lines)-1] = prefix + closing
488 // last line contains more comment text - assume
489 // it is aligned like the other lines and include
490 // in prefix computation
491 prefix = commonPrefix(prefix, last)
494 // Remove the common prefix from all but the first and empty lines.
495 for i, line := range lines[1:] {
497 lines[1+i] = line[len(prefix):] // range starts at line 1
502 func (p *printer) writeComment(comment *ast.Comment) {
505 if strings.HasPrefix(text, linePrefix) {
506 pos := strings.TrimSpace(text[len(linePrefix):])
507 i := strings.LastIndex(pos, ":")
509 // The line directive we are about to print changed
510 // the Filename and Line number used by go/token
511 // as it was reading the input originally.
512 // In order to match the original input, we have to
513 // update our own idea of the file and line number
514 // accordingly, after printing the directive.
516 line, _ := strconv.Atoi(pos[i+1:])
518 p.pos.Filename = file
525 // shortcut common case of //-style comments
527 p.writeItem(p.fset.Position(comment.Pos()), text, true)
531 // for /*-style comments, print line by line and let the
532 // write function take care of the proper indentation
534 stripCommonPrefix(lines)
536 // write comment lines, separated by formfeed,
537 // without a line break after the last line
538 pos := p.fset.Position(comment.Pos())
539 for i, line := range lines {
545 p.writeItem(pos, line, true)
550 // writeCommentSuffix writes a line break after a comment if indicated
551 // and processes any leftover indentation information. If a line break
552 // is needed, the kind of break (newline vs formfeed) depends on the
553 // pending whitespace. writeCommentSuffix returns true if a pending
554 // formfeed was dropped from the whitespace buffer.
556 func (p *printer) writeCommentSuffix(needsLinebreak bool) (droppedFF bool) {
557 for i, ch := range p.wsbuf {
560 // ignore trailing whitespace
562 case indent, unindent:
563 // don't lose indentation information
564 case newline, formfeed:
565 // if we need a line break, keep exactly one
566 // but remember if we dropped any formfeeds
568 needsLinebreak = false
577 p.writeWhitespace(len(p.wsbuf))
579 // make sure we have a line break
587 // intersperseComments consumes all comments that appear before the next token
588 // tok and prints it together with the buffered whitespace (i.e., the whitespace
589 // that needs to be written before the next token). A heuristic is used to mix
590 // the comments and whitespace. intersperseComments returns true if a pending
591 // formfeed was dropped from the whitespace buffer.
593 func (p *printer) intersperseComments(next token.Position, tok token.Token) (droppedFF bool) {
594 var last *ast.Comment
595 for ; p.commentBefore(next); p.cindex++ {
596 for _, c := range p.comments[p.cindex].List {
597 p.writeCommentPrefix(p.fset.Position(c.Pos()), next, last, c, tok.IsKeyword())
604 if last.Text[1] == '*' && p.fset.Position(last.Pos()).Line == next.Line {
605 // the last comment is a /*-style comment and the next item
606 // follows on the same line: separate with an extra blank
609 // ensure that there is a line break after a //-style comment,
610 // before a closing '}' unless explicitly disabled, or at eof
612 last.Text[1] == '/' ||
613 tok == token.RBRACE && p.mode&noExtraLinebreak == 0 ||
615 return p.writeCommentSuffix(needsLinebreak)
618 // no comment was written - we should never reach here since
619 // intersperseComments should not be called in that case
620 p.internalError("intersperseComments called without pending comments")
624 // whiteWhitespace writes the first n whitespace entries.
625 func (p *printer) writeWhitespace(n int) {
627 for i := 0; i < n; i++ {
628 switch ch := p.wsbuf[i]; ch {
636 p.internalError("negative indentation:", p.indent)
639 case newline, formfeed:
640 // A line break immediately followed by a "correcting"
641 // unindent is swapped with the unindent - this permits
642 // proper label positioning. If a comment is between
643 // the line break and the label, the unindent is not
644 // part of the comment whitespace prefix and the comment
645 // will be positioned correctly indented.
646 if i+1 < n && p.wsbuf[i+1] == unindent {
647 // Use a formfeed to terminate the current section.
648 // Otherwise, a long label name on the next line leading
649 // to a wide column may increase the indentation column
650 // of lines before the label; effectively leading to wrong
652 p.wsbuf[i], p.wsbuf[i+1] = unindent, formfeed
658 p.writeByte(byte(ch))
662 // shift remaining entries down
664 for ; n < len(p.wsbuf); n++ {
665 p.wsbuf[i] = p.wsbuf[n]
668 p.wsbuf = p.wsbuf[0:i]
671 // ----------------------------------------------------------------------------
672 // Printing interface
674 func mayCombine(prev token.Token, next byte) (b bool) {
677 b = next == '.' // 1.
679 b = next == '+' // ++
681 b = next == '-' // --
683 b = next == '*' // /*
685 b = next == '-' || next == '<' // <- or <<
687 b = next == '&' || next == '^' // && or &^
692 // print prints a list of "items" (roughly corresponding to syntactic
693 // tokens, but also including whitespace and formatting information).
694 // It is the only print function that should be called directly from
695 // any of the AST printing functions in nodes.go.
697 // Whitespace is accumulated until a non-whitespace token appears. Any
698 // comments that need to appear before that token are printed first,
699 // taking into account the amount and structure of any pending white-
700 // space for best comment placement. Then, any leftover whitespace is
701 // printed, followed by the actual token.
703 func (p *printer) print(args ...interface{}) {
704 for _, f := range args {
705 next := p.pos // estimated position of next item
710 switch x := f.(type) {
712 // toggle printer mode
716 // don't add ignore's to the buffer; they
717 // may screw up "correcting" unindents (see
722 if i == cap(p.wsbuf) {
723 // Whitespace sequences are very short so this should
724 // never happen. Handle gracefully (but possibly with
725 // bad comment placement) if it does happen.
729 p.wsbuf = p.wsbuf[0 : i+1]
740 if mayCombine(p.lastTok, s[0]) {
741 // the previous and the current token must be
742 // separated by a blank otherwise they combine
743 // into a different incorrect token sequence
744 // (except for token.INT followed by a '.' this
745 // should never happen because it is taken care
746 // of via binary expression formatting)
747 if len(p.wsbuf) != 0 {
748 p.internalError("whitespace buffer not empty")
750 p.wsbuf = p.wsbuf[0:1]
757 next = p.fset.Position(x) // accurate position of next item
761 fmt.Fprintf(os.Stderr, "print: unsupported argument type %T\n", f)
762 panic("go/printer type")
769 if p.flush(next, tok) {
770 nl = '\f' // dropped formfeed before
773 // intersperse extra newlines if present in the source
774 // (don't do this in flush as it will cause extra newlines
775 // at the end of a file) - use formfeeds if we dropped one
777 if n := next.Line - p.pos.Line; n > 0 {
778 p.writeNewlines(n, nl)
781 p.writeItem(next, data, isLit)
786 // commentBefore returns true iff the current comment occurs
787 // before the next position in the source code.
789 func (p *printer) commentBefore(next token.Position) bool {
790 return p.cindex < len(p.comments) && p.fset.Position(p.comments[p.cindex].List[0].Pos()).Offset < next.Offset
793 // Flush prints any pending comments and whitespace occurring
794 // textually before the position of the next token tok. Flush
795 // returns true if a pending formfeed character was dropped
796 // from the whitespace buffer as a result of interspersing
799 func (p *printer) flush(next token.Position, tok token.Token) (droppedFF bool) {
800 if p.commentBefore(next) {
801 // if there are comments before the next item, intersperse them
802 droppedFF = p.intersperseComments(next, tok)
804 // otherwise, write any leftover whitespace
805 p.writeWhitespace(len(p.wsbuf))
810 func (p *printer) printNode(node interface{}) error {
811 switch n := node.(type) {
813 p.useNodeComments = true
814 p.expr(n, ignoreMultiLine)
816 p.useNodeComments = true
817 // A labeled statement will un-indent to position the
818 // label. Set indent to 1 so we don't get indent "underflow".
819 if _, labeledStmt := n.(*ast.LabeledStmt); labeledStmt {
822 p.stmt(n, false, ignoreMultiLine)
824 p.useNodeComments = true
825 p.decl(n, ignoreMultiLine)
827 p.useNodeComments = true
828 p.spec(n, 1, false, ignoreMultiLine)
830 p.comments = n.Comments
831 p.useNodeComments = n.Comments == nil
834 return fmt.Errorf("go/printer: unsupported node type %T", n)
839 // ----------------------------------------------------------------------------
842 // A trimmer is an io.Writer filter for stripping tabwriter.Escape
843 // characters, trailing blanks and tabs, and for converting formfeed
844 // and vtab characters into newlines and htabs (in case no tabwriter
845 // is used). Text bracketed by tabwriter.Escape characters is passed
846 // through unchanged.
848 type trimmer struct {
854 // trimmer is implemented as a state machine.
855 // It can be in one of the following states:
857 inSpace = iota // inside space
858 inEscape // inside text bracketed by tabwriter.Escapes
859 inText // inside text
862 // Design note: It is tempting to eliminate extra blanks occurring in
863 // whitespace in this function as it could simplify some
864 // of the blanks logic in the node printing functions.
865 // However, this would mess up any formatting done by
868 var aNewline = []byte("\n")
870 func (p *trimmer) Write(data []byte) (n int, err error) {
872 // p.state == inSpace:
873 // p.space is unwritten
874 // p.state == inEscape, inText:
875 // data[m:n] is unwritten
878 for n, b = range data {
880 b = '\t' // convert to htab
886 p.space.WriteByte(b) // WriteByte returns no errors
888 p.space.Reset() // discard trailing space
889 _, err = p.output.Write(aNewline)
890 case tabwriter.Escape:
891 _, err = p.output.Write(p.space.Bytes())
893 m = n + 1 // +1: skip tabwriter.Escape
895 _, err = p.output.Write(p.space.Bytes())
900 if b == tabwriter.Escape {
901 _, err = p.output.Write(data[m:n])
908 _, err = p.output.Write(data[m:n])
911 p.space.WriteByte(b) // WriteByte returns no errors
913 _, err = p.output.Write(data[m:n])
916 _, err = p.output.Write(aNewline)
917 case tabwriter.Escape:
918 _, err = p.output.Write(data[m:n])
920 m = n + 1 // +1: skip tabwriter.Escape
932 case inEscape, inText:
933 _, err = p.output.Write(data[m:n])
941 // ----------------------------------------------------------------------------
944 // General printing is controlled with these Config.Mode flags.
946 RawFormat uint = 1 << iota // do not use a tabwriter; if set, UseSpaces is ignored
947 TabIndent // use tabs for indentation independent of UseSpaces
948 UseSpaces // use spaces instead of tabs for alignment
951 // A Config node controls the output of Fprint.
953 Mode uint // default: 0
954 Tabwidth int // default: 8
957 // fprint implements Fprint and takes a nodesSizes map for setting up the printer state.
958 func (cfg *Config) fprint(output io.Writer, fset *token.FileSet, node interface{}, nodeSizes map[ast.Node]int) (err error) {
961 p.init(cfg, fset, nodeSizes)
962 if err = p.printNode(node); err != nil {
965 p.flush(token.Position{Offset: infinity, Line: infinity}, token.EOF)
967 // redirect output through a trimmer to eliminate trailing whitespace
968 // (Input to a tabwriter must be untrimmed since trailing tabs provide
969 // formatting information. The tabwriter could provide trimming
970 // functionality but no tabwriter is used when RawFormat is set.)
971 output = &trimmer{output: output}
973 // redirect output through a tabwriter if necessary
974 if cfg.Mode&RawFormat == 0 {
975 minwidth := cfg.Tabwidth
977 padchar := byte('\t')
978 if cfg.Mode&UseSpaces != 0 {
982 twmode := tabwriter.DiscardEmptyColumns
983 if cfg.Mode&TabIndent != 0 {
985 twmode |= tabwriter.TabIndent
988 output = tabwriter.NewWriter(output, minwidth, cfg.Tabwidth, 1, padchar, twmode)
991 // write printer result via tabwriter/trimmer to output
992 if _, err = output.Write(p.output.Bytes()); err != nil {
996 // flush tabwriter, if any
997 if tw, _ := (output).(*tabwriter.Writer); tw != nil {
1004 // Fprint "pretty-prints" an AST node to output for a given configuration cfg.
1005 // Position information is interpreted relative to the file set fset.
1006 // The node type must be *ast.File, or assignment-compatible to ast.Expr,
1007 // ast.Decl, ast.Spec, or ast.Stmt.
1009 func (cfg *Config) Fprint(output io.Writer, fset *token.FileSet, node interface{}) error {
1010 return cfg.fprint(output, fset, node, make(map[ast.Node]int))
1013 // Fprint "pretty-prints" an AST node to output.
1014 // It calls Config.Fprint with default settings.
1016 func Fprint(output io.Writer, fset *token.FileSet, node interface{}) error {
1017 return (&Config{Tabwidth: 8}).Fprint(output, fset, node)