github.com/lovishpuri/go-40569/src@v0.0.0-20230519171745-f8623e7c56cf/go/printer/printer_test.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 6 7 import ( 8 "bytes" 9 "errors" 10 "flag" 11 "fmt" 12 "go/ast" 13 "go/parser" 14 "go/token" 15 "internal/diff" 16 "io" 17 "os" 18 "path/filepath" 19 "testing" 20 "time" 21 ) 22 23 const ( 24 dataDir = "testdata" 25 tabwidth = 8 26 ) 27 28 var update = flag.Bool("update", false, "update golden files") 29 30 var fset = token.NewFileSet() 31 32 type checkMode uint 33 34 const ( 35 export checkMode = 1 << iota 36 rawFormat 37 normNumber 38 idempotent 39 allowTypeParams 40 ) 41 42 // format parses src, prints the corresponding AST, verifies the resulting 43 // src is syntactically correct, and returns the resulting src or an error 44 // if any. 45 func format(src []byte, mode checkMode) ([]byte, error) { 46 // parse src 47 f, err := parser.ParseFile(fset, "", src, parser.ParseComments) 48 if err != nil { 49 return nil, fmt.Errorf("parse: %s\n%s", err, src) 50 } 51 52 // filter exports if necessary 53 if mode&export != 0 { 54 ast.FileExports(f) // ignore result 55 f.Comments = nil // don't print comments that are not in AST 56 } 57 58 // determine printer configuration 59 cfg := Config{Tabwidth: tabwidth} 60 if mode&rawFormat != 0 { 61 cfg.Mode |= RawFormat 62 } 63 if mode&normNumber != 0 { 64 cfg.Mode |= normalizeNumbers 65 } 66 67 // print AST 68 var buf bytes.Buffer 69 if err := cfg.Fprint(&buf, fset, f); err != nil { 70 return nil, fmt.Errorf("print: %s", err) 71 } 72 73 // make sure formatted output is syntactically correct 74 res := buf.Bytes() 75 if _, err := parser.ParseFile(fset, "", res, parser.ParseComments); err != nil { 76 return nil, fmt.Errorf("re-parse: %s\n%s", err, buf.Bytes()) 77 } 78 79 return res, nil 80 } 81 82 // lineAt returns the line in text starting at offset offs. 83 func lineAt(text []byte, offs int) []byte { 84 i := offs 85 for i < len(text) && text[i] != '\n' { 86 i++ 87 } 88 return text[offs:i] 89 } 90 91 // checkEqual compares a and b. 92 func checkEqual(aname, bname string, a, b []byte) error { 93 if bytes.Equal(a, b) { 94 return nil 95 } 96 return errors.New(string(diff.Diff(aname, a, bname, b))) 97 } 98 99 func runcheck(t *testing.T, source, golden string, mode checkMode) { 100 src, err := os.ReadFile(source) 101 if err != nil { 102 t.Error(err) 103 return 104 } 105 106 res, err := format(src, mode) 107 if err != nil { 108 t.Error(err) 109 return 110 } 111 112 // update golden files if necessary 113 if *update { 114 if err := os.WriteFile(golden, res, 0644); err != nil { 115 t.Error(err) 116 } 117 return 118 } 119 120 // get golden 121 gld, err := os.ReadFile(golden) 122 if err != nil { 123 t.Error(err) 124 return 125 } 126 127 // formatted source and golden must be the same 128 if err := checkEqual(source, golden, res, gld); err != nil { 129 t.Error(err) 130 return 131 } 132 133 if mode&idempotent != 0 { 134 // formatting golden must be idempotent 135 // (This is very difficult to achieve in general and for now 136 // it is only checked for files explicitly marked as such.) 137 res, err = format(gld, mode) 138 if err != nil { 139 t.Error(err) 140 return 141 } 142 if err := checkEqual(golden, fmt.Sprintf("format(%s)", golden), gld, res); err != nil { 143 t.Errorf("golden is not idempotent: %s", err) 144 } 145 } 146 } 147 148 func check(t *testing.T, source, golden string, mode checkMode) { 149 // run the test 150 cc := make(chan int, 1) 151 go func() { 152 runcheck(t, source, golden, mode) 153 cc <- 0 154 }() 155 156 // wait with timeout 157 select { 158 case <-time.After(10 * time.Second): // plenty of a safety margin, even for very slow machines 159 // test running past time out 160 t.Errorf("%s: running too slowly", source) 161 case <-cc: 162 // test finished within allotted time margin 163 } 164 } 165 166 type entry struct { 167 source, golden string 168 mode checkMode 169 } 170 171 // Use go test -update to create/update the respective golden files. 172 var data = []entry{ 173 {"empty.input", "empty.golden", idempotent}, 174 {"comments.input", "comments.golden", 0}, 175 {"comments.input", "comments.x", export}, 176 {"comments2.input", "comments2.golden", idempotent}, 177 {"alignment.input", "alignment.golden", idempotent}, 178 {"linebreaks.input", "linebreaks.golden", idempotent}, 179 {"expressions.input", "expressions.golden", idempotent}, 180 {"expressions.input", "expressions.raw", rawFormat | idempotent}, 181 {"declarations.input", "declarations.golden", 0}, 182 {"statements.input", "statements.golden", 0}, 183 {"slow.input", "slow.golden", idempotent}, 184 {"complit.input", "complit.x", export}, 185 {"go2numbers.input", "go2numbers.golden", idempotent}, 186 {"go2numbers.input", "go2numbers.norm", normNumber | idempotent}, 187 {"generics.input", "generics.golden", idempotent | allowTypeParams}, 188 {"gobuild1.input", "gobuild1.golden", idempotent}, 189 {"gobuild2.input", "gobuild2.golden", idempotent}, 190 {"gobuild3.input", "gobuild3.golden", idempotent}, 191 {"gobuild4.input", "gobuild4.golden", idempotent}, 192 {"gobuild5.input", "gobuild5.golden", idempotent}, 193 {"gobuild6.input", "gobuild6.golden", idempotent}, 194 {"gobuild7.input", "gobuild7.golden", idempotent}, 195 } 196 197 func TestFiles(t *testing.T) { 198 t.Parallel() 199 for _, e := range data { 200 source := filepath.Join(dataDir, e.source) 201 golden := filepath.Join(dataDir, e.golden) 202 mode := e.mode 203 t.Run(e.source, func(t *testing.T) { 204 t.Parallel() 205 check(t, source, golden, mode) 206 // TODO(gri) check that golden is idempotent 207 //check(t, golden, golden, e.mode) 208 }) 209 } 210 } 211 212 // TestLineComments, using a simple test case, checks that consecutive line 213 // comments are properly terminated with a newline even if the AST position 214 // information is incorrect. 215 func TestLineComments(t *testing.T) { 216 const src = `// comment 1 217 // comment 2 218 // comment 3 219 package main 220 ` 221 222 fset := token.NewFileSet() 223 f, err := parser.ParseFile(fset, "", src, parser.ParseComments) 224 if err != nil { 225 panic(err) // error in test 226 } 227 228 var buf bytes.Buffer 229 fset = token.NewFileSet() // use the wrong file set 230 Fprint(&buf, fset, f) 231 232 nlines := 0 233 for _, ch := range buf.Bytes() { 234 if ch == '\n' { 235 nlines++ 236 } 237 } 238 239 const expected = 3 240 if nlines < expected { 241 t.Errorf("got %d, expected %d\n", nlines, expected) 242 t.Errorf("result:\n%s", buf.Bytes()) 243 } 244 } 245 246 // Verify that the printer can be invoked during initialization. 247 func init() { 248 const name = "foobar" 249 var buf bytes.Buffer 250 if err := Fprint(&buf, fset, &ast.Ident{Name: name}); err != nil { 251 panic(err) // error in test 252 } 253 // in debug mode, the result contains additional information; 254 // ignore it 255 if s := buf.String(); !debug && s != name { 256 panic("got " + s + ", want " + name) 257 } 258 } 259 260 // Verify that the printer doesn't crash if the AST contains BadXXX nodes. 261 func TestBadNodes(t *testing.T) { 262 const src = "package p\n(" 263 const res = "package p\nBadDecl\n" 264 f, err := parser.ParseFile(fset, "", src, parser.ParseComments) 265 if err == nil { 266 t.Error("expected illegal program") // error in test 267 } 268 var buf bytes.Buffer 269 Fprint(&buf, fset, f) 270 if buf.String() != res { 271 t.Errorf("got %q, expected %q", buf.String(), res) 272 } 273 } 274 275 // testComment verifies that f can be parsed again after printing it 276 // with its first comment set to comment at any possible source offset. 277 func testComment(t *testing.T, f *ast.File, srclen int, comment *ast.Comment) { 278 f.Comments[0].List[0] = comment 279 var buf bytes.Buffer 280 for offs := 0; offs <= srclen; offs++ { 281 buf.Reset() 282 // Printing f should result in a correct program no 283 // matter what the (incorrect) comment position is. 284 if err := Fprint(&buf, fset, f); err != nil { 285 t.Error(err) 286 } 287 if _, err := parser.ParseFile(fset, "", buf.Bytes(), 0); err != nil { 288 t.Fatalf("incorrect program for pos = %d:\n%s", comment.Slash, buf.String()) 289 } 290 // Position information is just an offset. 291 // Move comment one byte down in the source. 292 comment.Slash++ 293 } 294 } 295 296 // Verify that the printer produces a correct program 297 // even if the position information of comments introducing newlines 298 // is incorrect. 299 func TestBadComments(t *testing.T) { 300 t.Parallel() 301 const src = ` 302 // first comment - text and position changed by test 303 package p 304 import "fmt" 305 const pi = 3.14 // rough circle 306 var ( 307 x, y, z int = 1, 2, 3 308 u, v float64 309 ) 310 func fibo(n int) { 311 if n < 2 { 312 return n /* seed values */ 313 } 314 return fibo(n-1) + fibo(n-2) 315 } 316 ` 317 318 f, err := parser.ParseFile(fset, "", src, parser.ParseComments) 319 if err != nil { 320 t.Error(err) // error in test 321 } 322 323 comment := f.Comments[0].List[0] 324 pos := comment.Pos() 325 if fset.PositionFor(pos, false /* absolute position */).Offset != 1 { 326 t.Error("expected offset 1") // error in test 327 } 328 329 testComment(t, f, len(src), &ast.Comment{Slash: pos, Text: "//-style comment"}) 330 testComment(t, f, len(src), &ast.Comment{Slash: pos, Text: "/*-style comment */"}) 331 testComment(t, f, len(src), &ast.Comment{Slash: pos, Text: "/*-style \n comment */"}) 332 testComment(t, f, len(src), &ast.Comment{Slash: pos, Text: "/*-style comment \n\n\n */"}) 333 } 334 335 type visitor chan *ast.Ident 336 337 func (v visitor) Visit(n ast.Node) (w ast.Visitor) { 338 if ident, ok := n.(*ast.Ident); ok { 339 v <- ident 340 } 341 return v 342 } 343 344 // idents is an iterator that returns all idents in f via the result channel. 345 func idents(f *ast.File) <-chan *ast.Ident { 346 v := make(visitor) 347 go func() { 348 ast.Walk(v, f) 349 close(v) 350 }() 351 return v 352 } 353 354 // identCount returns the number of identifiers found in f. 355 func identCount(f *ast.File) int { 356 n := 0 357 for range idents(f) { 358 n++ 359 } 360 return n 361 } 362 363 // Verify that the SourcePos mode emits correct //line directives 364 // by testing that position information for matching identifiers 365 // is maintained. 366 func TestSourcePos(t *testing.T) { 367 const src = ` 368 package p 369 import ( "go/printer"; "math" ) 370 const pi = 3.14; var x = 0 371 type t struct{ x, y, z int; u, v, w float32 } 372 func (t *t) foo(a, b, c int) int { 373 return a*t.x + b*t.y + 374 // two extra lines here 375 // ... 376 c*t.z 377 } 378 ` 379 380 // parse original 381 f1, err := parser.ParseFile(fset, "src", src, parser.ParseComments) 382 if err != nil { 383 t.Fatal(err) 384 } 385 386 // pretty-print original 387 var buf bytes.Buffer 388 err = (&Config{Mode: UseSpaces | SourcePos, Tabwidth: 8}).Fprint(&buf, fset, f1) 389 if err != nil { 390 t.Fatal(err) 391 } 392 393 // parse pretty printed original 394 // (//line directives must be interpreted even w/o parser.ParseComments set) 395 f2, err := parser.ParseFile(fset, "", buf.Bytes(), 0) 396 if err != nil { 397 t.Fatalf("%s\n%s", err, buf.Bytes()) 398 } 399 400 // At this point the position information of identifiers in f2 should 401 // match the position information of corresponding identifiers in f1. 402 403 // number of identifiers must be > 0 (test should run) and must match 404 n1 := identCount(f1) 405 n2 := identCount(f2) 406 if n1 == 0 { 407 t.Fatal("got no idents") 408 } 409 if n2 != n1 { 410 t.Errorf("got %d idents; want %d", n2, n1) 411 } 412 413 // verify that all identifiers have correct line information 414 i2range := idents(f2) 415 for i1 := range idents(f1) { 416 i2 := <-i2range 417 418 if i2.Name != i1.Name { 419 t.Errorf("got ident %s; want %s", i2.Name, i1.Name) 420 } 421 422 // here we care about the relative (line-directive adjusted) positions 423 l1 := fset.Position(i1.Pos()).Line 424 l2 := fset.Position(i2.Pos()).Line 425 if l2 != l1 { 426 t.Errorf("got line %d; want %d for %s", l2, l1, i1.Name) 427 } 428 } 429 430 if t.Failed() { 431 t.Logf("\n%s", buf.Bytes()) 432 } 433 } 434 435 // Verify that the SourcePos mode doesn't emit unnecessary //line directives 436 // before empty lines. 437 func TestIssue5945(t *testing.T) { 438 const orig = ` 439 package p // line 2 440 func f() {} // line 3 441 442 var x, y, z int 443 444 445 func g() { // line 8 446 } 447 ` 448 449 const want = `//line src.go:2 450 package p 451 452 //line src.go:3 453 func f() {} 454 455 var x, y, z int 456 457 //line src.go:8 458 func g() { 459 } 460 ` 461 462 // parse original 463 f1, err := parser.ParseFile(fset, "src.go", orig, 0) 464 if err != nil { 465 t.Fatal(err) 466 } 467 468 // pretty-print original 469 var buf bytes.Buffer 470 err = (&Config{Mode: UseSpaces | SourcePos, Tabwidth: 8}).Fprint(&buf, fset, f1) 471 if err != nil { 472 t.Fatal(err) 473 } 474 got := buf.String() 475 476 // compare original with desired output 477 if got != want { 478 t.Errorf("got:\n%s\nwant:\n%s\n", got, want) 479 } 480 } 481 482 var decls = []string{ 483 `import "fmt"`, 484 "const pi = 3.1415\nconst e = 2.71828\n\nvar x = pi", 485 "func sum(x, y int) int\t{ return x + y }", 486 } 487 488 func TestDeclLists(t *testing.T) { 489 for _, src := range decls { 490 file, err := parser.ParseFile(fset, "", "package p;"+src, parser.ParseComments) 491 if err != nil { 492 panic(err) // error in test 493 } 494 495 var buf bytes.Buffer 496 err = Fprint(&buf, fset, file.Decls) // only print declarations 497 if err != nil { 498 panic(err) // error in test 499 } 500 501 out := buf.String() 502 if out != src { 503 t.Errorf("\ngot : %q\nwant: %q\n", out, src) 504 } 505 } 506 } 507 508 var stmts = []string{ 509 "i := 0", 510 "select {}\nvar a, b = 1, 2\nreturn a + b", 511 "go f()\ndefer func() {}()", 512 } 513 514 func TestStmtLists(t *testing.T) { 515 for _, src := range stmts { 516 file, err := parser.ParseFile(fset, "", "package p; func _() {"+src+"}", parser.ParseComments) 517 if err != nil { 518 panic(err) // error in test 519 } 520 521 var buf bytes.Buffer 522 err = Fprint(&buf, fset, file.Decls[0].(*ast.FuncDecl).Body.List) // only print statements 523 if err != nil { 524 panic(err) // error in test 525 } 526 527 out := buf.String() 528 if out != src { 529 t.Errorf("\ngot : %q\nwant: %q\n", out, src) 530 } 531 } 532 } 533 534 func TestBaseIndent(t *testing.T) { 535 t.Parallel() 536 // The testfile must not contain multi-line raw strings since those 537 // are not indented (because their values must not change) and make 538 // this test fail. 539 const filename = "printer.go" 540 src, err := os.ReadFile(filename) 541 if err != nil { 542 panic(err) // error in test 543 } 544 545 file, err := parser.ParseFile(fset, filename, src, 0) 546 if err != nil { 547 panic(err) // error in test 548 } 549 550 for indent := 0; indent < 4; indent++ { 551 indent := indent 552 t.Run(fmt.Sprint(indent), func(t *testing.T) { 553 t.Parallel() 554 var buf bytes.Buffer 555 (&Config{Tabwidth: tabwidth, Indent: indent}).Fprint(&buf, fset, file) 556 // all code must be indented by at least 'indent' tabs 557 lines := bytes.Split(buf.Bytes(), []byte{'\n'}) 558 for i, line := range lines { 559 if len(line) == 0 { 560 continue // empty lines don't have indentation 561 } 562 n := 0 563 for j, b := range line { 564 if b != '\t' { 565 // end of indentation 566 n = j 567 break 568 } 569 } 570 if n < indent { 571 t.Errorf("line %d: got only %d tabs; want at least %d: %q", i, n, indent, line) 572 } 573 } 574 }) 575 } 576 } 577 578 // TestFuncType tests that an ast.FuncType with a nil Params field 579 // can be printed (per go/ast specification). Test case for issue 3870. 580 func TestFuncType(t *testing.T) { 581 src := &ast.File{ 582 Name: &ast.Ident{Name: "p"}, 583 Decls: []ast.Decl{ 584 &ast.FuncDecl{ 585 Name: &ast.Ident{Name: "f"}, 586 Type: &ast.FuncType{}, 587 }, 588 }, 589 } 590 591 var buf bytes.Buffer 592 if err := Fprint(&buf, fset, src); err != nil { 593 t.Fatal(err) 594 } 595 got := buf.String() 596 597 const want = `package p 598 599 func f() 600 ` 601 602 if got != want { 603 t.Fatalf("got:\n%s\nwant:\n%s\n", got, want) 604 } 605 } 606 607 type limitWriter struct { 608 remaining int 609 errCount int 610 } 611 612 func (l *limitWriter) Write(buf []byte) (n int, err error) { 613 n = len(buf) 614 if n >= l.remaining { 615 n = l.remaining 616 err = io.EOF 617 l.errCount++ 618 } 619 l.remaining -= n 620 return n, err 621 } 622 623 // Test whether the printer stops writing after the first error 624 func TestWriteErrors(t *testing.T) { 625 t.Parallel() 626 const filename = "printer.go" 627 src, err := os.ReadFile(filename) 628 if err != nil { 629 panic(err) // error in test 630 } 631 file, err := parser.ParseFile(fset, filename, src, 0) 632 if err != nil { 633 panic(err) // error in test 634 } 635 for i := 0; i < 20; i++ { 636 lw := &limitWriter{remaining: i} 637 err := (&Config{Mode: RawFormat}).Fprint(lw, fset, file) 638 if lw.errCount > 1 { 639 t.Fatal("Writes continued after first error returned") 640 } 641 // We expect errCount be 1 iff err is set 642 if (lw.errCount != 0) != (err != nil) { 643 t.Fatal("Expected err when errCount != 0") 644 } 645 } 646 } 647 648 // TextX is a skeleton test that can be filled in for debugging one-off cases. 649 // Do not remove. 650 func TestX(t *testing.T) { 651 const src = ` 652 package p 653 func _() {} 654 ` 655 _, err := format([]byte(src), 0) 656 if err != nil { 657 t.Error(err) 658 } 659 } 660 661 func TestCommentedNode(t *testing.T) { 662 const ( 663 input = `package main 664 665 func foo() { 666 // comment inside func 667 } 668 669 // leading comment 670 type bar int // comment2 671 672 ` 673 674 foo = `func foo() { 675 // comment inside func 676 }` 677 678 bar = `// leading comment 679 type bar int // comment2 680 ` 681 ) 682 683 fset := token.NewFileSet() 684 f, err := parser.ParseFile(fset, "input.go", input, parser.ParseComments) 685 if err != nil { 686 t.Fatal(err) 687 } 688 689 var buf bytes.Buffer 690 691 err = Fprint(&buf, fset, &CommentedNode{Node: f.Decls[0], Comments: f.Comments}) 692 if err != nil { 693 t.Fatal(err) 694 } 695 696 if buf.String() != foo { 697 t.Errorf("got %q, want %q", buf.String(), foo) 698 } 699 700 buf.Reset() 701 702 err = Fprint(&buf, fset, &CommentedNode{Node: f.Decls[1], Comments: f.Comments}) 703 if err != nil { 704 t.Fatal(err) 705 } 706 707 if buf.String() != bar { 708 t.Errorf("got %q, want %q", buf.String(), bar) 709 } 710 } 711 712 func TestIssue11151(t *testing.T) { 713 const src = "package p\t/*\r/1\r*\r/2*\r\r\r\r/3*\r\r+\r\r/4*/\n" 714 fset := token.NewFileSet() 715 f, err := parser.ParseFile(fset, "", src, parser.ParseComments) 716 if err != nil { 717 t.Fatal(err) 718 } 719 720 var buf bytes.Buffer 721 Fprint(&buf, fset, f) 722 got := buf.String() 723 const want = "package p\t/*/1*\r/2*\r/3*+/4*/\n" // \r following opening /* should be stripped 724 if got != want { 725 t.Errorf("\ngot : %q\nwant: %q", got, want) 726 } 727 728 // the resulting program must be valid 729 _, err = parser.ParseFile(fset, "", got, 0) 730 if err != nil { 731 t.Errorf("%v\norig: %q\ngot : %q", err, src, got) 732 } 733 } 734 735 // If a declaration has multiple specifications, a parenthesized 736 // declaration must be printed even if Lparen is token.NoPos. 737 func TestParenthesizedDecl(t *testing.T) { 738 // a package with multiple specs in a single declaration 739 const src = "package p; var ( a float64; b int )" 740 fset := token.NewFileSet() 741 f, err := parser.ParseFile(fset, "", src, 0) 742 if err != nil { 743 t.Fatal(err) 744 } 745 746 // print the original package 747 var buf bytes.Buffer 748 err = Fprint(&buf, fset, f) 749 if err != nil { 750 t.Fatal(err) 751 } 752 original := buf.String() 753 754 // now remove parentheses from the declaration 755 for i := 0; i != len(f.Decls); i++ { 756 f.Decls[i].(*ast.GenDecl).Lparen = token.NoPos 757 } 758 buf.Reset() 759 err = Fprint(&buf, fset, f) 760 if err != nil { 761 t.Fatal(err) 762 } 763 noparen := buf.String() 764 765 if noparen != original { 766 t.Errorf("got %q, want %q", noparen, original) 767 } 768 } 769 770 // Verify that we don't print a newline between "return" and its results, as 771 // that would incorrectly cause a naked return. 772 func TestIssue32854(t *testing.T) { 773 src := `package foo 774 775 func f() { 776 return Composite{ 777 call(), 778 } 779 }` 780 fset := token.NewFileSet() 781 file, err := parser.ParseFile(fset, "", src, 0) 782 if err != nil { 783 panic(err) 784 } 785 786 // Replace the result with call(), which is on the next line. 787 fd := file.Decls[0].(*ast.FuncDecl) 788 ret := fd.Body.List[0].(*ast.ReturnStmt) 789 ret.Results[0] = ret.Results[0].(*ast.CompositeLit).Elts[0] 790 791 var buf bytes.Buffer 792 if err := Fprint(&buf, fset, ret); err != nil { 793 t.Fatal(err) 794 } 795 want := "return call()" 796 if got := buf.String(); got != want { 797 t.Fatalf("got %q, want %q", got, want) 798 } 799 }