|  | // Copyright 2009 The Go Authors. All rights reserved. | 
|  | // Use of this source code is governed by a BSD-style | 
|  | // license that can be found in the LICENSE file. | 
|  |  | 
|  | package printer | 
|  |  | 
|  | import ( | 
|  | "bytes" | 
|  | "errors" | 
|  | "flag" | 
|  | "fmt" | 
|  | "go/ast" | 
|  | "go/parser" | 
|  | "go/token" | 
|  | "io" | 
|  | "io/ioutil" | 
|  | "path/filepath" | 
|  | "testing" | 
|  | "time" | 
|  | ) | 
|  |  | 
|  | const ( | 
|  | dataDir  = "testdata" | 
|  | tabwidth = 8 | 
|  | ) | 
|  |  | 
|  | var update = flag.Bool("update", false, "update golden files") | 
|  |  | 
|  | var fset = token.NewFileSet() | 
|  |  | 
|  | type checkMode uint | 
|  |  | 
|  | const ( | 
|  | export checkMode = 1 << iota | 
|  | rawFormat | 
|  | idempotent | 
|  | ) | 
|  |  | 
|  | // format parses src, prints the corresponding AST, verifies the resulting | 
|  | // src is syntactically correct, and returns the resulting src or an error | 
|  | // if any. | 
|  | func format(src []byte, mode checkMode) ([]byte, error) { | 
|  | // parse src | 
|  | f, err := parser.ParseFile(fset, "", src, parser.ParseComments) | 
|  | if err != nil { | 
|  | return nil, fmt.Errorf("parse: %s\n%s", err, src) | 
|  | } | 
|  |  | 
|  | // filter exports if necessary | 
|  | if mode&export != 0 { | 
|  | ast.FileExports(f) // ignore result | 
|  | f.Comments = nil   // don't print comments that are not in AST | 
|  | } | 
|  |  | 
|  | // determine printer configuration | 
|  | cfg := Config{Tabwidth: tabwidth} | 
|  | if mode&rawFormat != 0 { | 
|  | cfg.Mode |= RawFormat | 
|  | } | 
|  |  | 
|  | // print AST | 
|  | var buf bytes.Buffer | 
|  | if err := cfg.Fprint(&buf, fset, f); err != nil { | 
|  | return nil, fmt.Errorf("print: %s", err) | 
|  | } | 
|  |  | 
|  | // make sure formatted output is syntactically correct | 
|  | res := buf.Bytes() | 
|  | if _, err := parser.ParseFile(fset, "", res, 0); err != nil { | 
|  | return nil, fmt.Errorf("re-parse: %s\n%s", err, buf.Bytes()) | 
|  | } | 
|  |  | 
|  | return res, nil | 
|  | } | 
|  |  | 
|  | // lineAt returns the line in text starting at offset offs. | 
|  | func lineAt(text []byte, offs int) []byte { | 
|  | i := offs | 
|  | for i < len(text) && text[i] != '\n' { | 
|  | i++ | 
|  | } | 
|  | return text[offs:i] | 
|  | } | 
|  |  | 
|  | // diff compares a and b. | 
|  | func diff(aname, bname string, a, b []byte) error { | 
|  | var buf bytes.Buffer // holding long error message | 
|  |  | 
|  | // compare lengths | 
|  | if len(a) != len(b) { | 
|  | fmt.Fprintf(&buf, "\nlength changed: len(%s) = %d, len(%s) = %d", aname, len(a), bname, len(b)) | 
|  | } | 
|  |  | 
|  | // compare contents | 
|  | line := 1 | 
|  | offs := 1 | 
|  | for i := 0; i < len(a) && i < len(b); i++ { | 
|  | ch := a[i] | 
|  | if ch != b[i] { | 
|  | fmt.Fprintf(&buf, "\n%s:%d:%d: %s", aname, line, i-offs+1, lineAt(a, offs)) | 
|  | fmt.Fprintf(&buf, "\n%s:%d:%d: %s", bname, line, i-offs+1, lineAt(b, offs)) | 
|  | fmt.Fprintf(&buf, "\n\n") | 
|  | break | 
|  | } | 
|  | if ch == '\n' { | 
|  | line++ | 
|  | offs = i + 1 | 
|  | } | 
|  | } | 
|  |  | 
|  | if buf.Len() > 0 { | 
|  | return errors.New(buf.String()) | 
|  | } | 
|  | return nil | 
|  | } | 
|  |  | 
|  | func runcheck(t *testing.T, source, golden string, mode checkMode) { | 
|  | src, err := ioutil.ReadFile(source) | 
|  | if err != nil { | 
|  | t.Error(err) | 
|  | return | 
|  | } | 
|  |  | 
|  | res, err := format(src, mode) | 
|  | if err != nil { | 
|  | t.Error(err) | 
|  | return | 
|  | } | 
|  |  | 
|  | // update golden files if necessary | 
|  | if *update { | 
|  | if err := ioutil.WriteFile(golden, res, 0644); err != nil { | 
|  | t.Error(err) | 
|  | } | 
|  | return | 
|  | } | 
|  |  | 
|  | // get golden | 
|  | gld, err := ioutil.ReadFile(golden) | 
|  | if err != nil { | 
|  | t.Error(err) | 
|  | return | 
|  | } | 
|  |  | 
|  | // formatted source and golden must be the same | 
|  | if err := diff(source, golden, res, gld); err != nil { | 
|  | t.Error(err) | 
|  | return | 
|  | } | 
|  |  | 
|  | if mode&idempotent != 0 { | 
|  | // formatting golden must be idempotent | 
|  | // (This is very difficult to achieve in general and for now | 
|  | // it is only checked for files explicitly marked as such.) | 
|  | res, err = format(gld, mode) | 
|  | if err := diff(golden, fmt.Sprintf("format(%s)", golden), gld, res); err != nil { | 
|  | t.Errorf("golden is not idempotent: %s", err) | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | func check(t *testing.T, source, golden string, mode checkMode) { | 
|  | // run the test | 
|  | cc := make(chan int) | 
|  | go func() { | 
|  | runcheck(t, source, golden, mode) | 
|  | cc <- 0 | 
|  | }() | 
|  |  | 
|  | // wait with timeout | 
|  | select { | 
|  | case <-time.After(10 * time.Second): // plenty of a safety margin, even for very slow machines | 
|  | // test running past time out | 
|  | t.Errorf("%s: running too slowly", source) | 
|  | case <-cc: | 
|  | // test finished within allotted time margin | 
|  | } | 
|  | } | 
|  |  | 
|  | type entry struct { | 
|  | source, golden string | 
|  | mode           checkMode | 
|  | } | 
|  |  | 
|  | // Use go test -update to create/update the respective golden files. | 
|  | var data = []entry{ | 
|  | {"empty.input", "empty.golden", idempotent}, | 
|  | {"comments.input", "comments.golden", 0}, | 
|  | {"comments.input", "comments.x", export}, | 
|  | {"comments2.input", "comments2.golden", idempotent}, | 
|  | {"linebreaks.input", "linebreaks.golden", idempotent}, | 
|  | {"expressions.input", "expressions.golden", idempotent}, | 
|  | {"expressions.input", "expressions.raw", rawFormat | idempotent}, | 
|  | {"declarations.input", "declarations.golden", 0}, | 
|  | {"statements.input", "statements.golden", 0}, | 
|  | {"slow.input", "slow.golden", idempotent}, | 
|  | } | 
|  |  | 
|  | func TestFiles(t *testing.T) { | 
|  | t.Parallel() | 
|  | for _, e := range data { | 
|  | source := filepath.Join(dataDir, e.source) | 
|  | golden := filepath.Join(dataDir, e.golden) | 
|  | mode := e.mode | 
|  | t.Run(e.source, func(t *testing.T) { | 
|  | t.Parallel() | 
|  | check(t, source, golden, mode) | 
|  | // TODO(gri) check that golden is idempotent | 
|  | //check(t, golden, golden, e.mode) | 
|  | }) | 
|  | } | 
|  | } | 
|  |  | 
|  | // TestLineComments, using a simple test case, checks that consecutive line | 
|  | // comments are properly terminated with a newline even if the AST position | 
|  | // information is incorrect. | 
|  | // | 
|  | func TestLineComments(t *testing.T) { | 
|  | const src = `// comment 1 | 
|  | // comment 2 | 
|  | // comment 3 | 
|  | package main | 
|  | ` | 
|  |  | 
|  | fset := token.NewFileSet() | 
|  | f, err := parser.ParseFile(fset, "", src, parser.ParseComments) | 
|  | if err != nil { | 
|  | panic(err) // error in test | 
|  | } | 
|  |  | 
|  | var buf bytes.Buffer | 
|  | fset = token.NewFileSet() // use the wrong file set | 
|  | Fprint(&buf, fset, f) | 
|  |  | 
|  | nlines := 0 | 
|  | for _, ch := range buf.Bytes() { | 
|  | if ch == '\n' { | 
|  | nlines++ | 
|  | } | 
|  | } | 
|  |  | 
|  | const expected = 3 | 
|  | if nlines < expected { | 
|  | t.Errorf("got %d, expected %d\n", nlines, expected) | 
|  | t.Errorf("result:\n%s", buf.Bytes()) | 
|  | } | 
|  | } | 
|  |  | 
|  | // Verify that the printer can be invoked during initialization. | 
|  | func init() { | 
|  | const name = "foobar" | 
|  | var buf bytes.Buffer | 
|  | if err := Fprint(&buf, fset, &ast.Ident{Name: name}); err != nil { | 
|  | panic(err) // error in test | 
|  | } | 
|  | // in debug mode, the result contains additional information; | 
|  | // ignore it | 
|  | if s := buf.String(); !debug && s != name { | 
|  | panic("got " + s + ", want " + name) | 
|  | } | 
|  | } | 
|  |  | 
|  | // Verify that the printer doesn't crash if the AST contains BadXXX nodes. | 
|  | func TestBadNodes(t *testing.T) { | 
|  | const src = "package p\n(" | 
|  | const res = "package p\nBadDecl\n" | 
|  | f, err := parser.ParseFile(fset, "", src, parser.ParseComments) | 
|  | if err == nil { | 
|  | t.Error("expected illegal program") // error in test | 
|  | } | 
|  | var buf bytes.Buffer | 
|  | Fprint(&buf, fset, f) | 
|  | if buf.String() != res { | 
|  | t.Errorf("got %q, expected %q", buf.String(), res) | 
|  | } | 
|  | } | 
|  |  | 
|  | // testComment verifies that f can be parsed again after printing it | 
|  | // with its first comment set to comment at any possible source offset. | 
|  | func testComment(t *testing.T, f *ast.File, srclen int, comment *ast.Comment) { | 
|  | f.Comments[0].List[0] = comment | 
|  | var buf bytes.Buffer | 
|  | for offs := 0; offs <= srclen; offs++ { | 
|  | buf.Reset() | 
|  | // Printing f should result in a correct program no | 
|  | // matter what the (incorrect) comment position is. | 
|  | if err := Fprint(&buf, fset, f); err != nil { | 
|  | t.Error(err) | 
|  | } | 
|  | if _, err := parser.ParseFile(fset, "", buf.Bytes(), 0); err != nil { | 
|  | t.Fatalf("incorrect program for pos = %d:\n%s", comment.Slash, buf.String()) | 
|  | } | 
|  | // Position information is just an offset. | 
|  | // Move comment one byte down in the source. | 
|  | comment.Slash++ | 
|  | } | 
|  | } | 
|  |  | 
|  | // Verify that the printer produces a correct program | 
|  | // even if the position information of comments introducing newlines | 
|  | // is incorrect. | 
|  | func TestBadComments(t *testing.T) { | 
|  | t.Parallel() | 
|  | const src = ` | 
|  | // first comment - text and position changed by test | 
|  | package p | 
|  | import "fmt" | 
|  | const pi = 3.14 // rough circle | 
|  | var ( | 
|  | x, y, z int = 1, 2, 3 | 
|  | u, v float64 | 
|  | ) | 
|  | func fibo(n int) { | 
|  | if n < 2 { | 
|  | return n /* seed values */ | 
|  | } | 
|  | return fibo(n-1) + fibo(n-2) | 
|  | } | 
|  | ` | 
|  |  | 
|  | f, err := parser.ParseFile(fset, "", src, parser.ParseComments) | 
|  | if err != nil { | 
|  | t.Error(err) // error in test | 
|  | } | 
|  |  | 
|  | comment := f.Comments[0].List[0] | 
|  | pos := comment.Pos() | 
|  | if fset.Position(pos).Offset != 1 { | 
|  | t.Error("expected offset 1") // error in test | 
|  | } | 
|  |  | 
|  | testComment(t, f, len(src), &ast.Comment{Slash: pos, Text: "//-style comment"}) | 
|  | testComment(t, f, len(src), &ast.Comment{Slash: pos, Text: "/*-style comment */"}) | 
|  | testComment(t, f, len(src), &ast.Comment{Slash: pos, Text: "/*-style \n comment */"}) | 
|  | testComment(t, f, len(src), &ast.Comment{Slash: pos, Text: "/*-style comment \n\n\n */"}) | 
|  | } | 
|  |  | 
|  | type visitor chan *ast.Ident | 
|  |  | 
|  | func (v visitor) Visit(n ast.Node) (w ast.Visitor) { | 
|  | if ident, ok := n.(*ast.Ident); ok { | 
|  | v <- ident | 
|  | } | 
|  | return v | 
|  | } | 
|  |  | 
|  | // idents is an iterator that returns all idents in f via the result channel. | 
|  | func idents(f *ast.File) <-chan *ast.Ident { | 
|  | v := make(visitor) | 
|  | go func() { | 
|  | ast.Walk(v, f) | 
|  | close(v) | 
|  | }() | 
|  | return v | 
|  | } | 
|  |  | 
|  | // identCount returns the number of identifiers found in f. | 
|  | func identCount(f *ast.File) int { | 
|  | n := 0 | 
|  | for range idents(f) { | 
|  | n++ | 
|  | } | 
|  | return n | 
|  | } | 
|  |  | 
|  | // Verify that the SourcePos mode emits correct //line comments | 
|  | // by testing that position information for matching identifiers | 
|  | // is maintained. | 
|  | func TestSourcePos(t *testing.T) { | 
|  | const src = ` | 
|  | package p | 
|  | import ( "go/printer"; "math" ) | 
|  | const pi = 3.14; var x = 0 | 
|  | type t struct{ x, y, z int; u, v, w float32 } | 
|  | func (t *t) foo(a, b, c int) int { | 
|  | return a*t.x + b*t.y + | 
|  | // two extra lines here | 
|  | // ... | 
|  | c*t.z | 
|  | } | 
|  | ` | 
|  |  | 
|  | // parse original | 
|  | f1, err := parser.ParseFile(fset, "src", src, parser.ParseComments) | 
|  | if err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  |  | 
|  | // pretty-print original | 
|  | var buf bytes.Buffer | 
|  | err = (&Config{Mode: UseSpaces | SourcePos, Tabwidth: 8}).Fprint(&buf, fset, f1) | 
|  | if err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  |  | 
|  | // parse pretty printed original | 
|  | // (//line comments must be interpreted even w/o parser.ParseComments set) | 
|  | f2, err := parser.ParseFile(fset, "", buf.Bytes(), 0) | 
|  | if err != nil { | 
|  | t.Fatalf("%s\n%s", err, buf.Bytes()) | 
|  | } | 
|  |  | 
|  | // At this point the position information of identifiers in f2 should | 
|  | // match the position information of corresponding identifiers in f1. | 
|  |  | 
|  | // number of identifiers must be > 0 (test should run) and must match | 
|  | n1 := identCount(f1) | 
|  | n2 := identCount(f2) | 
|  | if n1 == 0 { | 
|  | t.Fatal("got no idents") | 
|  | } | 
|  | if n2 != n1 { | 
|  | t.Errorf("got %d idents; want %d", n2, n1) | 
|  | } | 
|  |  | 
|  | // verify that all identifiers have correct line information | 
|  | i2range := idents(f2) | 
|  | for i1 := range idents(f1) { | 
|  | i2 := <-i2range | 
|  |  | 
|  | if i2.Name != i1.Name { | 
|  | t.Errorf("got ident %s; want %s", i2.Name, i1.Name) | 
|  | } | 
|  |  | 
|  | l1 := fset.Position(i1.Pos()).Line | 
|  | l2 := fset.Position(i2.Pos()).Line | 
|  | if l2 != l1 { | 
|  | t.Errorf("got line %d; want %d for %s", l2, l1, i1.Name) | 
|  | } | 
|  | } | 
|  |  | 
|  | if t.Failed() { | 
|  | t.Logf("\n%s", buf.Bytes()) | 
|  | } | 
|  | } | 
|  |  | 
|  | var decls = []string{ | 
|  | `import "fmt"`, | 
|  | "const pi = 3.1415\nconst e = 2.71828\n\nvar x = pi", | 
|  | "func sum(x, y int) int\t{ return x + y }", | 
|  | } | 
|  |  | 
|  | func TestDeclLists(t *testing.T) { | 
|  | for _, src := range decls { | 
|  | file, err := parser.ParseFile(fset, "", "package p;"+src, parser.ParseComments) | 
|  | if err != nil { | 
|  | panic(err) // error in test | 
|  | } | 
|  |  | 
|  | var buf bytes.Buffer | 
|  | err = Fprint(&buf, fset, file.Decls) // only print declarations | 
|  | if err != nil { | 
|  | panic(err) // error in test | 
|  | } | 
|  |  | 
|  | out := buf.String() | 
|  | if out != src { | 
|  | t.Errorf("\ngot : %q\nwant: %q\n", out, src) | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | var stmts = []string{ | 
|  | "i := 0", | 
|  | "select {}\nvar a, b = 1, 2\nreturn a + b", | 
|  | "go f()\ndefer func() {}()", | 
|  | } | 
|  |  | 
|  | func TestStmtLists(t *testing.T) { | 
|  | for _, src := range stmts { | 
|  | file, err := parser.ParseFile(fset, "", "package p; func _() {"+src+"}", parser.ParseComments) | 
|  | if err != nil { | 
|  | panic(err) // error in test | 
|  | } | 
|  |  | 
|  | var buf bytes.Buffer | 
|  | err = Fprint(&buf, fset, file.Decls[0].(*ast.FuncDecl).Body.List) // only print statements | 
|  | if err != nil { | 
|  | panic(err) // error in test | 
|  | } | 
|  |  | 
|  | out := buf.String() | 
|  | if out != src { | 
|  | t.Errorf("\ngot : %q\nwant: %q\n", out, src) | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | func TestBaseIndent(t *testing.T) { | 
|  | t.Parallel() | 
|  | // The testfile must not contain multi-line raw strings since those | 
|  | // are not indented (because their values must not change) and make | 
|  | // this test fail. | 
|  | const filename = "printer.go" | 
|  | src, err := ioutil.ReadFile(filename) | 
|  | if err != nil { | 
|  | panic(err) // error in test | 
|  | } | 
|  |  | 
|  | file, err := parser.ParseFile(fset, filename, src, 0) | 
|  | if err != nil { | 
|  | panic(err) // error in test | 
|  | } | 
|  |  | 
|  | for indent := 0; indent < 4; indent++ { | 
|  | indent := indent | 
|  | t.Run(fmt.Sprint(indent), func(t *testing.T) { | 
|  | t.Parallel() | 
|  | var buf bytes.Buffer | 
|  | (&Config{Tabwidth: tabwidth, Indent: indent}).Fprint(&buf, fset, file) | 
|  | // all code must be indented by at least 'indent' tabs | 
|  | lines := bytes.Split(buf.Bytes(), []byte{'\n'}) | 
|  | for i, line := range lines { | 
|  | if len(line) == 0 { | 
|  | continue // empty lines don't have indentation | 
|  | } | 
|  | n := 0 | 
|  | for j, b := range line { | 
|  | if b != '\t' { | 
|  | // end of indentation | 
|  | n = j | 
|  | break | 
|  | } | 
|  | } | 
|  | if n < indent { | 
|  | t.Errorf("line %d: got only %d tabs; want at least %d: %q", i, n, indent, line) | 
|  | } | 
|  | } | 
|  | }) | 
|  | } | 
|  | } | 
|  |  | 
|  | // TestFuncType tests that an ast.FuncType with a nil Params field | 
|  | // can be printed (per go/ast specification). Test case for issue 3870. | 
|  | func TestFuncType(t *testing.T) { | 
|  | src := &ast.File{ | 
|  | Name: &ast.Ident{Name: "p"}, | 
|  | Decls: []ast.Decl{ | 
|  | &ast.FuncDecl{ | 
|  | Name: &ast.Ident{Name: "f"}, | 
|  | Type: &ast.FuncType{}, | 
|  | }, | 
|  | }, | 
|  | } | 
|  |  | 
|  | var buf bytes.Buffer | 
|  | if err := Fprint(&buf, fset, src); err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  | got := buf.String() | 
|  |  | 
|  | const want = `package p | 
|  |  | 
|  | func f() | 
|  | ` | 
|  |  | 
|  | if got != want { | 
|  | t.Fatalf("got:\n%s\nwant:\n%s\n", got, want) | 
|  | } | 
|  | } | 
|  |  | 
|  | type limitWriter struct { | 
|  | remaining int | 
|  | errCount  int | 
|  | } | 
|  |  | 
|  | func (l *limitWriter) Write(buf []byte) (n int, err error) { | 
|  | n = len(buf) | 
|  | if n >= l.remaining { | 
|  | n = l.remaining | 
|  | err = io.EOF | 
|  | l.errCount++ | 
|  | } | 
|  | l.remaining -= n | 
|  | return n, err | 
|  | } | 
|  |  | 
|  | // Test whether the printer stops writing after the first error | 
|  | func TestWriteErrors(t *testing.T) { | 
|  | t.Parallel() | 
|  | const filename = "printer.go" | 
|  | src, err := ioutil.ReadFile(filename) | 
|  | if err != nil { | 
|  | panic(err) // error in test | 
|  | } | 
|  | file, err := parser.ParseFile(fset, filename, src, 0) | 
|  | if err != nil { | 
|  | panic(err) // error in test | 
|  | } | 
|  | for i := 0; i < 20; i++ { | 
|  | lw := &limitWriter{remaining: i} | 
|  | err := (&Config{Mode: RawFormat}).Fprint(lw, fset, file) | 
|  | if lw.errCount > 1 { | 
|  | t.Fatal("Writes continued after first error returned") | 
|  | } | 
|  | // We expect errCount be 1 iff err is set | 
|  | if (lw.errCount != 0) != (err != nil) { | 
|  | t.Fatal("Expected err when errCount != 0") | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | // TextX is a skeleton test that can be filled in for debugging one-off cases. | 
|  | // Do not remove. | 
|  | func TestX(t *testing.T) { | 
|  | const src = ` | 
|  | package p | 
|  | func _() {} | 
|  | ` | 
|  | _, err := format([]byte(src), 0) | 
|  | if err != nil { | 
|  | t.Error(err) | 
|  | } | 
|  | } |