From f8cf82f6f2de1ea91b525ca70f92b51a3df4d9df Mon Sep 17 00:00:00 2001 From: Robert Griesemer Date: Fri, 10 Feb 2012 13:27:32 -0800 Subject: [PATCH] go/printer: implement SourcePos mode If a printer is configured with the SourcePos mode set, it will emit //-line comments as necessary to ensure that the result - if reparsed - reflects the original source position information. This change required a bit of reworking of the output section in printer.go. Specifically: - Introduced new Config mode 'SourcePos'. - Introduced new position 'out' which tracks the position of the generated output if it were read in again. If there is a discrepancy between out and the current AST/source position, a //line comment is emitted to correct for it. - Lazy emission of indentation so that //line comments can be placed correctly. As a result, the trimmer will have to do less work. - Merged writeItem into writeString. - Merged writeByteN into writeByte. - Use a []byte instead of a byte.Buffer both in the printer and in the trimmer (eliminates dependency). Also: introduced explicit printer.Mode type (in sync w/ parser.Mode, scanner.Mode, etc.) Runs all tests. Applied gofmt to src, misc w/o changes. Fixes #1047. Fixes #2697. R=rsc, rsc CC=golang-dev https://golang.org/cl/5643066 --- doc/go1.html | 20 +++ doc/go1.tmpl | 20 +++ src/cmd/cgo/godefs.go | 2 +- src/cmd/cgo/out.go | 17 ++- src/cmd/gofmt/gofmt.go | 2 +- src/pkg/go/printer/printer.go | 238 ++++++++++++++++------------- src/pkg/go/printer/printer_test.go | 128 +++++++++++++++- 7 files changed, 311 insertions(+), 116 deletions(-) diff --git a/doc/go1.html b/doc/go1.html index c8643914c4..fce1c079e7 100644 --- a/doc/go1.html +++ b/doc/go1.html @@ -1049,6 +1049,15 @@ The Duration flag is new and affects no existing code. Several packages under go have slightly revised APIs.

+

+A concrete Mode type was introduced for configuration mode flags +in the packages +go/scanner, +go/parser, +go/printer, and +go/doc. +

+

The modes AllowIllegalChars and InsertSemis have been removed from the go/scanner package. They were mostly @@ -1075,6 +1084,17 @@ convenience functions ParseDirParseExpr.

+

+The go/printer package supports an additional +configuration mode SourcePos; +if set, the printer will emit //line comments such that the generated +output contains the original source code position information. The new type +CommentedNode can be +used to provide comments associated with an arbitrary +ast.Node (until now only +ast.File carried comment information). +

+

The type names of the go/doc package have been streamlined by removing the Doc suffix: PackageDoc diff --git a/doc/go1.tmpl b/doc/go1.tmpl index f37f9516ee..985cf97e17 100644 --- a/doc/go1.tmpl +++ b/doc/go1.tmpl @@ -952,6 +952,15 @@ The Duration flag is new and affects no existing code. Several packages under go have slightly revised APIs.

+

+A concrete Mode type was introduced for configuration mode flags +in the packages +go/scanner, +go/parser, +go/printer, and +go/doc. +

+

The modes AllowIllegalChars and InsertSemis have been removed from the go/scanner package. They were mostly @@ -978,6 +987,17 @@ convenience functions ParseDirParseExpr.

+

+The go/printer package supports an additional +configuration mode SourcePos; +if set, the printer will emit //line comments such that the generated +output contains the original source code position information. The new type +CommentedNode can be +used to provide comments associated with an arbitrary +ast.Node (until now only +ast.File carried comment information). +

+

The type names of the go/doc package have been streamlined by removing the Doc suffix: PackageDoc diff --git a/src/cmd/cgo/godefs.go b/src/cmd/cgo/godefs.go index 6838729274..478ed261cb 100644 --- a/src/cmd/cgo/godefs.go +++ b/src/cmd/cgo/godefs.go @@ -109,7 +109,7 @@ func (p *Package) godefs(f *File, srcfile string) string { } } - printer.Fprint(&buf, fset, f.AST) + conf.Fprint(&buf, fset, f.AST) return buf.String() } diff --git a/src/cmd/cgo/out.go b/src/cmd/cgo/out.go index 2c01074991..bfbcf50dc7 100644 --- a/src/cmd/cgo/out.go +++ b/src/cmd/cgo/out.go @@ -17,6 +17,8 @@ import ( "strings" ) +var conf = printer.Config{Mode: printer.SourcePos, Tabwidth: 8} + // writeDefs creates output files to be compiled by 6g, 6c, and gcc. // (The comments here say 6g and 6c but the code applies to the 8 and 5 tools too.) func (p *Package) writeDefs() { @@ -57,7 +59,7 @@ func (p *Package) writeDefs() { for name, def := range typedef { fmt.Fprintf(fgo2, "type %s ", name) - printer.Fprint(fgo2, fset, def) + conf.Fprint(fgo2, fset, def) fmt.Fprintf(fgo2, "\n\n") } fmt.Fprintf(fgo2, "type _Ctype_void [0]byte\n") @@ -87,7 +89,7 @@ func (p *Package) writeDefs() { fmt.Fprintf(fc, "\n") fmt.Fprintf(fgo2, "var %s ", n.Mangle) - printer.Fprint(fgo2, fset, &ast.StarExpr{X: n.Type.Go}) + conf.Fprint(fgo2, fset, &ast.StarExpr{X: n.Type.Go}) fmt.Fprintf(fgo2, "\n") } fmt.Fprintf(fc, "\n") @@ -255,7 +257,7 @@ func (p *Package) writeDefsFunc(fc, fgo2 *os.File, n *Name) { Name: ast.NewIdent(n.Mangle), Type: gtype, } - printer.Fprint(fgo2, fset, d) + conf.Fprint(fgo2, fset, d) if *gccgo { fmt.Fprintf(fgo2, " __asm__(\"%s\")\n", n.C) } else { @@ -327,8 +329,7 @@ func (p *Package) writeOutput(f *File, srcfile string) { // Write Go output: Go input with rewrites of C.xxx to _C_xxx. fmt.Fprintf(fgo1, "// Created by cgo - DO NOT EDIT\n\n") - fmt.Fprintf(fgo1, "//line %s:1\n", srcfile) - printer.Fprint(fgo1, fset, f.AST) + conf.Fprint(fgo1, fset, f.AST) // While we process the vars and funcs, also write 6c and gcc output. // Gcc output starts with the preamble. @@ -542,11 +543,11 @@ func (p *Package) writeExports(fgo2, fc, fm *os.File) { // a Go wrapper function. if fn.Recv != nil { fmt.Fprintf(fgo2, "func %s(recv ", goname) - printer.Fprint(fgo2, fset, fn.Recv.List[0].Type) + conf.Fprint(fgo2, fset, fn.Recv.List[0].Type) forFieldList(fntype.Params, func(i int, atype ast.Expr) { fmt.Fprintf(fgo2, ", p%d ", i) - printer.Fprint(fgo2, fset, atype) + conf.Fprint(fgo2, fset, atype) }) fmt.Fprintf(fgo2, ")") if gccResult != "void" { @@ -556,7 +557,7 @@ func (p *Package) writeExports(fgo2, fc, fm *os.File) { if i > 0 { fmt.Fprint(fgo2, ", ") } - printer.Fprint(fgo2, fset, atype) + conf.Fprint(fgo2, fset, atype) }) fmt.Fprint(fgo2, ")") } diff --git a/src/cmd/gofmt/gofmt.go b/src/cmd/gofmt/gofmt.go index 6d610adc0e..55c01beb55 100644 --- a/src/cmd/gofmt/gofmt.go +++ b/src/cmd/gofmt/gofmt.go @@ -45,7 +45,7 @@ var ( exitCode = 0 rewrite func(*ast.File) *ast.File parserMode parser.Mode - printerMode uint + printerMode printer.Mode ) func report(err error) { diff --git a/src/pkg/go/printer/printer.go b/src/pkg/go/printer/printer.go index fe99e675eb..e9ab5fd5de 100644 --- a/src/pkg/go/printer/printer.go +++ b/src/pkg/go/printer/printer.go @@ -6,7 +6,6 @@ package printer import ( - "bytes" "fmt" "go/ast" "go/token" @@ -51,22 +50,22 @@ type printer struct { fset *token.FileSet // Current state - output bytes.Buffer // raw printer result + output []byte // raw printer result indent int // current indentation mode pmode // current printer mode impliedSemi bool // if set, a linebreak implies a semicolon lastTok token.Token // the last token printed (token.ILLEGAL if it's whitespace) wsbuf []whiteSpace // delayed white space - // The (possibly estimated) position in the generated output; - // in AST space (i.e., pos is set whenever a token position is - // known accurately, and updated dependending on what has been - // written). - pos token.Position - - // The value of pos immediately after the last item has been - // written using writeItem. - last token.Position + // Positions + // The out position differs from the pos position when the result + // formatting differs from the source formatting (in the amount of + // white space). If there's a difference and SourcePos is set in + // ConfigMode, //line comments are used in the output to restore + // original source positions for a reader. + pos token.Position // current position in AST (source) space + out token.Position // current position in output space + last token.Position // value of pos after calling writeString // The list of all source comments, in order of appearance. comments []*ast.CommentGroup // may be nil @@ -89,6 +88,8 @@ type printer struct { func (p *printer) init(cfg *Config, fset *token.FileSet, nodeSizes map[ast.Node]int) { p.Config = *cfg p.fset = fset + p.pos = token.Position{Line: 1, Column: 1} + p.out = token.Position{Line: 1, Column: 1} p.wsbuf = make([]whiteSpace, 0, 16) // whitespace sequences are short p.nodeSizes = nodeSizes p.cachedPos = -1 @@ -151,41 +152,57 @@ func (p *printer) lineFor(pos token.Pos) int { return p.cachedLine } -// writeByte writes ch to p.output and updates p.pos. -func (p *printer) writeByte(ch byte) { - p.output.WriteByte(ch) - p.pos.Offset++ - p.pos.Column++ +// atLineBegin emits a //line comment if necessary and prints indentation. +func (p *printer) atLineBegin(pos token.Position) { + // write a //line comment if necessary + if p.Config.Mode&SourcePos != 0 && pos.IsValid() && (p.out.Line != pos.Line || p.out.Filename != pos.Filename) { + p.output = append(p.output, tabwriter.Escape) // protect '\n' in //line from tabwriter interpretation + p.output = append(p.output, fmt.Sprintf("//line %s:%d\n", pos.Filename, pos.Line)...) + p.output = append(p.output, tabwriter.Escape) + // p.out must match the //line comment + p.out.Filename = pos.Filename + p.out.Line = pos.Line + } + // write indentation + // use "hard" htabs - indentation columns + // must not be discarded by the tabwriter + for i := 0; i < p.indent; i++ { + p.output = append(p.output, '\t') + } + + // update positions + i := p.indent + p.pos.Offset += i + p.pos.Column += i + p.out.Column += i +} + +// writeByte writes ch n times to p.output and updates p.pos. +func (p *printer) writeByte(ch byte, n int) { + if p.out.Column == 1 { + p.atLineBegin(p.pos) + } + + for i := 0; i < n; i++ { + p.output = append(p.output, ch) + } + + // update positions + p.pos.Offset += n if ch == '\n' || ch == '\f' { - // write indentation - // use "hard" htabs - indentation columns - // must not be discarded by the tabwriter - const htabs = "\t\t\t\t\t\t\t\t" - j := p.indent - for j > len(htabs) { - p.output.WriteString(htabs) - j -= len(htabs) - } - p.output.WriteString(htabs[0:j]) - - // update p.pos - p.pos.Line++ - p.pos.Offset += p.indent - p.pos.Column = 1 + p.indent + p.pos.Line += n + p.out.Line += n + p.pos.Column = 1 + p.out.Column = 1 + return } + p.pos.Column += n + p.out.Column += n } -// writeByteN writes ch n times to p.output and updates p.pos. -func (p *printer) writeByteN(ch byte, n int) { - for n > 0 { - p.writeByte(ch) - n-- - } -} - -// writeString writes the string s to p.output and updates p.pos. -// If isLit is set, s is escaped w/ tabwriter.Escape characters +// writeString writes the string s to p.output and updates p.pos, p.out, +// and p.last. If isLit is set, s is escaped w/ tabwriter.Escape characters // to protect s from being interpreted by the tabwriter. // // Note: writeString is only used to write Go tokens, literals, and @@ -195,59 +212,66 @@ func (p *printer) writeByteN(ch byte, n int) { // avoids processing extra escape characters and reduces run time of the // printer benchmark by up to 10%. // -func (p *printer) writeString(s string, isLit bool) { +func (p *printer) writeString(pos token.Position, s string, isLit bool) { + if p.out.Column == 1 { + p.atLineBegin(pos) + } + + if pos.IsValid() { + // update p.pos (if pos is invalid, continue with existing p.pos) + // Note: Must do this after handling line beginnings because + // atLineBegin updates p.pos if there's indentation, but p.pos + // is the position of s. + p.pos = pos + // reset state if the file changed + // (used when printing merged ASTs of different files + // e.g., the result of ast.MergePackageFiles) + if p.last.IsValid() && p.last.Filename != pos.Filename { + p.indent = 0 + p.mode = 0 + p.wsbuf = p.wsbuf[0:0] + } + } + if isLit { // Protect s such that is passes through the tabwriter // unchanged. Note that valid Go programs cannot contain // tabwriter.Escape bytes since they do not appear in legal // UTF-8 sequences. - p.output.WriteByte(tabwriter.Escape) + p.output = append(p.output, tabwriter.Escape) } - p.output.WriteString(s) + if debug { + p.output = append(p.output, fmt.Sprintf("/*%s*/", pos)...) // do not update p.pos! + } + p.output = append(p.output, s...) - // update p.pos + // update positions nlines := 0 - column := p.pos.Column + len(s) + var li int // index of last newline; valid if nlines > 0 for i := 0; i < len(s); i++ { + // Go tokens cannot contain '\f' - no need to look for it if s[i] == '\n' { nlines++ - column = len(s) - i + li = i } } p.pos.Offset += len(s) - p.pos.Line += nlines - p.pos.Column = column + if nlines > 0 { + p.pos.Line += nlines + p.out.Line += nlines + c := len(s) - li + p.pos.Column = c + p.out.Column = c + } else { + p.pos.Column += len(s) + p.out.Column += len(s) + } if isLit { - p.output.WriteByte(tabwriter.Escape) + p.output = append(p.output, tabwriter.Escape) } -} -// writeItem writes data at position pos. data is the text corresponding to -// a single lexical token, but may also be comment text. pos is the actual -// (or at least very accurately estimated) position of the data in the original -// source text. writeItem updates p.last to the position immediately following -// the data. -// -func (p *printer) writeItem(pos token.Position, data string, isLit bool) { - if pos.IsValid() { - // continue with previous position if we don't have a valid pos - if p.last.IsValid() && p.last.Filename != pos.Filename { - // the file has changed - reset state - // (used when printing merged ASTs of different files - // e.g., the result of ast.MergePackageFiles) - p.indent = 0 - p.mode = 0 - p.wsbuf = p.wsbuf[0:0] - } - p.pos = pos - } - if debug { - // do not update p.pos - use write0 - fmt.Fprintf(&p.output, "/*%s*/", pos) - } - p.writeString(data, isLit) p.last = p.pos } @@ -262,14 +286,14 @@ const linePrefix = "//line " // next item is a keyword. // func (p *printer) writeCommentPrefix(pos, next token.Position, prev, comment *ast.Comment, isKeyword bool) { - if p.output.Len() == 0 { + if len(p.output) == 0 { // the comment is the first item to be printed - don't write any whitespace return } if pos.IsValid() && pos.Filename != p.last.Filename { // comment in a different file - separate with newlines - p.writeByteN('\f', maxNewlines) + p.writeByte('\f', maxNewlines) return } @@ -309,7 +333,7 @@ func (p *printer) writeCommentPrefix(pos, next token.Position, prev, comment *as // with a blank instead of a tab sep = ' ' } - p.writeByte(sep) + p.writeByte(sep, 1) } } else { @@ -381,7 +405,7 @@ func (p *printer) writeCommentPrefix(pos, next token.Position, prev, comment *as // use formfeeds to break columns before a comment; // this is analogous to using formfeeds to separate // individual lines of /*-style comments - p.writeByteN('\f', nlimit(n)) + p.writeByte('\f', nlimit(n)) p.indent = indent // restore indent } } @@ -587,7 +611,7 @@ func (p *printer) writeComment(comment *ast.Comment) { // shortcut common case of //-style comments if text[1] == '/' { - p.writeItem(p.posFor(comment.Pos()), text, true) + p.writeString(p.posFor(comment.Pos()), text, true) return } @@ -601,11 +625,11 @@ func (p *printer) writeComment(comment *ast.Comment) { pos := p.posFor(comment.Pos()) for i, line := range lines { if i > 0 { - p.writeByte('\f') + p.writeByte('\f', 1) pos = p.pos } if len(line) > 0 { - p.writeItem(pos, line, true) + p.writeString(pos, line, true) } } } @@ -643,7 +667,7 @@ func (p *printer) writeCommentSuffix(needsLinebreak bool) (wroteNewline, dropped // make sure we have a line break if needsLinebreak { - p.writeByte('\n') + p.writeByte('\n', 1) wroteNewline = true } @@ -671,7 +695,7 @@ func (p *printer) intersperseComments(next token.Position, tok token.Token) (wro if last.Text[1] == '*' && p.lineFor(last.Pos()) == next.Line { // the last comment is a /*-style comment and the next item // follows on the same line: separate with an extra blank - p.writeByte(' ') + p.writeByte(' ', 1) } // ensure that there is a line break after a //-style comment, // before a closing '}' unless explicitly disabled, or at eof @@ -722,7 +746,7 @@ func (p *printer) writeWhitespace(n int) { } fallthrough default: - p.writeByte(byte(ch)) + p.writeByte(byte(ch), 1) } } @@ -886,12 +910,12 @@ func (p *printer) print(args ...interface{}) { if droppedFF { ch = '\f' // use formfeed since we dropped one before } - p.writeByteN(ch, n) + p.writeByte(ch, n) impliedSemi = false } } - p.writeItem(next, data, isLit) + p.writeString(next, data, isLit) p.impliedSemi = impliedSemi } } @@ -1027,7 +1051,7 @@ unsupported: type trimmer struct { output io.Writer state int - space bytes.Buffer + space []byte } // trimmer is implemented as a state machine. @@ -1038,6 +1062,11 @@ const ( inText // inside text ) +func (p *trimmer) resetSpace() { + p.state = inSpace + p.space = p.space[0:0] +} + // Design note: It is tempting to eliminate extra blanks occurring in // whitespace in this function as it could simplify some // of the blanks logic in the node printing functions. @@ -1062,36 +1091,33 @@ func (p *trimmer) Write(data []byte) (n int, err error) { case inSpace: switch b { case '\t', ' ': - p.space.WriteByte(b) // WriteByte returns no errors + p.space = append(p.space, b) case '\n', '\f': - p.space.Reset() // discard trailing space + p.resetSpace() // discard trailing space _, err = p.output.Write(aNewline) case tabwriter.Escape: - _, err = p.output.Write(p.space.Bytes()) + _, err = p.output.Write(p.space) p.state = inEscape m = n + 1 // +1: skip tabwriter.Escape default: - _, err = p.output.Write(p.space.Bytes()) + _, err = p.output.Write(p.space) p.state = inText m = n } case inEscape: if b == tabwriter.Escape { _, err = p.output.Write(data[m:n]) - p.state = inSpace - p.space.Reset() + p.resetSpace() } case inText: switch b { case '\t', ' ': _, err = p.output.Write(data[m:n]) - p.state = inSpace - p.space.Reset() - p.space.WriteByte(b) // WriteByte returns no errors + p.resetSpace() + p.space = append(p.space, b) case '\n', '\f': _, err = p.output.Write(data[m:n]) - p.state = inSpace - p.space.Reset() + p.resetSpace() _, err = p.output.Write(aNewline) case tabwriter.Escape: _, err = p.output.Write(data[m:n]) @@ -1110,8 +1136,7 @@ func (p *trimmer) Write(data []byte) (n int, err error) { switch p.state { case inEscape, inText: _, err = p.output.Write(data[m:n]) - p.state = inSpace - p.space.Reset() + p.resetSpace() } return @@ -1120,16 +1145,19 @@ func (p *trimmer) Write(data []byte) (n int, err error) { // ---------------------------------------------------------------------------- // Public interface -// General printing is controlled with these Config.Mode flags. +// A Mode value is a set of flags (or 0). They coontrol printing. +type Mode uint + const ( - RawFormat uint = 1 << iota // do not use a tabwriter; if set, UseSpaces is ignored + RawFormat Mode = 1 << iota // do not use a tabwriter; if set, UseSpaces is ignored TabIndent // use tabs for indentation independent of UseSpaces UseSpaces // use spaces instead of tabs for alignment + SourcePos // emit //line comments to preserve original source positions ) // A Config node controls the output of Fprint. type Config struct { - Mode uint // default: 0 + Mode Mode // default: 0 Tabwidth int // default: 8 } @@ -1170,7 +1198,7 @@ func (cfg *Config) fprint(output io.Writer, fset *token.FileSet, node interface{ } // write printer result via tabwriter/trimmer to output - if _, err = output.Write(p.output.Bytes()); err != nil { + if _, err = output.Write(p.output); err != nil { return } diff --git a/src/pkg/go/printer/printer_test.go b/src/pkg/go/printer/printer_test.go index 9adf48cda6..a0578814aa 100644 --- a/src/pkg/go/printer/printer_test.go +++ b/src/pkg/go/printer/printer_test.go @@ -223,7 +223,8 @@ func TestBadNodes(t *testing.T) { } } -// Print and parse f with +// 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 @@ -280,3 +281,128 @@ func fibo(n int) { testComment(t, f, len(src), &ast.Comment{pos, "/*-style \n comment */"}) testComment(t, f, len(src), &ast.Comment{pos, "/*-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()) + } +} + +// 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 _() {} +` + // parse original + f, err := parser.ParseFile(fset, "src", src, parser.ParseComments) + if err != nil { + t.Fatal(err) + } + + // pretty-print original + var buf bytes.Buffer + if err = (&Config{Mode: UseSpaces, Tabwidth: 8}).Fprint(&buf, fset, f); err != nil { + t.Fatal(err) + } + + // parse pretty printed original + if _, err := parser.ParseFile(fset, "", buf.Bytes(), 0); err != nil { + t.Fatalf("%s\n%s", err, buf.Bytes()) + } + +}