Source File
printer.go
Belonging Package
go/printer
// Copyright 2009 The Go Authors. All rights reserved.// Use of this source code is governed by a BSD-style// license that can be found in the LICENSE file.// Package printer implements printing of AST nodes.package printerimport ()const (maxNewlines = 2 // max. number of newlines between source textdebug = false // enable for debugginginfinity = 1 << 30)type whiteSpace byteconst (ignore = whiteSpace(0)blank = whiteSpace(' ')vtab = whiteSpace('\v')newline = whiteSpace('\n')formfeed = whiteSpace('\f')indent = whiteSpace('>')unindent = whiteSpace('<'))// A pmode value represents the current printer mode.type pmode intconst (noExtraBlank pmode = 1 << iota // disables extra blank after /*-style commentnoExtraLinebreak // disables extra line break after /*-style comment)type commentInfo struct {cindex int // index of the next commentcomment *ast.CommentGroup // = printer.comments[cindex-1]; or nilcommentOffset int // = printer.posFor(printer.comments[cindex-1].List[0].Pos()).Offset; or infinitycommentNewline bool // true if the comment group contains newlines}type printer struct {// Configuration (does not change after initialization)Configfset *token.FileSet// Current stateoutput []byte // raw printer resultindent int // current indentationlevel int // level == 0: outside composite literal; level > 0: inside composite literalmode pmode // current printer modeendAlignment bool // if set, terminate alignment immediatelyimpliedSemi bool // if set, a linebreak implies a semicolonlastTok token.Token // last token printed (token.ILLEGAL if it's whitespace)prevOpen token.Token // previous non-brace "open" token (, [, or token.ILLEGALwsbuf []whiteSpace // delayed white spacegoBuild []int // start index of all //go:build comments in outputplusBuild []int // start index of all // +build comments in output// Positions// The out position differs from the pos position when the result// formatting differs from the source formatting (in the amount of// white space). If there's a difference and SourcePos is set in// ConfigMode, //line directives are used in the output to restore// original source positions for a reader.pos token.Position // current position in AST (source) spaceout token.Position // current position in output spacelast token.Position // value of pos after calling writeStringlinePtr *int // if set, record out.Line for the next token in *linePtrsourcePosErr error // if non-nil, the first error emitting a //line directive// The list of all source comments, in order of appearance.comments []*ast.CommentGroup // may be niluseNodeComments bool // if not set, ignore lead and line comments of nodes// Information about p.comments[p.cindex]; set up by nextComment.commentInfo// Cache of already computed node sizes.nodeSizes map[ast.Node]int// Cache of most recently computed line position.cachedPos token.PoscachedLine int // line corresponding to cachedPos}func ( *printer) ( ...any) {if debug {fmt.Print(.pos.String() + ": ")fmt.Println(...)panic("go/printer")}}// commentsHaveNewline reports whether a list of comments belonging to// an *ast.CommentGroup contains newlines. Because the position information// may only be partially correct, we also have to read the comment text.func ( *printer) ( []*ast.Comment) bool {// len(list) > 0:= .lineFor([0].Pos())for , := range {if > 0 && .lineFor([].Pos()) != {// not all comments on the same linereturn true}if := .Text; len() >= 2 && ([1] == '/' || strings.Contains(, "\n")) {return true}}_ =return false}func ( *printer) () {for .cindex < len(.comments) {:= .comments[.cindex].cindex++if := .List; len() > 0 {.comment =.commentOffset = .posFor([0].Pos()).Offset.commentNewline = .commentsHaveNewline()return}// we should not reach here (correct ASTs don't have empty// ast.CommentGroup nodes), but be conservative and try again}// no more comments.commentOffset = infinity}// commentBefore reports whether the current comment group occurs// before the next position in the source code and printing it does// not introduce implicit semicolons.func ( *printer) ( token.Position) bool {return .commentOffset < .Offset && (!.impliedSemi || !.commentNewline)}// commentSizeBefore returns the estimated size of the// comments on the same line before the next position.func ( *printer) ( token.Position) int {// save/restore current p.commentInfo (p.nextComment() modifies it)defer func( commentInfo) {.commentInfo =}(.commentInfo):= 0for .commentBefore() {for , := range .comment.List {+= len(.Text)}.nextComment()}return}// recordLine records the output line number for the next non-whitespace// token in *linePtr. It is used to compute an accurate line number for a// formatted construct, independent of pending (not yet emitted) whitespace// or comments.func ( *printer) ( *int) {.linePtr =}// linesFrom returns the number of output lines between the current// output line and the line argument, ignoring any pending (not yet// emitted) whitespace or comments. It is used to compute an accurate// size (in number of lines) for a formatted construct.func ( *printer) ( int) int {return .out.Line -}func ( *printer) ( token.Pos) token.Position {// not used frequently enough to cache entire token.Positionreturn .fset.PositionFor(, false /* absolute position */)}func ( *printer) ( token.Pos) int {if != .cachedPos {.cachedPos =.cachedLine = .fset.PositionFor(, false /* absolute position */).Line}return .cachedLine}// writeLineDirective writes a //line directive if necessary.func ( *printer) ( token.Position) {if .IsValid() && (.out.Line != .Line || .out.Filename != .Filename) {if strings.ContainsAny(.Filename, "\r\n") {if .sourcePosErr == nil {.sourcePosErr = fmt.Errorf("go/printer: source filename contains unexpected newline character: %q", .Filename)}return}.output = append(.output, tabwriter.Escape) // protect '\n' in //line from tabwriter interpretation.output = append(.output, fmt.Sprintf("//line %s:%d\n", .Filename, .Line)...).output = append(.output, tabwriter.Escape)// p.out must match the //line directive.out.Filename = .Filename.out.Line = .Line}}// writeIndent writes indentation.func ( *printer) () {// use "hard" htabs - indentation columns// must not be discarded by the tabwriter:= .Config.Indent + .indent // include base indentationfor := 0; < ; ++ {.output = append(.output, '\t')}// update positions.pos.Offset +=.pos.Column +=.out.Column +=}// writeByte writes ch n times to p.output and updates p.pos.// Only used to write formatting (white space) characters.func ( *printer) ( byte, int) {if .endAlignment {// Ignore any alignment control character;// and at the end of the line, break with// a formfeed to indicate termination of// existing columns.switch {case '\t', '\v':= ' 'case '\n', '\f':= '\f'.endAlignment = false}}if .out.Column == 1 {// no need to write line directives before white space.writeIndent()}for := 0; < ; ++ {.output = append(.output, )}// update positions.pos.Offset +=if == '\n' || == '\f' {.pos.Line +=.out.Line +=.pos.Column = 1.out.Column = 1return}.pos.Column +=.out.Column +=}// writeString writes the string s to p.output and updates p.pos, p.out,// and p.last. If isLit is set, s is escaped w/ tabwriter.Escape characters// to protect s from being interpreted by the tabwriter.//// Note: writeString is only used to write Go tokens, literals, and// comments, all of which must be written literally. Thus, it is correct// to always set isLit = true. However, setting it explicitly only when// needed (i.e., when we don't know that s contains no tabs or line breaks)// avoids processing extra escape characters and reduces run time of the// printer benchmark by up to 10%.func ( *printer) ( token.Position, string, bool) {if .out.Column == 1 {if .Config.Mode&SourcePos != 0 {.writeLineDirective()}.writeIndent()}if .IsValid() {// update p.pos (if pos is invalid, continue with existing p.pos)// Note: Must do this after handling line beginnings because// writeIndent updates p.pos if there's indentation, but p.pos// is the position of s..pos =}if {// Protect s such that is passes through the tabwriter// unchanged. Note that valid Go programs cannot contain// tabwriter.Escape bytes since they do not appear in legal// UTF-8 sequences..output = append(.output, tabwriter.Escape)}if debug {.output = append(.output, fmt.Sprintf("/*%s*/", )...) // do not update p.pos!}.output = append(.output, ...)// update positions:= 0var int // index of last newline; valid if nlines > 0for := 0; < len(); ++ {// Raw string literals may contain any character except back quote (`).if := []; == '\n' || == '\f' {// account for line break++=// A line break inside a literal will break whatever column// formatting is in place; ignore any further alignment through// the end of the line..endAlignment = true}}.pos.Offset += len()if > 0 {.pos.Line +=.out.Line +=:= len() -.pos.Column =.out.Column =} else {.pos.Column += len().out.Column += len()}if {.output = append(.output, tabwriter.Escape)}.last = .pos}// writeCommentPrefix writes the whitespace before a comment.// If there is any pending whitespace, it consumes as much of// it as is likely to help position the comment nicely.// pos is the comment position, next the position of the item// after all pending comments, prev is the previous comment in// a group of comments (or nil), and tok is the next token.func ( *printer) (, token.Position, *ast.Comment, token.Token) {if len(.output) == 0 {// the comment is the first item to be printed - don't write any whitespacereturn}if .IsValid() && .Filename != .last.Filename {// comment in a different file - separate with newlines.writeByte('\f', maxNewlines)return}if .Line == .last.Line && ( == nil || .Text[1] != '/') {// comment on the same line as last item:// separate with at least one separator:= falseif == nil {// first comment of a comment group:= 0for , := range .wsbuf {switch {case blank:// ignore any blanks before a comment.wsbuf[] = ignorecontinuecase vtab:// respect existing tabs - important// for proper formatting of commented structs= truecontinuecase indent:// apply pending indentationcontinue}=break}.writeWhitespace()}// make sure there is at least one separatorif ! {:= byte('\t')if .Line == .Line {// next item is on the same line as the comment// (which must be a /*-style comment): separate// with a blank instead of a tab= ' '}.writeByte(, 1)}} else {// comment on a different line:// separate with at least one line break:= false:= 0for , := range .wsbuf {switch {case blank, vtab:// ignore any horizontal whitespace before line breaks.wsbuf[] = ignorecontinuecase indent:// apply pending indentationcontinuecase unindent:// if this is not the last unindent, apply it// as it is (likely) belonging to the last// construct (e.g., a multi-line expression list)// and is not part of closing a blockif +1 < len(.wsbuf) && .wsbuf[+1] == unindent {continue}// if the next token is not a closing }, apply the unindent// if it appears that the comment is aligned with the// token; otherwise assume the unindent is part of a// closing block and stop (this scenario appears with// comments before a case label where the comments// apply to the next case instead of the current one)if != token.RBRACE && .Column == .Column {continue}case newline, formfeed:.wsbuf[] = ignore= == nil // record only if first comment of a group}=break}.writeWhitespace()// determine number of linebreaks before the comment:= 0if .IsValid() && .last.IsValid() {= .Line - .last.Lineif < 0 { // should never happen= 0}}// at the package scope level only (p.indent == 0),// add an extra newline if we dropped one before:// this preserves a blank line before documentation// comments at the package scope level (issue 2570)if .indent == 0 && {++}// make sure there is at least one line break// if the previous comment was a line commentif == 0 && != nil && .Text[1] == '/' {= 1}if > 0 {// use formfeeds to break columns before a comment;// this is analogous to using formfeeds to separate// individual lines of /*-style comments.writeByte('\f', nlimit())}}}// Returns true if s contains only white space// (only tabs and blanks can appear in the printer's context).func ( string) bool {for := 0; < len(); ++ {if [] > ' ' {return false}}return true}// commonPrefix returns the common prefix of a and b.func (, string) string {:= 0for < len() && < len() && [] == [] && ([] <= ' ' || [] == '*') {++}return [0:]}// trimRight returns s with trailing whitespace removed.func ( string) string {return strings.TrimRightFunc(, unicode.IsSpace)}// stripCommonPrefix removes a common prefix from /*-style comment lines (unless no// comment line is indented, all but the first line have some form of space prefix).// The prefix is computed using heuristics such that is likely that the comment// contents are nicely laid out after re-printing each line using the printer's// current indentation.func ( []string) {if len() <= 1 {return // at most one line - nothing to do}// len(lines) > 1// The heuristic in this function tries to handle a few// common patterns of /*-style comments: Comments where// the opening /* and closing */ are aligned and the// rest of the comment text is aligned and indented with// blanks or tabs, cases with a vertical "line of stars"// on the left, and cases where the closing */ is on the// same line as the last comment text.// Compute maximum common white prefix of all but the first,// last, and blank lines, and replace blank lines with empty// lines (the first line starts with /* and has no prefix).// In cases where only the first and last lines are not blank,// such as two-line comments, or comments where all inner lines// are blank, consider the last line for the prefix computation// since otherwise the prefix would be empty.//// Note that the first and last line are never empty (they// contain the opening /* and closing */ respectively) and// thus they can be ignored by the blank line check.:= "":= falseif len() > 2 {for , := range [1 : len()-1] {if isBlank() {[1+] = "" // range starts with lines[1]} else {if ! {== true}= commonPrefix(, )}}}// If we don't have a prefix yet, consider the last line.if ! {:= [len()-1]= commonPrefix(, )}/** Check for vertical "line of stars" and correct prefix accordingly.*/:= falseif , , := strings.Cut(, "*"); {// remove trailing blank from prefix so stars remain aligned= strings.TrimSuffix(, " ")= true} else {// No line of stars present.// Determine the white space on the first line after the /*// and before the beginning of the comment text, assume two// blanks instead of the /* unless the first character after// the /* is a tab. If the first comment line is empty but// for the opening /*, assume up to 3 blanks or a tab. This// whitespace may be found as suffix in the common prefix.:= [0]if isBlank([2:]) {// no comment text on the first line:// reduce prefix by up to 3 blanks or a tab// if present - this keeps comment text indented// relative to the /* and */'s if it was indented// in the first place:= len()for := 0; < 3 && > 0 && [-1] == ' '; ++ {--}if == len() && > 0 && [-1] == '\t' {--}= [0:]} else {// comment text on the first line:= make([]byte, len()):= 2 // start after opening /*for < len() && [] <= ' ' {[] = []++}if > 2 && [2] == '\t' {// assume the '\t' compensates for the /*= [2:]} else {// otherwise assume two blanks[0], [1] = ' ', ' '= [0:]}// Shorten the computed common prefix by the length of// suffix, if it is found as suffix of the prefix.= strings.TrimSuffix(, string())}}// Handle last line: If it only contains a closing */, align it// with the opening /*, otherwise align the text with the other// lines.:= [len()-1]:= "*/", , := strings.Cut(, ) // closing always presentif isBlank() {// last line only contains closing */if {= " */" // add blank to align final star}[len()-1] = +} else {// last line contains more comment text - assume// it is aligned like the other lines and include// in prefix computation= commonPrefix(, )}// Remove the common prefix from all but the first and empty lines.for , := range {if > 0 && != "" {[] = [len():]}}}func ( *printer) ( *ast.Comment) {:= .Text:= .posFor(.Pos())const = "//line "if strings.HasPrefix(, ) && (!.IsValid() || .Column == 1) {// Possibly a //-style line directive.// Suspend indentation temporarily to keep line directive valid.defer func( int) { .indent = }(.indent).indent = 0}// shortcut common case of //-style commentsif [1] == '/' {if constraint.IsGoBuild() {.goBuild = append(.goBuild, len(.output))} else if constraint.IsPlusBuild() {.plusBuild = append(.plusBuild, len(.output))}.writeString(, trimRight(), true)return}// for /*-style comments, print line by line and let the// write function take care of the proper indentation:= strings.Split(, "\n")// The comment started in the first column but is going// to be indented. For an idempotent result, add indentation// to all lines such that they look like they were indented// before - this will make sure the common prefix computation// is the same independent of how many times formatting is// applied (was issue 1835).if .IsValid() && .Column == 1 && .indent > 0 {for , := range [1:] {[1+] = " " +}}stripCommonPrefix()// write comment lines, separated by formfeed,// without a line break after the last linefor , := range {if > 0 {.writeByte('\f', 1)= .pos}if len() > 0 {.writeString(, trimRight(), true)}}}// writeCommentSuffix writes a line break after a comment if indicated// and processes any leftover indentation information. If a line break// is needed, the kind of break (newline vs formfeed) depends on the// pending whitespace. The writeCommentSuffix result indicates if a// newline was written or if a formfeed was dropped from the whitespace// buffer.func ( *printer) ( bool) (, bool) {for , := range .wsbuf {switch {case blank, vtab:// ignore trailing whitespace.wsbuf[] = ignorecase indent, unindent:// don't lose indentation informationcase newline, formfeed:// if we need a line break, keep exactly one// but remember if we dropped any formfeedsif {= false= true} else {if == formfeed {= true}.wsbuf[] = ignore}}}.writeWhitespace(len(.wsbuf))// make sure we have a line breakif {.writeByte('\n', 1)= true}return}// containsLinebreak reports whether the whitespace buffer contains any line breaks.func ( *printer) () bool {for , := range .wsbuf {if == newline || == formfeed {return true}}return false}// intersperseComments consumes all comments that appear before the next token// tok and prints it together with the buffered whitespace (i.e., the whitespace// that needs to be written before the next token). A heuristic is used to mix// the comments and whitespace. The intersperseComments result indicates if a// newline was written or if a formfeed was dropped from the whitespace buffer.func ( *printer) ( token.Position, token.Token) (, bool) {var *ast.Commentfor .commentBefore() {:= .comment.List:= falseif .lastTok != token.IMPORT && // do not rewrite cgo's import "C" comments.posFor(.comment.Pos()).Column == 1 &&.posFor(.comment.End()+1) == {// Unindented comment abutting next token position:// a top-level doc comment.= formatDocComment()= trueif len(.comment.List) > 0 && len() == 0 {// The doc comment was removed entirely.// Keep preceding whitespace..writeCommentPrefix(.posFor(.comment.Pos()), , , )// Change print state to continue at next..pos =.last =// There can't be any more comments..nextComment()return .writeCommentSuffix(false)}}for , := range {.writeCommentPrefix(.posFor(.Pos()), , , ).writeComment()=}// In case list was rewritten, change print state to where// the original list would have ended.if len(.comment.List) > 0 && {= .comment.List[len(.comment.List)-1].pos = .posFor(.End()).last = .pos}.nextComment()}if != nil {// If the last comment is a /*-style comment and the next item// follows on the same line but is not a comma, and not a "closing"// token immediately following its corresponding "opening" token,// add an extra separator unless explicitly disabled. Use a blank// as separator unless we have pending linebreaks, they are not// disabled, and we are outside a composite literal, in which case// we want a linebreak (issue 15137).// TODO(gri) This has become overly complicated. We should be able// to track whether we're inside an expression or statement and// use that information to decide more directly.:= falseif .mode&noExtraBlank == 0 &&.Text[1] == '*' && .lineFor(.Pos()) == .Line &&!= token.COMMA &&( != token.RPAREN || .prevOpen == token.LPAREN) &&( != token.RBRACK || .prevOpen == token.LBRACK) {if .containsLinebreak() && .mode&noExtraLinebreak == 0 && .level == 0 {= true} else {.writeByte(' ', 1)}}// Ensure that there is a line break after a //-style comment,// before EOF, and before a closing '}' unless explicitly disabled.if .Text[1] == '/' ||== token.EOF ||== token.RBRACE && .mode&noExtraLinebreak == 0 {= true}return .writeCommentSuffix()}// no comment was written - we should never reach here since// intersperseComments should not be called in that case.internalError("intersperseComments called without pending comments")return}// writeWhitespace writes the first n whitespace entries.func ( *printer) ( int) {// write entriesfor := 0; < ; ++ {switch := .wsbuf[]; {case ignore:// ignore!case indent:.indent++case unindent:.indent--if .indent < 0 {.internalError("negative indentation:", .indent).indent = 0}case newline, formfeed:// A line break immediately followed by a "correcting"// unindent is swapped with the unindent - this permits// proper label positioning. If a comment is between// the line break and the label, the unindent is not// part of the comment whitespace prefix and the comment// will be positioned correctly indented.if +1 < && .wsbuf[+1] == unindent {// Use a formfeed to terminate the current section.// Otherwise, a long label name on the next line leading// to a wide column may increase the indentation column// of lines before the label; effectively leading to wrong// indentation..wsbuf[], .wsbuf[+1] = unindent, formfeed-- // do it againcontinue}fallthroughdefault:.writeByte(byte(), 1)}}// shift remaining entries down:= copy(.wsbuf, .wsbuf[:]).wsbuf = .wsbuf[:]}// ----------------------------------------------------------------------------// Printing interface// nlimit limits n to maxNewlines.func ( int) int {return min(, maxNewlines)}func ( token.Token, byte) ( bool) {switch {case token.INT:= == '.' // 1.case token.ADD:= == '+' // ++case token.SUB:= == '-' // --case token.QUO:= == '*' // /*case token.LSS:= == '-' || == '<' // <- or <<case token.AND:= == '&' || == '^' // && or &^}return}func ( *printer) ( token.Pos) {if .IsValid() {.pos = .posFor() // accurate position of next item}}// print prints a list of "items" (roughly corresponding to syntactic// tokens, but also including whitespace and formatting information).// It is the only print function that should be called directly from// any of the AST printing functions in nodes.go.//// Whitespace is accumulated until a non-whitespace token appears. Any// comments that need to appear before that token are printed first,// taking into account the amount and structure of any pending white-// space for best comment placement. Then, any leftover whitespace is// printed, followed by the actual token.func ( *printer) ( ...any) {for , := range {// information about the current argvar stringvar boolvar bool // value for p.impliedSemi after this arg// record previous opening token, if anyswitch .lastTok {case token.ILLEGAL:// ignore (white space)case token.LPAREN, token.LBRACK:.prevOpen = .lastTokdefault:// other tokens followed any opening token.prevOpen = token.ILLEGAL}switch x := .(type) {case pmode:// toggle printer mode.mode ^=continuecase whiteSpace:if == ignore {// don't add ignore's to the buffer; they// may screw up "correcting" unindents (see// LabeledStmt)continue}:= len(.wsbuf)if == cap(.wsbuf) {// Whitespace sequences are very short so this should// never happen. Handle gracefully (but possibly with// bad comment placement) if it does happen..writeWhitespace()= 0}.wsbuf = .wsbuf[0 : +1].wsbuf[] =if == newline || == formfeed {// newlines affect the current state (p.impliedSemi)// and not the state after printing arg (impliedSemi)// because comments can be interspersed before the arg// in this case.impliedSemi = false}.lastTok = token.ILLEGALcontinuecase *ast.Ident:= .Name= true.lastTok = token.IDENTcase *ast.BasicLit:= .Value= true= true.lastTok = .Kindcase token.Token::= .String()if mayCombine(.lastTok, [0]) {// the previous and the current token must be// separated by a blank otherwise they combine// into a different incorrect token sequence// (except for token.INT followed by a '.' this// should never happen because it is taken care// of via binary expression formatting)if len(.wsbuf) != 0 {.internalError("whitespace buffer not empty")}.wsbuf = .wsbuf[0:1].wsbuf[0] = ' '}=// some keywords followed by a newline imply a semicolonswitch {case token.BREAK, token.CONTINUE, token.FALLTHROUGH, token.RETURN,token.INC, token.DEC, token.RPAREN, token.RBRACK, token.RBRACE:= true}.lastTok =case string:// incorrect AST - print error message== true= true.lastTok = token.STRINGdefault:fmt.Fprintf(os.Stderr, "print: unsupported argument %v (%T)\n", , )panic("go/printer type")}// data != "":= .pos // estimated/accurate position of next item, := .flush(, .lastTok)// intersperse extra newlines if present in the source and// if they don't cause extra semicolons (don't do this in// flush as it will cause extra newlines at the end of a file)if !.impliedSemi {:= nlimit(.Line - .pos.Line)// don't exceed maxNewlines if we already wrote oneif && == maxNewlines {= maxNewlines - 1}if > 0 {:= byte('\n')if {= '\f' // use formfeed since we dropped one before}.writeByte(, )= false}}// the next token starts now - record its line number if requestedif .linePtr != nil {*.linePtr = .out.Line.linePtr = nil}.writeString(, , ).impliedSemi =}}// flush prints any pending comments and whitespace occurring textually// before the position of the next token tok. The flush result indicates// if a newline was written or if a formfeed was dropped from the whitespace// buffer.func ( *printer) ( token.Position, token.Token) (, bool) {if .commentBefore() {// if there are comments before the next item, intersperse them, = .intersperseComments(, )} else {// otherwise, write any leftover whitespace.writeWhitespace(len(.wsbuf))}return}// getDoc returns the ast.CommentGroup associated with n, if any.func ( ast.Node) *ast.CommentGroup {switch n := .(type) {case *ast.Field:return .Doccase *ast.ImportSpec:return .Doccase *ast.ValueSpec:return .Doccase *ast.TypeSpec:return .Doccase *ast.GenDecl:return .Doccase *ast.FuncDecl:return .Doccase *ast.File:return .Doc}return nil}func ( ast.Node) *ast.CommentGroup {switch n := .(type) {case *ast.Field:return .Commentcase *ast.ImportSpec:return .Commentcase *ast.ValueSpec:return .Commentcase *ast.TypeSpec:return .Commentcase *ast.GenDecl:if len(.Specs) > 0 {return (.Specs[len(.Specs)-1])}case *ast.File:if len(.Comments) > 0 {return .Comments[len(.Comments)-1]}}return nil}func ( *printer) ( any) error {// unpack *CommentedNode, if anyvar []*ast.CommentGroupif , := .(*CommentedNode); {= .Node= .Comments}if != nil {// commented node - restrict comment list to relevant range, := .(ast.Node)if ! {goto}:= .Pos():= .End()// if the node has associated documentation,// include that commentgroup in the range// (the comment list is sorted in the order// of the comment appearance in the source code)if := getDoc(); != nil {= .Pos()}if := getLastComment(); != nil {if := .End(); > {=}}// token.Pos values are global offsets, we can// compare them directly:= 0for < len() && [].End() < {++}:=for < len() && [].Pos() < {++}if < {.comments = [:]}} else if , := .(*ast.File); {// use ast.File comments, if any.comments = .Comments}// if there are no comments, use node comments.useNodeComments = .comments == nil// get comments ready for use.nextComment().print(pmode(0))// format nodeswitch n := .(type) {case ast.Expr:.expr()case ast.Stmt:// A labeled statement will un-indent to position the label.// Set p.indent to 1 so we don't get indent "underflow".if , := .(*ast.LabeledStmt); {.indent = 1}.stmt(, false)case ast.Decl:.decl()case ast.Spec:.spec(, 1, false)case []ast.Stmt:// A labeled statement will un-indent to position the label.// Set p.indent to 1 so we don't get indent "underflow".for , := range {if , := .(*ast.LabeledStmt); {.indent = 1}}.stmtList(, 0, false)case []ast.Decl:.declList()case *ast.File:.file()default:goto}return .sourcePosErr:return fmt.Errorf("go/printer: unsupported node type %T", )}// ----------------------------------------------------------------------------// Trimmer// A trimmer is an io.Writer filter for stripping tabwriter.Escape// characters, trailing blanks and tabs, and for converting formfeed// and vtab characters into newlines and htabs (in case no tabwriter// is used). Text bracketed by tabwriter.Escape characters is passed// through unchanged.type trimmer struct {output io.Writerstate intspace []byte}// trimmer is implemented as a state machine.// It can be in one of the following states:const (inSpace = iota // inside spaceinEscape // inside text bracketed by tabwriter.EscapesinText // inside text)func ( *trimmer) () {.state = inSpace.space = .space[0:0]}// Design note: It is tempting to eliminate extra blanks occurring in// whitespace in this function as it could simplify some// of the blanks logic in the node printing functions.// However, this would mess up any formatting done by// the tabwriter.var aNewline = []byte("\n")func ( *trimmer) ( []byte) ( int, error) {// invariants:// p.state == inSpace:// p.space is unwritten// p.state == inEscape, inText:// data[m:n] is unwritten:= 0var bytefor , = range {if == '\v' {= '\t' // convert to htab}switch .state {case inSpace:switch {case '\t', ' ':.space = append(.space, )case '\n', '\f':.resetSpace() // discard trailing space_, = .output.Write(aNewline)case tabwriter.Escape:_, = .output.Write(.space).state = inEscape= + 1 // +1: skip tabwriter.Escapedefault:_, = .output.Write(.space).state = inText=}case inEscape:if == tabwriter.Escape {_, = .output.Write([:]).resetSpace()}case inText:switch {case '\t', ' ':_, = .output.Write([:]).resetSpace().space = append(.space, )case '\n', '\f':_, = .output.Write([:]).resetSpace()if == nil {_, = .output.Write(aNewline)}case tabwriter.Escape:_, = .output.Write([:]).state = inEscape= + 1 // +1: skip tabwriter.Escape}default:panic("unreachable")}if != nil {return}}= len()switch .state {case inEscape, inText:_, = .output.Write([:]).resetSpace()}return}// ----------------------------------------------------------------------------// Public interface// A Mode value is a set of flags (or 0). They control printing.type Mode uintconst (RawFormat Mode = 1 << iota // do not use a tabwriter; if set, UseSpaces is ignoredTabIndent // use tabs for indentation independent of UseSpacesUseSpaces // use spaces instead of tabs for alignmentSourcePos // emit //line directives to preserve original source positions)// The mode below is not included in printer's public API because// editing code text is deemed out of scope. Because this mode is// unexported, it's also possible to modify or remove it based on// the evolving needs of go/format and cmd/gofmt without breaking// users. See discussion in CL 240683.const (// normalizeNumbers means to canonicalize number// literal prefixes and exponents while printing.//// This value is known in and used by go/format and cmd/gofmt.// It is currently more convenient and performant for those// packages to apply number normalization during printing,// rather than by modifying the AST in advance.normalizeNumbers Mode = 1 << 30)// A Config node controls the output of Fprint.type Config struct {Mode Mode // default: 0Tabwidth int // default: 8Indent int // default: 0 (all code is indented at least by this much)}var printerPool = sync.Pool{New: func() any {return &printer{// Whitespace sequences are short.wsbuf: make([]whiteSpace, 0, 16),// We start the printer with a 16K output buffer, which is currently// larger than about 80% of Go files in the standard library.output: make([]byte, 0, 16<<10),}},}func ( *Config, *token.FileSet, map[ast.Node]int) *printer {:= printerPool.Get().(*printer)* = printer{Config: *,fset: ,pos: token.Position{Line: 1, Column: 1},out: token.Position{Line: 1, Column: 1},wsbuf: .wsbuf[:0],nodeSizes: ,cachedPos: -1,output: .output[:0],}return}func ( *printer) () {// Hard limit on buffer size; see https://golang.org/issue/23199.if cap(.output) > 64<<10 {return}printerPool.Put()}// fprint implements Fprint and takes a nodesSizes map for setting up the printer state.func ( *Config) ( io.Writer, *token.FileSet, any, map[ast.Node]int) ( error) {// print node:= newPrinter(, , )defer .free()if = .printNode(); != nil {return}// print outstanding comments.impliedSemi = false // EOF acts like a newline.flush(token.Position{Offset: infinity, Line: infinity}, token.EOF)// output is buffered in p.output now.// fix //go:build and // +build comments if needed..fixGoBuildLines()// redirect output through a trimmer to eliminate trailing whitespace// (Input to a tabwriter must be untrimmed since trailing tabs provide// formatting information. The tabwriter could provide trimming// functionality but no tabwriter is used when RawFormat is set.)= &trimmer{output: }// redirect output through a tabwriter if necessaryif .Mode&RawFormat == 0 {:= .Tabwidth:= byte('\t')if .Mode&UseSpaces != 0 {= ' '}:= tabwriter.DiscardEmptyColumnsif .Mode&TabIndent != 0 {= 0|= tabwriter.TabIndent}= tabwriter.NewWriter(, , .Tabwidth, 1, , )}// write printer result via tabwriter/trimmer to outputif _, = .Write(.output); != nil {return}// flush tabwriter, if anyif , := .(*tabwriter.Writer); != nil {= .Flush()}return}// A CommentedNode bundles an AST node and corresponding comments.// It may be provided as argument to any of the [Fprint] functions.type CommentedNode struct {Node any // *ast.File, or ast.Expr, ast.Decl, ast.Spec, or ast.StmtComments []*ast.CommentGroup}// Fprint "pretty-prints" an AST node to output for a given configuration cfg.// Position information is interpreted relative to the file set fset.// The node type must be *[ast.File], *[CommentedNode], [][ast.Decl], [][ast.Stmt],// or assignment-compatible to [ast.Expr], [ast.Decl], [ast.Spec], or [ast.Stmt].func ( *Config) ( io.Writer, *token.FileSet, any) error {return .fprint(, , , make(map[ast.Node]int))}// Fprint "pretty-prints" an AST node to output.// It calls [Config.Fprint] with default settings.// Note that gofmt uses tabs for indentation but spaces for alignment;// use format.Node (package go/format) for output that matches gofmt.func ( io.Writer, *token.FileSet, any) error {return (&Config{Tabwidth: 8}).Fprint(, , )}
The pages are generated with Golds v0.7.6. (GOOS=linux GOARCH=amd64)