github.com/bir3/gocompiler@v0.3.205/src/go/printer/printer.go (about) 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. 4 5 // Package printer implements printing of AST nodes. 6 package printer 7 8 import ( 9 "fmt" 10 "github.com/bir3/gocompiler/src/go/ast" 11 "github.com/bir3/gocompiler/src/go/build/constraint" 12 "github.com/bir3/gocompiler/src/go/token" 13 "io" 14 "os" 15 "strings" 16 "sync" 17 "text/tabwriter" 18 "unicode" 19 ) 20 21 const ( 22 maxNewlines = 2 // max. number of newlines between source text 23 debug = false // enable for debugging 24 infinity = 1 << 30 25 ) 26 27 type whiteSpace byte 28 29 const ( 30 ignore = whiteSpace(0) 31 blank = whiteSpace(' ') 32 vtab = whiteSpace('\v') 33 newline = whiteSpace('\n') 34 formfeed = whiteSpace('\f') 35 indent = whiteSpace('>') 36 unindent = whiteSpace('<') 37 ) 38 39 // A pmode value represents the current printer mode. 40 type pmode int 41 42 const ( 43 noExtraBlank pmode = 1 << iota // disables extra blank after /*-style comment 44 noExtraLinebreak // disables extra line break after /*-style comment 45 ) 46 47 type commentInfo struct { 48 cindex int // current comment index 49 comment *ast.CommentGroup // = printer.comments[cindex]; or nil 50 commentOffset int // = printer.posFor(printer.comments[cindex].List[0].Pos()).Offset; or infinity 51 commentNewline bool // true if the comment group contains newlines 52 } 53 54 type printer struct { 55 // Configuration (does not change after initialization) 56 Config 57 fset *token.FileSet 58 59 // Current state 60 output []byte // raw printer result 61 indent int // current indentation 62 level int // level == 0: outside composite literal; level > 0: inside composite literal 63 mode pmode // current printer mode 64 endAlignment bool // if set, terminate alignment immediately 65 impliedSemi bool // if set, a linebreak implies a semicolon 66 lastTok token.Token // last token printed (token.ILLEGAL if it's whitespace) 67 prevOpen token.Token // previous non-brace "open" token (, [, or token.ILLEGAL 68 wsbuf []whiteSpace // delayed white space 69 goBuild []int // start index of all //go:build comments in output 70 plusBuild []int // start index of all // +build comments in output 71 72 // Positions 73 // The out position differs from the pos position when the result 74 // formatting differs from the source formatting (in the amount of 75 // white space). If there's a difference and SourcePos is set in 76 // ConfigMode, //line directives are used in the output to restore 77 // original source positions for a reader. 78 pos token.Position // current position in AST (source) space 79 out token.Position // current position in output space 80 last token.Position // value of pos after calling writeString 81 linePtr *int // if set, record out.Line for the next token in *linePtr 82 83 // The list of all source comments, in order of appearance. 84 comments []*ast.CommentGroup // may be nil 85 useNodeComments bool // if not set, ignore lead and line comments of nodes 86 87 // Information about p.comments[p.cindex]; set up by nextComment. 88 commentInfo 89 90 // Cache of already computed node sizes. 91 nodeSizes map[ast.Node]int 92 93 // Cache of most recently computed line position. 94 cachedPos token.Pos 95 cachedLine int // line corresponding to cachedPos 96 } 97 98 func (p *printer) internalError(msg ...any) { 99 if debug { 100 fmt.Print(p.pos.String() + ": ") 101 fmt.Println(msg...) 102 panic("go/printer") 103 } 104 } 105 106 // commentsHaveNewline reports whether a list of comments belonging to 107 // an *ast.CommentGroup contains newlines. Because the position information 108 // may only be partially correct, we also have to read the comment text. 109 func (p *printer) commentsHaveNewline(list []*ast.Comment) bool { 110 // len(list) > 0 111 line := p.lineFor(list[0].Pos()) 112 for i, c := range list { 113 if i > 0 && p.lineFor(list[i].Pos()) != line { 114 // not all comments on the same line 115 return true 116 } 117 if t := c.Text; len(t) >= 2 && (t[1] == '/' || strings.Contains(t, "\n")) { 118 return true 119 } 120 } 121 _ = line 122 return false 123 } 124 125 func (p *printer) nextComment() { 126 for p.cindex < len(p.comments) { 127 c := p.comments[p.cindex] 128 p.cindex++ 129 if list := c.List; len(list) > 0 { 130 p.comment = c 131 p.commentOffset = p.posFor(list[0].Pos()).Offset 132 p.commentNewline = p.commentsHaveNewline(list) 133 return 134 } 135 // we should not reach here (correct ASTs don't have empty 136 // ast.CommentGroup nodes), but be conservative and try again 137 } 138 // no more comments 139 p.commentOffset = infinity 140 } 141 142 // commentBefore reports whether the current comment group occurs 143 // before the next position in the source code and printing it does 144 // not introduce implicit semicolons. 145 func (p *printer) commentBefore(next token.Position) bool { 146 return p.commentOffset < next.Offset && (!p.impliedSemi || !p.commentNewline) 147 } 148 149 // commentSizeBefore returns the estimated size of the 150 // comments on the same line before the next position. 151 func (p *printer) commentSizeBefore(next token.Position) int { 152 // save/restore current p.commentInfo (p.nextComment() modifies it) 153 defer func(info commentInfo) { 154 p.commentInfo = info 155 }(p.commentInfo) 156 157 size := 0 158 for p.commentBefore(next) { 159 for _, c := range p.comment.List { 160 size += len(c.Text) 161 } 162 p.nextComment() 163 } 164 return size 165 } 166 167 // recordLine records the output line number for the next non-whitespace 168 // token in *linePtr. It is used to compute an accurate line number for a 169 // formatted construct, independent of pending (not yet emitted) whitespace 170 // or comments. 171 func (p *printer) recordLine(linePtr *int) { 172 p.linePtr = linePtr 173 } 174 175 // linesFrom returns the number of output lines between the current 176 // output line and the line argument, ignoring any pending (not yet 177 // emitted) whitespace or comments. It is used to compute an accurate 178 // size (in number of lines) for a formatted construct. 179 func (p *printer) linesFrom(line int) int { 180 return p.out.Line - line 181 } 182 183 func (p *printer) posFor(pos token.Pos) token.Position { 184 // not used frequently enough to cache entire token.Position 185 return p.fset.PositionFor(pos, false /* absolute position */) 186 } 187 188 func (p *printer) lineFor(pos token.Pos) int { 189 if pos != p.cachedPos { 190 p.cachedPos = pos 191 p.cachedLine = p.fset.PositionFor(pos, false /* absolute position */).Line 192 } 193 return p.cachedLine 194 } 195 196 // writeLineDirective writes a //line directive if necessary. 197 func (p *printer) writeLineDirective(pos token.Position) { 198 if pos.IsValid() && (p.out.Line != pos.Line || p.out.Filename != pos.Filename) { 199 p.output = append(p.output, tabwriter.Escape) // protect '\n' in //line from tabwriter interpretation 200 p.output = append(p.output, fmt.Sprintf("//line %s:%d\n", pos.Filename, pos.Line)...) 201 p.output = append(p.output, tabwriter.Escape) 202 // p.out must match the //line directive 203 p.out.Filename = pos.Filename 204 p.out.Line = pos.Line 205 } 206 } 207 208 // writeIndent writes indentation. 209 func (p *printer) writeIndent() { 210 // use "hard" htabs - indentation columns 211 // must not be discarded by the tabwriter 212 n := p.Config.Indent + p.indent // include base indentation 213 for i := 0; i < n; i++ { 214 p.output = append(p.output, '\t') 215 } 216 217 // update positions 218 p.pos.Offset += n 219 p.pos.Column += n 220 p.out.Column += n 221 } 222 223 // writeByte writes ch n times to p.output and updates p.pos. 224 // Only used to write formatting (white space) characters. 225 func (p *printer) writeByte(ch byte, n int) { 226 if p.endAlignment { 227 // Ignore any alignment control character; 228 // and at the end of the line, break with 229 // a formfeed to indicate termination of 230 // existing columns. 231 switch ch { 232 case '\t', '\v': 233 ch = ' ' 234 case '\n', '\f': 235 ch = '\f' 236 p.endAlignment = false 237 } 238 } 239 240 if p.out.Column == 1 { 241 // no need to write line directives before white space 242 p.writeIndent() 243 } 244 245 for i := 0; i < n; i++ { 246 p.output = append(p.output, ch) 247 } 248 249 // update positions 250 p.pos.Offset += n 251 if ch == '\n' || ch == '\f' { 252 p.pos.Line += n 253 p.out.Line += n 254 p.pos.Column = 1 255 p.out.Column = 1 256 return 257 } 258 p.pos.Column += n 259 p.out.Column += n 260 } 261 262 // writeString writes the string s to p.output and updates p.pos, p.out, 263 // and p.last. If isLit is set, s is escaped w/ tabwriter.Escape characters 264 // to protect s from being interpreted by the tabwriter. 265 // 266 // Note: writeString is only used to write Go tokens, literals, and 267 // comments, all of which must be written literally. Thus, it is correct 268 // to always set isLit = true. However, setting it explicitly only when 269 // needed (i.e., when we don't know that s contains no tabs or line breaks) 270 // avoids processing extra escape characters and reduces run time of the 271 // printer benchmark by up to 10%. 272 func (p *printer) writeString(pos token.Position, s string, isLit bool) { 273 if p.out.Column == 1 { 274 if p.Config.Mode&SourcePos != 0 { 275 p.writeLineDirective(pos) 276 } 277 p.writeIndent() 278 } 279 280 if pos.IsValid() { 281 // update p.pos (if pos is invalid, continue with existing p.pos) 282 // Note: Must do this after handling line beginnings because 283 // writeIndent updates p.pos if there's indentation, but p.pos 284 // is the position of s. 285 p.pos = pos 286 } 287 288 if isLit { 289 // Protect s such that is passes through the tabwriter 290 // unchanged. Note that valid Go programs cannot contain 291 // tabwriter.Escape bytes since they do not appear in legal 292 // UTF-8 sequences. 293 p.output = append(p.output, tabwriter.Escape) 294 } 295 296 if debug { 297 p.output = append(p.output, fmt.Sprintf("/*%s*/", pos)...) // do not update p.pos! 298 } 299 p.output = append(p.output, s...) 300 301 // update positions 302 nlines := 0 303 var li int // index of last newline; valid if nlines > 0 304 for i := 0; i < len(s); i++ { 305 // Raw string literals may contain any character except back quote (`). 306 if ch := s[i]; ch == '\n' || ch == '\f' { 307 // account for line break 308 nlines++ 309 li = i 310 // A line break inside a literal will break whatever column 311 // formatting is in place; ignore any further alignment through 312 // the end of the line. 313 p.endAlignment = true 314 } 315 } 316 p.pos.Offset += len(s) 317 if nlines > 0 { 318 p.pos.Line += nlines 319 p.out.Line += nlines 320 c := len(s) - li 321 p.pos.Column = c 322 p.out.Column = c 323 } else { 324 p.pos.Column += len(s) 325 p.out.Column += len(s) 326 } 327 328 if isLit { 329 p.output = append(p.output, tabwriter.Escape) 330 } 331 332 p.last = p.pos 333 } 334 335 // writeCommentPrefix writes the whitespace before a comment. 336 // If there is any pending whitespace, it consumes as much of 337 // it as is likely to help position the comment nicely. 338 // pos is the comment position, next the position of the item 339 // after all pending comments, prev is the previous comment in 340 // a group of comments (or nil), and tok is the next token. 341 func (p *printer) writeCommentPrefix(pos, next token.Position, prev *ast.Comment, tok token.Token) { 342 if len(p.output) == 0 { 343 // the comment is the first item to be printed - don't write any whitespace 344 return 345 } 346 347 if pos.IsValid() && pos.Filename != p.last.Filename { 348 // comment in a different file - separate with newlines 349 p.writeByte('\f', maxNewlines) 350 return 351 } 352 353 if pos.Line == p.last.Line && (prev == nil || prev.Text[1] != '/') { 354 // comment on the same line as last item: 355 // separate with at least one separator 356 hasSep := false 357 if prev == nil { 358 // first comment of a comment group 359 j := 0 360 for i, ch := range p.wsbuf { 361 switch ch { 362 case blank: 363 // ignore any blanks before a comment 364 p.wsbuf[i] = ignore 365 continue 366 case vtab: 367 // respect existing tabs - important 368 // for proper formatting of commented structs 369 hasSep = true 370 continue 371 case indent: 372 // apply pending indentation 373 continue 374 } 375 j = i 376 break 377 } 378 p.writeWhitespace(j) 379 } 380 // make sure there is at least one separator 381 if !hasSep { 382 sep := byte('\t') 383 if pos.Line == next.Line { 384 // next item is on the same line as the comment 385 // (which must be a /*-style comment): separate 386 // with a blank instead of a tab 387 sep = ' ' 388 } 389 p.writeByte(sep, 1) 390 } 391 392 } else { 393 // comment on a different line: 394 // separate with at least one line break 395 droppedLinebreak := false 396 j := 0 397 for i, ch := range p.wsbuf { 398 switch ch { 399 case blank, vtab: 400 // ignore any horizontal whitespace before line breaks 401 p.wsbuf[i] = ignore 402 continue 403 case indent: 404 // apply pending indentation 405 continue 406 case unindent: 407 // if this is not the last unindent, apply it 408 // as it is (likely) belonging to the last 409 // construct (e.g., a multi-line expression list) 410 // and is not part of closing a block 411 if i+1 < len(p.wsbuf) && p.wsbuf[i+1] == unindent { 412 continue 413 } 414 // if the next token is not a closing }, apply the unindent 415 // if it appears that the comment is aligned with the 416 // token; otherwise assume the unindent is part of a 417 // closing block and stop (this scenario appears with 418 // comments before a case label where the comments 419 // apply to the next case instead of the current one) 420 if tok != token.RBRACE && pos.Column == next.Column { 421 continue 422 } 423 case newline, formfeed: 424 p.wsbuf[i] = ignore 425 droppedLinebreak = prev == nil // record only if first comment of a group 426 } 427 j = i 428 break 429 } 430 p.writeWhitespace(j) 431 432 // determine number of linebreaks before the comment 433 n := 0 434 if pos.IsValid() && p.last.IsValid() { 435 n = pos.Line - p.last.Line 436 if n < 0 { // should never happen 437 n = 0 438 } 439 } 440 441 // at the package scope level only (p.indent == 0), 442 // add an extra newline if we dropped one before: 443 // this preserves a blank line before documentation 444 // comments at the package scope level (issue 2570) 445 if p.indent == 0 && droppedLinebreak { 446 n++ 447 } 448 449 // make sure there is at least one line break 450 // if the previous comment was a line comment 451 if n == 0 && prev != nil && prev.Text[1] == '/' { 452 n = 1 453 } 454 455 if n > 0 { 456 // use formfeeds to break columns before a comment; 457 // this is analogous to using formfeeds to separate 458 // individual lines of /*-style comments 459 p.writeByte('\f', nlimit(n)) 460 } 461 } 462 } 463 464 // Returns true if s contains only white space 465 // (only tabs and blanks can appear in the printer's context). 466 func isBlank(s string) bool { 467 for i := 0; i < len(s); i++ { 468 if s[i] > ' ' { 469 return false 470 } 471 } 472 return true 473 } 474 475 // commonPrefix returns the common prefix of a and b. 476 func commonPrefix(a, b string) string { 477 i := 0 478 for i < len(a) && i < len(b) && a[i] == b[i] && (a[i] <= ' ' || a[i] == '*') { 479 i++ 480 } 481 return a[0:i] 482 } 483 484 // trimRight returns s with trailing whitespace removed. 485 func trimRight(s string) string { 486 return strings.TrimRightFunc(s, unicode.IsSpace) 487 } 488 489 // stripCommonPrefix removes a common prefix from /*-style comment lines (unless no 490 // comment line is indented, all but the first line have some form of space prefix). 491 // The prefix is computed using heuristics such that is likely that the comment 492 // contents are nicely laid out after re-printing each line using the printer's 493 // current indentation. 494 func stripCommonPrefix(lines []string) { 495 if len(lines) <= 1 { 496 return // at most one line - nothing to do 497 } 498 // len(lines) > 1 499 500 // The heuristic in this function tries to handle a few 501 // common patterns of /*-style comments: Comments where 502 // the opening /* and closing */ are aligned and the 503 // rest of the comment text is aligned and indented with 504 // blanks or tabs, cases with a vertical "line of stars" 505 // on the left, and cases where the closing */ is on the 506 // same line as the last comment text. 507 508 // Compute maximum common white prefix of all but the first, 509 // last, and blank lines, and replace blank lines with empty 510 // lines (the first line starts with /* and has no prefix). 511 // In cases where only the first and last lines are not blank, 512 // such as two-line comments, or comments where all inner lines 513 // are blank, consider the last line for the prefix computation 514 // since otherwise the prefix would be empty. 515 // 516 // Note that the first and last line are never empty (they 517 // contain the opening /* and closing */ respectively) and 518 // thus they can be ignored by the blank line check. 519 prefix := "" 520 prefixSet := false 521 if len(lines) > 2 { 522 for i, line := range lines[1 : len(lines)-1] { 523 if isBlank(line) { 524 lines[1+i] = "" // range starts with lines[1] 525 } else { 526 if !prefixSet { 527 prefix = line 528 prefixSet = true 529 } 530 prefix = commonPrefix(prefix, line) 531 } 532 533 } 534 } 535 // If we don't have a prefix yet, consider the last line. 536 if !prefixSet { 537 line := lines[len(lines)-1] 538 prefix = commonPrefix(line, line) 539 } 540 541 /* 542 * Check for vertical "line of stars" and correct prefix accordingly. 543 */ 544 lineOfStars := false 545 if p, _, ok := strings.Cut(prefix, "*"); ok { 546 // remove trailing blank from prefix so stars remain aligned 547 prefix = strings.TrimSuffix(p, " ") 548 lineOfStars = true 549 } else { 550 // No line of stars present. 551 // Determine the white space on the first line after the /* 552 // and before the beginning of the comment text, assume two 553 // blanks instead of the /* unless the first character after 554 // the /* is a tab. If the first comment line is empty but 555 // for the opening /*, assume up to 3 blanks or a tab. This 556 // whitespace may be found as suffix in the common prefix. 557 first := lines[0] 558 if isBlank(first[2:]) { 559 // no comment text on the first line: 560 // reduce prefix by up to 3 blanks or a tab 561 // if present - this keeps comment text indented 562 // relative to the /* and */'s if it was indented 563 // in the first place 564 i := len(prefix) 565 for n := 0; n < 3 && i > 0 && prefix[i-1] == ' '; n++ { 566 i-- 567 } 568 if i == len(prefix) && i > 0 && prefix[i-1] == '\t' { 569 i-- 570 } 571 prefix = prefix[0:i] 572 } else { 573 // comment text on the first line 574 suffix := make([]byte, len(first)) 575 n := 2 // start after opening /* 576 for n < len(first) && first[n] <= ' ' { 577 suffix[n] = first[n] 578 n++ 579 } 580 if n > 2 && suffix[2] == '\t' { 581 // assume the '\t' compensates for the /* 582 suffix = suffix[2:n] 583 } else { 584 // otherwise assume two blanks 585 suffix[0], suffix[1] = ' ', ' ' 586 suffix = suffix[0:n] 587 } 588 // Shorten the computed common prefix by the length of 589 // suffix, if it is found as suffix of the prefix. 590 prefix = strings.TrimSuffix(prefix, string(suffix)) 591 } 592 } 593 594 // Handle last line: If it only contains a closing */, align it 595 // with the opening /*, otherwise align the text with the other 596 // lines. 597 last := lines[len(lines)-1] 598 closing := "*/" 599 before, _, _ := strings.Cut(last, closing) // closing always present 600 if isBlank(before) { 601 // last line only contains closing */ 602 if lineOfStars { 603 closing = " */" // add blank to align final star 604 } 605 lines[len(lines)-1] = prefix + closing 606 } else { 607 // last line contains more comment text - assume 608 // it is aligned like the other lines and include 609 // in prefix computation 610 prefix = commonPrefix(prefix, last) 611 } 612 613 // Remove the common prefix from all but the first and empty lines. 614 for i, line := range lines { 615 if i > 0 && line != "" { 616 lines[i] = line[len(prefix):] 617 } 618 } 619 } 620 621 func (p *printer) writeComment(comment *ast.Comment) { 622 text := comment.Text 623 pos := p.posFor(comment.Pos()) 624 625 const linePrefix = "//line " 626 if strings.HasPrefix(text, linePrefix) && (!pos.IsValid() || pos.Column == 1) { 627 // Possibly a //-style line directive. 628 // Suspend indentation temporarily to keep line directive valid. 629 defer func(indent int) { p.indent = indent }(p.indent) 630 p.indent = 0 631 } 632 633 // shortcut common case of //-style comments 634 if text[1] == '/' { 635 if constraint.IsGoBuild(text) { 636 p.goBuild = append(p.goBuild, len(p.output)) 637 } else if constraint.IsPlusBuild(text) { 638 p.plusBuild = append(p.plusBuild, len(p.output)) 639 } 640 p.writeString(pos, trimRight(text), true) 641 return 642 } 643 644 // for /*-style comments, print line by line and let the 645 // write function take care of the proper indentation 646 lines := strings.Split(text, "\n") 647 648 // The comment started in the first column but is going 649 // to be indented. For an idempotent result, add indentation 650 // to all lines such that they look like they were indented 651 // before - this will make sure the common prefix computation 652 // is the same independent of how many times formatting is 653 // applied (was issue 1835). 654 if pos.IsValid() && pos.Column == 1 && p.indent > 0 { 655 for i, line := range lines[1:] { 656 lines[1+i] = " " + line 657 } 658 } 659 660 stripCommonPrefix(lines) 661 662 // write comment lines, separated by formfeed, 663 // without a line break after the last line 664 for i, line := range lines { 665 if i > 0 { 666 p.writeByte('\f', 1) 667 pos = p.pos 668 } 669 if len(line) > 0 { 670 p.writeString(pos, trimRight(line), true) 671 } 672 } 673 } 674 675 // writeCommentSuffix writes a line break after a comment if indicated 676 // and processes any leftover indentation information. If a line break 677 // is needed, the kind of break (newline vs formfeed) depends on the 678 // pending whitespace. The writeCommentSuffix result indicates if a 679 // newline was written or if a formfeed was dropped from the whitespace 680 // buffer. 681 func (p *printer) writeCommentSuffix(needsLinebreak bool) (wroteNewline, droppedFF bool) { 682 for i, ch := range p.wsbuf { 683 switch ch { 684 case blank, vtab: 685 // ignore trailing whitespace 686 p.wsbuf[i] = ignore 687 case indent, unindent: 688 // don't lose indentation information 689 case newline, formfeed: 690 // if we need a line break, keep exactly one 691 // but remember if we dropped any formfeeds 692 if needsLinebreak { 693 needsLinebreak = false 694 wroteNewline = true 695 } else { 696 if ch == formfeed { 697 droppedFF = true 698 } 699 p.wsbuf[i] = ignore 700 } 701 } 702 } 703 p.writeWhitespace(len(p.wsbuf)) 704 705 // make sure we have a line break 706 if needsLinebreak { 707 p.writeByte('\n', 1) 708 wroteNewline = true 709 } 710 711 return 712 } 713 714 // containsLinebreak reports whether the whitespace buffer contains any line breaks. 715 func (p *printer) containsLinebreak() bool { 716 for _, ch := range p.wsbuf { 717 if ch == newline || ch == formfeed { 718 return true 719 } 720 } 721 return false 722 } 723 724 // intersperseComments consumes all comments that appear before the next token 725 // tok and prints it together with the buffered whitespace (i.e., the whitespace 726 // that needs to be written before the next token). A heuristic is used to mix 727 // the comments and whitespace. The intersperseComments result indicates if a 728 // newline was written or if a formfeed was dropped from the whitespace buffer. 729 func (p *printer) intersperseComments(next token.Position, tok token.Token) (wroteNewline, droppedFF bool) { 730 var last *ast.Comment 731 for p.commentBefore(next) { 732 list := p.comment.List 733 changed := false 734 if p.lastTok != token.IMPORT && // do not rewrite cgo's import "C" comments 735 p.posFor(p.comment.Pos()).Column == 1 && 736 p.posFor(p.comment.End()+1) == next { 737 // Unindented comment abutting next token position: 738 // a top-level doc comment. 739 list = formatDocComment(list) 740 changed = true 741 742 if len(p.comment.List) > 0 && len(list) == 0 { 743 // The doc comment was removed entirely. 744 // Keep preceding whitespace. 745 p.writeCommentPrefix(p.posFor(p.comment.Pos()), next, last, tok) 746 // Change print state to continue at next. 747 p.pos = next 748 p.last = next 749 // There can't be any more comments. 750 p.nextComment() 751 return p.writeCommentSuffix(false) 752 } 753 } 754 for _, c := range list { 755 p.writeCommentPrefix(p.posFor(c.Pos()), next, last, tok) 756 p.writeComment(c) 757 last = c 758 } 759 // In case list was rewritten, change print state to where 760 // the original list would have ended. 761 if len(p.comment.List) > 0 && changed { 762 last = p.comment.List[len(p.comment.List)-1] 763 p.pos = p.posFor(last.End()) 764 p.last = p.pos 765 } 766 p.nextComment() 767 } 768 769 if last != nil { 770 // If the last comment is a /*-style comment and the next item 771 // follows on the same line but is not a comma, and not a "closing" 772 // token immediately following its corresponding "opening" token, 773 // add an extra separator unless explicitly disabled. Use a blank 774 // as separator unless we have pending linebreaks, they are not 775 // disabled, and we are outside a composite literal, in which case 776 // we want a linebreak (issue 15137). 777 // TODO(gri) This has become overly complicated. We should be able 778 // to track whether we're inside an expression or statement and 779 // use that information to decide more directly. 780 needsLinebreak := false 781 if p.mode&noExtraBlank == 0 && 782 last.Text[1] == '*' && p.lineFor(last.Pos()) == next.Line && 783 tok != token.COMMA && 784 (tok != token.RPAREN || p.prevOpen == token.LPAREN) && 785 (tok != token.RBRACK || p.prevOpen == token.LBRACK) { 786 if p.containsLinebreak() && p.mode&noExtraLinebreak == 0 && p.level == 0 { 787 needsLinebreak = true 788 } else { 789 p.writeByte(' ', 1) 790 } 791 } 792 // Ensure that there is a line break after a //-style comment, 793 // before EOF, and before a closing '}' unless explicitly disabled. 794 if last.Text[1] == '/' || 795 tok == token.EOF || 796 tok == token.RBRACE && p.mode&noExtraLinebreak == 0 { 797 needsLinebreak = true 798 } 799 return p.writeCommentSuffix(needsLinebreak) 800 } 801 802 // no comment was written - we should never reach here since 803 // intersperseComments should not be called in that case 804 p.internalError("intersperseComments called without pending comments") 805 return 806 } 807 808 // whiteWhitespace writes the first n whitespace entries. 809 func (p *printer) writeWhitespace(n int) { 810 // write entries 811 for i := 0; i < n; i++ { 812 switch ch := p.wsbuf[i]; ch { 813 case ignore: 814 // ignore! 815 case indent: 816 p.indent++ 817 case unindent: 818 p.indent-- 819 if p.indent < 0 { 820 p.internalError("negative indentation:", p.indent) 821 p.indent = 0 822 } 823 case newline, formfeed: 824 // A line break immediately followed by a "correcting" 825 // unindent is swapped with the unindent - this permits 826 // proper label positioning. If a comment is between 827 // the line break and the label, the unindent is not 828 // part of the comment whitespace prefix and the comment 829 // will be positioned correctly indented. 830 if i+1 < n && p.wsbuf[i+1] == unindent { 831 // Use a formfeed to terminate the current section. 832 // Otherwise, a long label name on the next line leading 833 // to a wide column may increase the indentation column 834 // of lines before the label; effectively leading to wrong 835 // indentation. 836 p.wsbuf[i], p.wsbuf[i+1] = unindent, formfeed 837 i-- // do it again 838 continue 839 } 840 fallthrough 841 default: 842 p.writeByte(byte(ch), 1) 843 } 844 } 845 846 // shift remaining entries down 847 l := copy(p.wsbuf, p.wsbuf[n:]) 848 p.wsbuf = p.wsbuf[:l] 849 } 850 851 // ---------------------------------------------------------------------------- 852 // Printing interface 853 854 // nlimit limits n to maxNewlines. 855 func nlimit(n int) int { 856 if n > maxNewlines { 857 n = maxNewlines 858 } 859 return n 860 } 861 862 func mayCombine(prev token.Token, next byte) (b bool) { 863 switch prev { 864 case token.INT: 865 b = next == '.' // 1. 866 case token.ADD: 867 b = next == '+' // ++ 868 case token.SUB: 869 b = next == '-' // -- 870 case token.QUO: 871 b = next == '*' // /* 872 case token.LSS: 873 b = next == '-' || next == '<' // <- or << 874 case token.AND: 875 b = next == '&' || next == '^' // && or &^ 876 } 877 return 878 } 879 880 func (p *printer) setPos(pos token.Pos) { 881 if pos.IsValid() { 882 p.pos = p.posFor(pos) // accurate position of next item 883 } 884 } 885 886 // print prints a list of "items" (roughly corresponding to syntactic 887 // tokens, but also including whitespace and formatting information). 888 // It is the only print function that should be called directly from 889 // any of the AST printing functions in nodes.go. 890 // 891 // Whitespace is accumulated until a non-whitespace token appears. Any 892 // comments that need to appear before that token are printed first, 893 // taking into account the amount and structure of any pending white- 894 // space for best comment placement. Then, any leftover whitespace is 895 // printed, followed by the actual token. 896 func (p *printer) print(args ...any) { 897 for _, arg := range args { 898 // information about the current arg 899 var data string 900 var isLit bool 901 var impliedSemi bool // value for p.impliedSemi after this arg 902 903 // record previous opening token, if any 904 switch p.lastTok { 905 case token.ILLEGAL: 906 // ignore (white space) 907 case token.LPAREN, token.LBRACK: 908 p.prevOpen = p.lastTok 909 default: 910 // other tokens followed any opening token 911 p.prevOpen = token.ILLEGAL 912 } 913 914 switch x := arg.(type) { 915 case pmode: 916 // toggle printer mode 917 p.mode ^= x 918 continue 919 920 case whiteSpace: 921 if x == ignore { 922 // don't add ignore's to the buffer; they 923 // may screw up "correcting" unindents (see 924 // LabeledStmt) 925 continue 926 } 927 i := len(p.wsbuf) 928 if i == cap(p.wsbuf) { 929 // Whitespace sequences are very short so this should 930 // never happen. Handle gracefully (but possibly with 931 // bad comment placement) if it does happen. 932 p.writeWhitespace(i) 933 i = 0 934 } 935 p.wsbuf = p.wsbuf[0 : i+1] 936 p.wsbuf[i] = x 937 if x == newline || x == formfeed { 938 // newlines affect the current state (p.impliedSemi) 939 // and not the state after printing arg (impliedSemi) 940 // because comments can be interspersed before the arg 941 // in this case 942 p.impliedSemi = false 943 } 944 p.lastTok = token.ILLEGAL 945 continue 946 947 case *ast.Ident: 948 data = x.Name 949 impliedSemi = true 950 p.lastTok = token.IDENT 951 952 case *ast.BasicLit: 953 data = x.Value 954 isLit = true 955 impliedSemi = true 956 p.lastTok = x.Kind 957 958 case token.Token: 959 s := x.String() 960 if mayCombine(p.lastTok, s[0]) { 961 // the previous and the current token must be 962 // separated by a blank otherwise they combine 963 // into a different incorrect token sequence 964 // (except for token.INT followed by a '.' this 965 // should never happen because it is taken care 966 // of via binary expression formatting) 967 if len(p.wsbuf) != 0 { 968 p.internalError("whitespace buffer not empty") 969 } 970 p.wsbuf = p.wsbuf[0:1] 971 p.wsbuf[0] = ' ' 972 } 973 data = s 974 // some keywords followed by a newline imply a semicolon 975 switch x { 976 case token.BREAK, token.CONTINUE, token.FALLTHROUGH, token.RETURN, 977 token.INC, token.DEC, token.RPAREN, token.RBRACK, token.RBRACE: 978 impliedSemi = true 979 } 980 p.lastTok = x 981 982 case string: 983 // incorrect AST - print error message 984 data = x 985 isLit = true 986 impliedSemi = true 987 p.lastTok = token.STRING 988 989 default: 990 fmt.Fprintf(os.Stderr, "print: unsupported argument %v (%T)\n", arg, arg) 991 panic("go/printer type") 992 } 993 // data != "" 994 995 next := p.pos // estimated/accurate position of next item 996 wroteNewline, droppedFF := p.flush(next, p.lastTok) 997 998 // intersperse extra newlines if present in the source and 999 // if they don't cause extra semicolons (don't do this in 1000 // flush as it will cause extra newlines at the end of a file) 1001 if !p.impliedSemi { 1002 n := nlimit(next.Line - p.pos.Line) 1003 // don't exceed maxNewlines if we already wrote one 1004 if wroteNewline && n == maxNewlines { 1005 n = maxNewlines - 1 1006 } 1007 if n > 0 { 1008 ch := byte('\n') 1009 if droppedFF { 1010 ch = '\f' // use formfeed since we dropped one before 1011 } 1012 p.writeByte(ch, n) 1013 impliedSemi = false 1014 } 1015 } 1016 1017 // the next token starts now - record its line number if requested 1018 if p.linePtr != nil { 1019 *p.linePtr = p.out.Line 1020 p.linePtr = nil 1021 } 1022 1023 p.writeString(next, data, isLit) 1024 p.impliedSemi = impliedSemi 1025 } 1026 } 1027 1028 // flush prints any pending comments and whitespace occurring textually 1029 // before the position of the next token tok. The flush result indicates 1030 // if a newline was written or if a formfeed was dropped from the whitespace 1031 // buffer. 1032 func (p *printer) flush(next token.Position, tok token.Token) (wroteNewline, droppedFF bool) { 1033 if p.commentBefore(next) { 1034 // if there are comments before the next item, intersperse them 1035 wroteNewline, droppedFF = p.intersperseComments(next, tok) 1036 } else { 1037 // otherwise, write any leftover whitespace 1038 p.writeWhitespace(len(p.wsbuf)) 1039 } 1040 return 1041 } 1042 1043 // getDoc returns the ast.CommentGroup associated with n, if any. 1044 func getDoc(n ast.Node) *ast.CommentGroup { 1045 switch n := n.(type) { 1046 case *ast.Field: 1047 return n.Doc 1048 case *ast.ImportSpec: 1049 return n.Doc 1050 case *ast.ValueSpec: 1051 return n.Doc 1052 case *ast.TypeSpec: 1053 return n.Doc 1054 case *ast.GenDecl: 1055 return n.Doc 1056 case *ast.FuncDecl: 1057 return n.Doc 1058 case *ast.File: 1059 return n.Doc 1060 } 1061 return nil 1062 } 1063 1064 func getLastComment(n ast.Node) *ast.CommentGroup { 1065 switch n := n.(type) { 1066 case *ast.Field: 1067 return n.Comment 1068 case *ast.ImportSpec: 1069 return n.Comment 1070 case *ast.ValueSpec: 1071 return n.Comment 1072 case *ast.TypeSpec: 1073 return n.Comment 1074 case *ast.GenDecl: 1075 if len(n.Specs) > 0 { 1076 return getLastComment(n.Specs[len(n.Specs)-1]) 1077 } 1078 case *ast.File: 1079 if len(n.Comments) > 0 { 1080 return n.Comments[len(n.Comments)-1] 1081 } 1082 } 1083 return nil 1084 } 1085 1086 func (p *printer) printNode(node any) error { 1087 // unpack *CommentedNode, if any 1088 var comments []*ast.CommentGroup 1089 if cnode, ok := node.(*CommentedNode); ok { 1090 node = cnode.Node 1091 comments = cnode.Comments 1092 } 1093 1094 if comments != nil { 1095 // commented node - restrict comment list to relevant range 1096 n, ok := node.(ast.Node) 1097 if !ok { 1098 goto unsupported 1099 } 1100 beg := n.Pos() 1101 end := n.End() 1102 // if the node has associated documentation, 1103 // include that commentgroup in the range 1104 // (the comment list is sorted in the order 1105 // of the comment appearance in the source code) 1106 if doc := getDoc(n); doc != nil { 1107 beg = doc.Pos() 1108 } 1109 if com := getLastComment(n); com != nil { 1110 if e := com.End(); e > end { 1111 end = e 1112 } 1113 } 1114 // token.Pos values are global offsets, we can 1115 // compare them directly 1116 i := 0 1117 for i < len(comments) && comments[i].End() < beg { 1118 i++ 1119 } 1120 j := i 1121 for j < len(comments) && comments[j].Pos() < end { 1122 j++ 1123 } 1124 if i < j { 1125 p.comments = comments[i:j] 1126 } 1127 } else if n, ok := node.(*ast.File); ok { 1128 // use ast.File comments, if any 1129 p.comments = n.Comments 1130 } 1131 1132 // if there are no comments, use node comments 1133 p.useNodeComments = p.comments == nil 1134 1135 // get comments ready for use 1136 p.nextComment() 1137 1138 p.print(pmode(0)) 1139 1140 // format node 1141 switch n := node.(type) { 1142 case ast.Expr: 1143 p.expr(n) 1144 case ast.Stmt: 1145 // A labeled statement will un-indent to position the label. 1146 // Set p.indent to 1 so we don't get indent "underflow". 1147 if _, ok := n.(*ast.LabeledStmt); ok { 1148 p.indent = 1 1149 } 1150 p.stmt(n, false) 1151 case ast.Decl: 1152 p.decl(n) 1153 case ast.Spec: 1154 p.spec(n, 1, false) 1155 case []ast.Stmt: 1156 // A labeled statement will un-indent to position the label. 1157 // Set p.indent to 1 so we don't get indent "underflow". 1158 for _, s := range n { 1159 if _, ok := s.(*ast.LabeledStmt); ok { 1160 p.indent = 1 1161 } 1162 } 1163 p.stmtList(n, 0, false) 1164 case []ast.Decl: 1165 p.declList(n) 1166 case *ast.File: 1167 p.file(n) 1168 default: 1169 goto unsupported 1170 } 1171 1172 return nil 1173 1174 unsupported: 1175 return fmt.Errorf("go/printer: unsupported node type %T", node) 1176 } 1177 1178 // ---------------------------------------------------------------------------- 1179 // Trimmer 1180 1181 // A trimmer is an io.Writer filter for stripping tabwriter.Escape 1182 // characters, trailing blanks and tabs, and for converting formfeed 1183 // and vtab characters into newlines and htabs (in case no tabwriter 1184 // is used). Text bracketed by tabwriter.Escape characters is passed 1185 // through unchanged. 1186 type trimmer struct { 1187 output io.Writer 1188 state int 1189 space []byte 1190 } 1191 1192 // trimmer is implemented as a state machine. 1193 // It can be in one of the following states: 1194 const ( 1195 inSpace = iota // inside space 1196 inEscape // inside text bracketed by tabwriter.Escapes 1197 inText // inside text 1198 ) 1199 1200 func (p *trimmer) resetSpace() { 1201 p.state = inSpace 1202 p.space = p.space[0:0] 1203 } 1204 1205 // Design note: It is tempting to eliminate extra blanks occurring in 1206 // whitespace in this function as it could simplify some 1207 // of the blanks logic in the node printing functions. 1208 // However, this would mess up any formatting done by 1209 // the tabwriter. 1210 1211 var aNewline = []byte("\n") 1212 1213 func (p *trimmer) Write(data []byte) (n int, err error) { 1214 // invariants: 1215 // p.state == inSpace: 1216 // p.space is unwritten 1217 // p.state == inEscape, inText: 1218 // data[m:n] is unwritten 1219 m := 0 1220 var b byte 1221 for n, b = range data { 1222 if b == '\v' { 1223 b = '\t' // convert to htab 1224 } 1225 switch p.state { 1226 case inSpace: 1227 switch b { 1228 case '\t', ' ': 1229 p.space = append(p.space, b) 1230 case '\n', '\f': 1231 p.resetSpace() // discard trailing space 1232 _, err = p.output.Write(aNewline) 1233 case tabwriter.Escape: 1234 _, err = p.output.Write(p.space) 1235 p.state = inEscape 1236 m = n + 1 // +1: skip tabwriter.Escape 1237 default: 1238 _, err = p.output.Write(p.space) 1239 p.state = inText 1240 m = n 1241 } 1242 case inEscape: 1243 if b == tabwriter.Escape { 1244 _, err = p.output.Write(data[m:n]) 1245 p.resetSpace() 1246 } 1247 case inText: 1248 switch b { 1249 case '\t', ' ': 1250 _, err = p.output.Write(data[m:n]) 1251 p.resetSpace() 1252 p.space = append(p.space, b) 1253 case '\n', '\f': 1254 _, err = p.output.Write(data[m:n]) 1255 p.resetSpace() 1256 if err == nil { 1257 _, err = p.output.Write(aNewline) 1258 } 1259 case tabwriter.Escape: 1260 _, err = p.output.Write(data[m:n]) 1261 p.state = inEscape 1262 m = n + 1 // +1: skip tabwriter.Escape 1263 } 1264 default: 1265 panic("unreachable") 1266 } 1267 if err != nil { 1268 return 1269 } 1270 } 1271 n = len(data) 1272 1273 switch p.state { 1274 case inEscape, inText: 1275 _, err = p.output.Write(data[m:n]) 1276 p.resetSpace() 1277 } 1278 1279 return 1280 } 1281 1282 // ---------------------------------------------------------------------------- 1283 // Public interface 1284 1285 // A Mode value is a set of flags (or 0). They control printing. 1286 type Mode uint 1287 1288 const ( 1289 RawFormat Mode = 1 << iota // do not use a tabwriter; if set, UseSpaces is ignored 1290 TabIndent // use tabs for indentation independent of UseSpaces 1291 UseSpaces // use spaces instead of tabs for alignment 1292 SourcePos // emit //line directives to preserve original source positions 1293 ) 1294 1295 // The mode below is not included in printer's public API because 1296 // editing code text is deemed out of scope. Because this mode is 1297 // unexported, it's also possible to modify or remove it based on 1298 // the evolving needs of go/format and cmd/gofmt without breaking 1299 // users. See discussion in CL 240683. 1300 const ( 1301 // normalizeNumbers means to canonicalize number 1302 // literal prefixes and exponents while printing. 1303 // 1304 // This value is known in and used by go/format and cmd/gofmt. 1305 // It is currently more convenient and performant for those 1306 // packages to apply number normalization during printing, 1307 // rather than by modifying the AST in advance. 1308 normalizeNumbers Mode = 1 << 30 1309 ) 1310 1311 // A Config node controls the output of Fprint. 1312 type Config struct { 1313 Mode Mode // default: 0 1314 Tabwidth int // default: 8 1315 Indent int // default: 0 (all code is indented at least by this much) 1316 } 1317 1318 var printerPool = sync.Pool{ 1319 New: func() any { 1320 return &printer{ 1321 // Whitespace sequences are short. 1322 wsbuf: make([]whiteSpace, 0, 16), 1323 // We start the printer with a 16K output buffer, which is currently 1324 // larger than about 80% of Go files in the standard library. 1325 output: make([]byte, 0, 16<<10), 1326 } 1327 }, 1328 } 1329 1330 func newPrinter(cfg *Config, fset *token.FileSet, nodeSizes map[ast.Node]int) *printer { 1331 p := printerPool.Get().(*printer) 1332 *p = printer{ 1333 Config: *cfg, 1334 fset: fset, 1335 pos: token.Position{Line: 1, Column: 1}, 1336 out: token.Position{Line: 1, Column: 1}, 1337 wsbuf: p.wsbuf[:0], 1338 nodeSizes: nodeSizes, 1339 cachedPos: -1, 1340 output: p.output[:0], 1341 } 1342 return p 1343 } 1344 1345 func (p *printer) free() { 1346 // Hard limit on buffer size; see https://golang.org/issue/23199. 1347 if cap(p.output) > 64<<10 { 1348 return 1349 } 1350 1351 printerPool.Put(p) 1352 } 1353 1354 // fprint implements Fprint and takes a nodesSizes map for setting up the printer state. 1355 func (cfg *Config) fprint(output io.Writer, fset *token.FileSet, node any, nodeSizes map[ast.Node]int) (err error) { 1356 // print node 1357 p := newPrinter(cfg, fset, nodeSizes) 1358 defer p.free() 1359 if err = p.printNode(node); err != nil { 1360 return 1361 } 1362 // print outstanding comments 1363 p.impliedSemi = false // EOF acts like a newline 1364 p.flush(token.Position{Offset: infinity, Line: infinity}, token.EOF) 1365 1366 // output is buffered in p.output now. 1367 // fix //go:build and // +build comments if needed. 1368 p.fixGoBuildLines() 1369 1370 // redirect output through a trimmer to eliminate trailing whitespace 1371 // (Input to a tabwriter must be untrimmed since trailing tabs provide 1372 // formatting information. The tabwriter could provide trimming 1373 // functionality but no tabwriter is used when RawFormat is set.) 1374 output = &trimmer{output: output} 1375 1376 // redirect output through a tabwriter if necessary 1377 if cfg.Mode&RawFormat == 0 { 1378 minwidth := cfg.Tabwidth 1379 1380 padchar := byte('\t') 1381 if cfg.Mode&UseSpaces != 0 { 1382 padchar = ' ' 1383 } 1384 1385 twmode := tabwriter.DiscardEmptyColumns 1386 if cfg.Mode&TabIndent != 0 { 1387 minwidth = 0 1388 twmode |= tabwriter.TabIndent 1389 } 1390 1391 output = tabwriter.NewWriter(output, minwidth, cfg.Tabwidth, 1, padchar, twmode) 1392 } 1393 1394 // write printer result via tabwriter/trimmer to output 1395 if _, err = output.Write(p.output); err != nil { 1396 return 1397 } 1398 1399 // flush tabwriter, if any 1400 if tw, _ := output.(*tabwriter.Writer); tw != nil { 1401 err = tw.Flush() 1402 } 1403 1404 return 1405 } 1406 1407 // A CommentedNode bundles an AST node and corresponding comments. 1408 // It may be provided as argument to any of the Fprint functions. 1409 type CommentedNode struct { 1410 Node any // *ast.File, or ast.Expr, ast.Decl, ast.Spec, or ast.Stmt 1411 Comments []*ast.CommentGroup 1412 } 1413 1414 // Fprint "pretty-prints" an AST node to output for a given configuration cfg. 1415 // Position information is interpreted relative to the file set fset. 1416 // The node type must be *ast.File, *CommentedNode, []ast.Decl, []ast.Stmt, 1417 // or assignment-compatible to ast.Expr, ast.Decl, ast.Spec, or ast.Stmt. 1418 func (cfg *Config) Fprint(output io.Writer, fset *token.FileSet, node any) error { 1419 return cfg.fprint(output, fset, node, make(map[ast.Node]int)) 1420 } 1421 1422 // Fprint "pretty-prints" an AST node to output. 1423 // It calls Config.Fprint with default settings. 1424 // Note that gofmt uses tabs for indentation but spaces for alignment; 1425 // use format.Node (package go/format) for output that matches gofmt. 1426 func Fprint(output io.Writer, fset *token.FileSet, node any) error { 1427 return (&Config{Tabwidth: 8}).Fprint(output, fset, node) 1428 }