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