Home | History | Annotate | Download | only in printer
      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 	"io"
     16 	"io/ioutil"
     17 	"path/filepath"
     18 	"testing"
     19 	"time"
     20 )
     21 
     22 const (
     23 	dataDir  = "testdata"
     24 	tabwidth = 8
     25 )
     26 
     27 var update = flag.Bool("update", false, "update golden files")
     28 
     29 var fset = token.NewFileSet()
     30 
     31 type checkMode uint
     32 
     33 const (
     34 	export checkMode = 1 << iota
     35 	rawFormat
     36 	idempotent
     37 )
     38 
     39 // format parses src, prints the corresponding AST, verifies the resulting
     40 // src is syntactically correct, and returns the resulting src or an error
     41 // if any.
     42 func format(src []byte, mode checkMode) ([]byte, error) {
     43 	// parse src
     44 	f, err := parser.ParseFile(fset, "", src, parser.ParseComments)
     45 	if err != nil {
     46 		return nil, fmt.Errorf("parse: %s\n%s", err, src)
     47 	}
     48 
     49 	// filter exports if necessary
     50 	if mode&export != 0 {
     51 		ast.FileExports(f) // ignore result
     52 		f.Comments = nil   // don't print comments that are not in AST
     53 	}
     54 
     55 	// determine printer configuration
     56 	cfg := Config{Tabwidth: tabwidth}
     57 	if mode&rawFormat != 0 {
     58 		cfg.Mode |= RawFormat
     59 	}
     60 
     61 	// print AST
     62 	var buf bytes.Buffer
     63 	if err := cfg.Fprint(&buf, fset, f); err != nil {
     64 		return nil, fmt.Errorf("print: %s", err)
     65 	}
     66 
     67 	// make sure formatted output is syntactically correct
     68 	res := buf.Bytes()
     69 	if _, err := parser.ParseFile(fset, "", res, 0); err != nil {
     70 		return nil, fmt.Errorf("re-parse: %s\n%s", err, buf.Bytes())
     71 	}
     72 
     73 	return res, nil
     74 }
     75 
     76 // lineAt returns the line in text starting at offset offs.
     77 func lineAt(text []byte, offs int) []byte {
     78 	i := offs
     79 	for i < len(text) && text[i] != '\n' {
     80 		i++
     81 	}
     82 	return text[offs:i]
     83 }
     84 
     85 // diff compares a and b.
     86 func diff(aname, bname string, a, b []byte) error {
     87 	var buf bytes.Buffer // holding long error message
     88 
     89 	// compare lengths
     90 	if len(a) != len(b) {
     91 		fmt.Fprintf(&buf, "\nlength changed: len(%s) = %d, len(%s) = %d", aname, len(a), bname, len(b))
     92 	}
     93 
     94 	// compare contents
     95 	line := 1
     96 	offs := 1
     97 	for i := 0; i < len(a) && i < len(b); i++ {
     98 		ch := a[i]
     99 		if ch != b[i] {
    100 			fmt.Fprintf(&buf, "\n%s:%d:%d: %s", aname, line, i-offs+1, lineAt(a, offs))
    101 			fmt.Fprintf(&buf, "\n%s:%d:%d: %s", bname, line, i-offs+1, lineAt(b, offs))
    102 			fmt.Fprintf(&buf, "\n\n")
    103 			break
    104 		}
    105 		if ch == '\n' {
    106 			line++
    107 			offs = i + 1
    108 		}
    109 	}
    110 
    111 	if buf.Len() > 0 {
    112 		return errors.New(buf.String())
    113 	}
    114 	return nil
    115 }
    116 
    117 func runcheck(t *testing.T, source, golden string, mode checkMode) {
    118 	src, err := ioutil.ReadFile(source)
    119 	if err != nil {
    120 		t.Error(err)
    121 		return
    122 	}
    123 
    124 	res, err := format(src, mode)
    125 	if err != nil {
    126 		t.Error(err)
    127 		return
    128 	}
    129 
    130 	// update golden files if necessary
    131 	if *update {
    132 		if err := ioutil.WriteFile(golden, res, 0644); err != nil {
    133 			t.Error(err)
    134 		}
    135 		return
    136 	}
    137 
    138 	// get golden
    139 	gld, err := ioutil.ReadFile(golden)
    140 	if err != nil {
    141 		t.Error(err)
    142 		return
    143 	}
    144 
    145 	// formatted source and golden must be the same
    146 	if err := diff(source, golden, res, gld); err != nil {
    147 		t.Error(err)
    148 		return
    149 	}
    150 
    151 	if mode&idempotent != 0 {
    152 		// formatting golden must be idempotent
    153 		// (This is very difficult to achieve in general and for now
    154 		// it is only checked for files explicitly marked as such.)
    155 		res, err = format(gld, mode)
    156 		if err := diff(golden, fmt.Sprintf("format(%s)", golden), gld, res); err != nil {
    157 			t.Errorf("golden is not idempotent: %s", err)
    158 		}
    159 	}
    160 }
    161 
    162 func check(t *testing.T, source, golden string, mode checkMode) {
    163 	// run the test
    164 	cc := make(chan int)
    165 	go func() {
    166 		runcheck(t, source, golden, mode)
    167 		cc <- 0
    168 	}()
    169 
    170 	// wait with timeout
    171 	select {
    172 	case <-time.After(10 * time.Second): // plenty of a safety margin, even for very slow machines
    173 		// test running past time out
    174 		t.Errorf("%s: running too slowly", source)
    175 	case <-cc:
    176 		// test finished within allotted time margin
    177 	}
    178 }
    179 
    180 type entry struct {
    181 	source, golden string
    182 	mode           checkMode
    183 }
    184 
    185 // Use go test -update to create/update the respective golden files.
    186 var data = []entry{
    187 	{"empty.input", "empty.golden", idempotent},
    188 	{"comments.input", "comments.golden", 0},
    189 	{"comments.input", "comments.x", export},
    190 	{"comments2.input", "comments2.golden", idempotent},
    191 	{"linebreaks.input", "linebreaks.golden", idempotent},
    192 	{"expressions.input", "expressions.golden", idempotent},
    193 	{"expressions.input", "expressions.raw", rawFormat | idempotent},
    194 	{"declarations.input", "declarations.golden", 0},
    195 	{"statements.input", "statements.golden", 0},
    196 	{"slow.input", "slow.golden", idempotent},
    197 }
    198 
    199 func TestFiles(t *testing.T) {
    200 	t.Parallel()
    201 	for _, e := range data {
    202 		source := filepath.Join(dataDir, e.source)
    203 		golden := filepath.Join(dataDir, e.golden)
    204 		mode := e.mode
    205 		t.Run(e.source, func(t *testing.T) {
    206 			t.Parallel()
    207 			check(t, source, golden, mode)
    208 			// TODO(gri) check that golden is idempotent
    209 			//check(t, golden, golden, e.mode)
    210 		})
    211 	}
    212 }
    213 
    214 // TestLineComments, using a simple test case, checks that consecutive line
    215 // comments are properly terminated with a newline even if the AST position
    216 // information is incorrect.
    217 //
    218 func TestLineComments(t *testing.T) {
    219 	const src = `// comment 1
    220 	// comment 2
    221 	// comment 3
    222 	package main
    223 	`
    224 
    225 	fset := token.NewFileSet()
    226 	f, err := parser.ParseFile(fset, "", src, parser.ParseComments)
    227 	if err != nil {
    228 		panic(err) // error in test
    229 	}
    230 
    231 	var buf bytes.Buffer
    232 	fset = token.NewFileSet() // use the wrong file set
    233 	Fprint(&buf, fset, f)
    234 
    235 	nlines := 0
    236 	for _, ch := range buf.Bytes() {
    237 		if ch == '\n' {
    238 			nlines++
    239 		}
    240 	}
    241 
    242 	const expected = 3
    243 	if nlines < expected {
    244 		t.Errorf("got %d, expected %d\n", nlines, expected)
    245 		t.Errorf("result:\n%s", buf.Bytes())
    246 	}
    247 }
    248 
    249 // Verify that the printer can be invoked during initialization.
    250 func init() {
    251 	const name = "foobar"
    252 	var buf bytes.Buffer
    253 	if err := Fprint(&buf, fset, &ast.Ident{Name: name}); err != nil {
    254 		panic(err) // error in test
    255 	}
    256 	// in debug mode, the result contains additional information;
    257 	// ignore it
    258 	if s := buf.String(); !debug && s != name {
    259 		panic("got " + s + ", want " + name)
    260 	}
    261 }
    262 
    263 // Verify that the printer doesn't crash if the AST contains BadXXX nodes.
    264 func TestBadNodes(t *testing.T) {
    265 	const src = "package p\n("
    266 	const res = "package p\nBadDecl\n"
    267 	f, err := parser.ParseFile(fset, "", src, parser.ParseComments)
    268 	if err == nil {
    269 		t.Error("expected illegal program") // error in test
    270 	}
    271 	var buf bytes.Buffer
    272 	Fprint(&buf, fset, f)
    273 	if buf.String() != res {
    274 		t.Errorf("got %q, expected %q", buf.String(), res)
    275 	}
    276 }
    277 
    278 // testComment verifies that f can be parsed again after printing it
    279 // with its first comment set to comment at any possible source offset.
    280 func testComment(t *testing.T, f *ast.File, srclen int, comment *ast.Comment) {
    281 	f.Comments[0].List[0] = comment
    282 	var buf bytes.Buffer
    283 	for offs := 0; offs <= srclen; offs++ {
    284 		buf.Reset()
    285 		// Printing f should result in a correct program no
    286 		// matter what the (incorrect) comment position is.
    287 		if err := Fprint(&buf, fset, f); err != nil {
    288 			t.Error(err)
    289 		}
    290 		if _, err := parser.ParseFile(fset, "", buf.Bytes(), 0); err != nil {
    291 			t.Fatalf("incorrect program for pos = %d:\n%s", comment.Slash, buf.String())
    292 		}
    293 		// Position information is just an offset.
    294 		// Move comment one byte down in the source.
    295 		comment.Slash++
    296 	}
    297 }
    298 
    299 // Verify that the printer produces a correct program
    300 // even if the position information of comments introducing newlines
    301 // is incorrect.
    302 func TestBadComments(t *testing.T) {
    303 	t.Parallel()
    304 	const src = `
    305 // first comment - text and position changed by test
    306 package p
    307 import "fmt"
    308 const pi = 3.14 // rough circle
    309 var (
    310 	x, y, z int = 1, 2, 3
    311 	u, v float64
    312 )
    313 func fibo(n int) {
    314 	if n < 2 {
    315 		return n /* seed values */
    316 	}
    317 	return fibo(n-1) + fibo(n-2)
    318 }
    319 `
    320 
    321 	f, err := parser.ParseFile(fset, "", src, parser.ParseComments)
    322 	if err != nil {
    323 		t.Error(err) // error in test
    324 	}
    325 
    326 	comment := f.Comments[0].List[0]
    327 	pos := comment.Pos()
    328 	if fset.Position(pos).Offset != 1 {
    329 		t.Error("expected offset 1") // error in test
    330 	}
    331 
    332 	testComment(t, f, len(src), &ast.Comment{Slash: pos, Text: "//-style comment"})
    333 	testComment(t, f, len(src), &ast.Comment{Slash: pos, Text: "/*-style comment */"})
    334 	testComment(t, f, len(src), &ast.Comment{Slash: pos, Text: "/*-style \n comment */"})
    335 	testComment(t, f, len(src), &ast.Comment{Slash: pos, Text: "/*-style comment \n\n\n */"})
    336 }
    337 
    338 type visitor chan *ast.Ident
    339 
    340 func (v visitor) Visit(n ast.Node) (w ast.Visitor) {
    341 	if ident, ok := n.(*ast.Ident); ok {
    342 		v <- ident
    343 	}
    344 	return v
    345 }
    346 
    347 // idents is an iterator that returns all idents in f via the result channel.
    348 func idents(f *ast.File) <-chan *ast.Ident {
    349 	v := make(visitor)
    350 	go func() {
    351 		ast.Walk(v, f)
    352 		close(v)
    353 	}()
    354 	return v
    355 }
    356 
    357 // identCount returns the number of identifiers found in f.
    358 func identCount(f *ast.File) int {
    359 	n := 0
    360 	for range idents(f) {
    361 		n++
    362 	}
    363 	return n
    364 }
    365 
    366 // Verify that the SourcePos mode emits correct //line comments
    367 // by testing that position information for matching identifiers
    368 // is maintained.
    369 func TestSourcePos(t *testing.T) {
    370 	const src = `
    371 package p
    372 import ( "go/printer"; "math" )
    373 const pi = 3.14; var x = 0
    374 type t struct{ x, y, z int; u, v, w float32 }
    375 func (t *t) foo(a, b, c int) int {
    376 	return a*t.x + b*t.y +
    377 		// two extra lines here
    378 		// ...
    379 		c*t.z
    380 }
    381 `
    382 
    383 	// parse original
    384 	f1, err := parser.ParseFile(fset, "src", src, parser.ParseComments)
    385 	if err != nil {
    386 		t.Fatal(err)
    387 	}
    388 
    389 	// pretty-print original
    390 	var buf bytes.Buffer
    391 	err = (&Config{Mode: UseSpaces | SourcePos, Tabwidth: 8}).Fprint(&buf, fset, f1)
    392 	if err != nil {
    393 		t.Fatal(err)
    394 	}
    395 
    396 	// parse pretty printed original
    397 	// (//line comments must be interpreted even w/o parser.ParseComments set)
    398 	f2, err := parser.ParseFile(fset, "", buf.Bytes(), 0)
    399 	if err != nil {
    400 		t.Fatalf("%s\n%s", err, buf.Bytes())
    401 	}
    402 
    403 	// At this point the position information of identifiers in f2 should
    404 	// match the position information of corresponding identifiers in f1.
    405 
    406 	// number of identifiers must be > 0 (test should run) and must match
    407 	n1 := identCount(f1)
    408 	n2 := identCount(f2)
    409 	if n1 == 0 {
    410 		t.Fatal("got no idents")
    411 	}
    412 	if n2 != n1 {
    413 		t.Errorf("got %d idents; want %d", n2, n1)
    414 	}
    415 
    416 	// verify that all identifiers have correct line information
    417 	i2range := idents(f2)
    418 	for i1 := range idents(f1) {
    419 		i2 := <-i2range
    420 
    421 		if i2.Name != i1.Name {
    422 			t.Errorf("got ident %s; want %s", i2.Name, i1.Name)
    423 		}
    424 
    425 		l1 := fset.Position(i1.Pos()).Line
    426 		l2 := fset.Position(i2.Pos()).Line
    427 		if l2 != l1 {
    428 			t.Errorf("got line %d; want %d for %s", l2, l1, i1.Name)
    429 		}
    430 	}
    431 
    432 	if t.Failed() {
    433 		t.Logf("\n%s", buf.Bytes())
    434 	}
    435 }
    436 
    437 var decls = []string{
    438 	`import "fmt"`,
    439 	"const pi = 3.1415\nconst e = 2.71828\n\nvar x = pi",
    440 	"func sum(x, y int) int\t{ return x + y }",
    441 }
    442 
    443 func TestDeclLists(t *testing.T) {
    444 	for _, src := range decls {
    445 		file, err := parser.ParseFile(fset, "", "package p;"+src, parser.ParseComments)
    446 		if err != nil {
    447 			panic(err) // error in test
    448 		}
    449 
    450 		var buf bytes.Buffer
    451 		err = Fprint(&buf, fset, file.Decls) // only print declarations
    452 		if err != nil {
    453 			panic(err) // error in test
    454 		}
    455 
    456 		out := buf.String()
    457 		if out != src {
    458 			t.Errorf("\ngot : %q\nwant: %q\n", out, src)
    459 		}
    460 	}
    461 }
    462 
    463 var stmts = []string{
    464 	"i := 0",
    465 	"select {}\nvar a, b = 1, 2\nreturn a + b",
    466 	"go f()\ndefer func() {}()",
    467 }
    468 
    469 func TestStmtLists(t *testing.T) {
    470 	for _, src := range stmts {
    471 		file, err := parser.ParseFile(fset, "", "package p; func _() {"+src+"}", parser.ParseComments)
    472 		if err != nil {
    473 			panic(err) // error in test
    474 		}
    475 
    476 		var buf bytes.Buffer
    477 		err = Fprint(&buf, fset, file.Decls[0].(*ast.FuncDecl).Body.List) // only print statements
    478 		if err != nil {
    479 			panic(err) // error in test
    480 		}
    481 
    482 		out := buf.String()
    483 		if out != src {
    484 			t.Errorf("\ngot : %q\nwant: %q\n", out, src)
    485 		}
    486 	}
    487 }
    488 
    489 func TestBaseIndent(t *testing.T) {
    490 	t.Parallel()
    491 	// The testfile must not contain multi-line raw strings since those
    492 	// are not indented (because their values must not change) and make
    493 	// this test fail.
    494 	const filename = "printer.go"
    495 	src, err := ioutil.ReadFile(filename)
    496 	if err != nil {
    497 		panic(err) // error in test
    498 	}
    499 
    500 	file, err := parser.ParseFile(fset, filename, src, 0)
    501 	if err != nil {
    502 		panic(err) // error in test
    503 	}
    504 
    505 	for indent := 0; indent < 4; indent++ {
    506 		indent := indent
    507 		t.Run(fmt.Sprint(indent), func(t *testing.T) {
    508 			t.Parallel()
    509 			var buf bytes.Buffer
    510 			(&Config{Tabwidth: tabwidth, Indent: indent}).Fprint(&buf, fset, file)
    511 			// all code must be indented by at least 'indent' tabs
    512 			lines := bytes.Split(buf.Bytes(), []byte{'\n'})
    513 			for i, line := range lines {
    514 				if len(line) == 0 {
    515 					continue // empty lines don't have indentation
    516 				}
    517 				n := 0
    518 				for j, b := range line {
    519 					if b != '\t' {
    520 						// end of indentation
    521 						n = j
    522 						break
    523 					}
    524 				}
    525 				if n < indent {
    526 					t.Errorf("line %d: got only %d tabs; want at least %d: %q", i, n, indent, line)
    527 				}
    528 			}
    529 		})
    530 	}
    531 }
    532 
    533 // TestFuncType tests that an ast.FuncType with a nil Params field
    534 // can be printed (per go/ast specification). Test case for issue 3870.
    535 func TestFuncType(t *testing.T) {
    536 	src := &ast.File{
    537 		Name: &ast.Ident{Name: "p"},
    538 		Decls: []ast.Decl{
    539 			&ast.FuncDecl{
    540 				Name: &ast.Ident{Name: "f"},
    541 				Type: &ast.FuncType{},
    542 			},
    543 		},
    544 	}
    545 
    546 	var buf bytes.Buffer
    547 	if err := Fprint(&buf, fset, src); err != nil {
    548 		t.Fatal(err)
    549 	}
    550 	got := buf.String()
    551 
    552 	const want = `package p
    553 
    554 func f()
    555 `
    556 
    557 	if got != want {
    558 		t.Fatalf("got:\n%s\nwant:\n%s\n", got, want)
    559 	}
    560 }
    561 
    562 type limitWriter struct {
    563 	remaining int
    564 	errCount  int
    565 }
    566 
    567 func (l *limitWriter) Write(buf []byte) (n int, err error) {
    568 	n = len(buf)
    569 	if n >= l.remaining {
    570 		n = l.remaining
    571 		err = io.EOF
    572 		l.errCount++
    573 	}
    574 	l.remaining -= n
    575 	return n, err
    576 }
    577 
    578 // Test whether the printer stops writing after the first error
    579 func TestWriteErrors(t *testing.T) {
    580 	t.Parallel()
    581 	const filename = "printer.go"
    582 	src, err := ioutil.ReadFile(filename)
    583 	if err != nil {
    584 		panic(err) // error in test
    585 	}
    586 	file, err := parser.ParseFile(fset, filename, src, 0)
    587 	if err != nil {
    588 		panic(err) // error in test
    589 	}
    590 	for i := 0; i < 20; i++ {
    591 		lw := &limitWriter{remaining: i}
    592 		err := (&Config{Mode: RawFormat}).Fprint(lw, fset, file)
    593 		if lw.errCount > 1 {
    594 			t.Fatal("Writes continued after first error returned")
    595 		}
    596 		// We expect errCount be 1 iff err is set
    597 		if (lw.errCount != 0) != (err != nil) {
    598 			t.Fatal("Expected err when errCount != 0")
    599 		}
    600 	}
    601 }
    602 
    603 // TextX is a skeleton test that can be filled in for debugging one-off cases.
    604 // Do not remove.
    605 func TestX(t *testing.T) {
    606 	const src = `
    607 package p
    608 func _() {}
    609 `
    610 	_, err := format([]byte(src), 0)
    611 	if err != nil {
    612 		t.Error(err)
    613 	}
    614 }
    615