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