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
5package printer
6
7import (
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
22const (
23	dataDir  = "testdata"
24	tabwidth = 8
25)
26
27var update = flag.Bool("update", false, "update golden files")
28
29var fset = token.NewFileSet()
30
31type checkMode uint
32
33const (
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.
42func 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.
77func 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.
86func 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
117func 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
162func 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
180type entry struct {
181	source, golden string
182	mode           checkMode
183}
184
185// Use go test -update to create/update the respective golden files.
186var 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
199func 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//
218func 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.
250func 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.
264func 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.
280func 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.
302func TestBadComments(t *testing.T) {
303	t.Parallel()
304	const src = `
305// first comment - text and position changed by test
306package p
307import "fmt"
308const pi = 3.14 // rough circle
309var (
310	x, y, z int = 1, 2, 3
311	u, v float64
312)
313func 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
338type visitor chan *ast.Ident
339
340func (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.
348func 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.
358func 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 directives
367// by testing that position information for matching identifiers
368// is maintained.
369func TestSourcePos(t *testing.T) {
370	const src = `
371package p
372import ( "go/printer"; "math" )
373const pi = 3.14; var x = 0
374type t struct{ x, y, z int; u, v, w float32 }
375func (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 directives 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// Verify that the SourcePos mode doesn't emit unnecessary //line directives
438// before empty lines.
439func TestIssue5945(t *testing.T) {
440	const orig = `
441package p   // line 2
442func f() {} // line 3
443
444var x, y, z int
445
446
447func g() { // line 8
448}
449`
450
451	const want = `//line src.go:2
452package p
453
454//line src.go:3
455func f() {}
456
457var x, y, z int
458
459//line src.go:8
460func g() {
461}
462`
463
464	// parse original
465	f1, err := parser.ParseFile(fset, "src.go", orig, 0)
466	if err != nil {
467		t.Fatal(err)
468	}
469
470	// pretty-print original
471	var buf bytes.Buffer
472	err = (&Config{Mode: UseSpaces | SourcePos, Tabwidth: 8}).Fprint(&buf, fset, f1)
473	if err != nil {
474		t.Fatal(err)
475	}
476	got := buf.String()
477
478	// compare original with desired output
479	if got != want {
480		t.Errorf("got:\n%s\nwant:\n%s\n", got, want)
481	}
482}
483
484var decls = []string{
485	`import "fmt"`,
486	"const pi = 3.1415\nconst e = 2.71828\n\nvar x = pi",
487	"func sum(x, y int) int\t{ return x + y }",
488}
489
490func TestDeclLists(t *testing.T) {
491	for _, src := range decls {
492		file, err := parser.ParseFile(fset, "", "package p;"+src, parser.ParseComments)
493		if err != nil {
494			panic(err) // error in test
495		}
496
497		var buf bytes.Buffer
498		err = Fprint(&buf, fset, file.Decls) // only print declarations
499		if err != nil {
500			panic(err) // error in test
501		}
502
503		out := buf.String()
504		if out != src {
505			t.Errorf("\ngot : %q\nwant: %q\n", out, src)
506		}
507	}
508}
509
510var stmts = []string{
511	"i := 0",
512	"select {}\nvar a, b = 1, 2\nreturn a + b",
513	"go f()\ndefer func() {}()",
514}
515
516func TestStmtLists(t *testing.T) {
517	for _, src := range stmts {
518		file, err := parser.ParseFile(fset, "", "package p; func _() {"+src+"}", parser.ParseComments)
519		if err != nil {
520			panic(err) // error in test
521		}
522
523		var buf bytes.Buffer
524		err = Fprint(&buf, fset, file.Decls[0].(*ast.FuncDecl).Body.List) // only print statements
525		if err != nil {
526			panic(err) // error in test
527		}
528
529		out := buf.String()
530		if out != src {
531			t.Errorf("\ngot : %q\nwant: %q\n", out, src)
532		}
533	}
534}
535
536func TestBaseIndent(t *testing.T) {
537	t.Parallel()
538	// The testfile must not contain multi-line raw strings since those
539	// are not indented (because their values must not change) and make
540	// this test fail.
541	const filename = "printer.go"
542	src, err := ioutil.ReadFile(filename)
543	if err != nil {
544		panic(err) // error in test
545	}
546
547	file, err := parser.ParseFile(fset, filename, src, 0)
548	if err != nil {
549		panic(err) // error in test
550	}
551
552	for indent := 0; indent < 4; indent++ {
553		indent := indent
554		t.Run(fmt.Sprint(indent), func(t *testing.T) {
555			t.Parallel()
556			var buf bytes.Buffer
557			(&Config{Tabwidth: tabwidth, Indent: indent}).Fprint(&buf, fset, file)
558			// all code must be indented by at least 'indent' tabs
559			lines := bytes.Split(buf.Bytes(), []byte{'\n'})
560			for i, line := range lines {
561				if len(line) == 0 {
562					continue // empty lines don't have indentation
563				}
564				n := 0
565				for j, b := range line {
566					if b != '\t' {
567						// end of indentation
568						n = j
569						break
570					}
571				}
572				if n < indent {
573					t.Errorf("line %d: got only %d tabs; want at least %d: %q", i, n, indent, line)
574				}
575			}
576		})
577	}
578}
579
580// TestFuncType tests that an ast.FuncType with a nil Params field
581// can be printed (per go/ast specification). Test case for issue 3870.
582func TestFuncType(t *testing.T) {
583	src := &ast.File{
584		Name: &ast.Ident{Name: "p"},
585		Decls: []ast.Decl{
586			&ast.FuncDecl{
587				Name: &ast.Ident{Name: "f"},
588				Type: &ast.FuncType{},
589			},
590		},
591	}
592
593	var buf bytes.Buffer
594	if err := Fprint(&buf, fset, src); err != nil {
595		t.Fatal(err)
596	}
597	got := buf.String()
598
599	const want = `package p
600
601func f()
602`
603
604	if got != want {
605		t.Fatalf("got:\n%s\nwant:\n%s\n", got, want)
606	}
607}
608
609type limitWriter struct {
610	remaining int
611	errCount  int
612}
613
614func (l *limitWriter) Write(buf []byte) (n int, err error) {
615	n = len(buf)
616	if n >= l.remaining {
617		n = l.remaining
618		err = io.EOF
619		l.errCount++
620	}
621	l.remaining -= n
622	return n, err
623}
624
625// Test whether the printer stops writing after the first error
626func TestWriteErrors(t *testing.T) {
627	t.Parallel()
628	const filename = "printer.go"
629	src, err := ioutil.ReadFile(filename)
630	if err != nil {
631		panic(err) // error in test
632	}
633	file, err := parser.ParseFile(fset, filename, src, 0)
634	if err != nil {
635		panic(err) // error in test
636	}
637	for i := 0; i < 20; i++ {
638		lw := &limitWriter{remaining: i}
639		err := (&Config{Mode: RawFormat}).Fprint(lw, fset, file)
640		if lw.errCount > 1 {
641			t.Fatal("Writes continued after first error returned")
642		}
643		// We expect errCount be 1 iff err is set
644		if (lw.errCount != 0) != (err != nil) {
645			t.Fatal("Expected err when errCount != 0")
646		}
647	}
648}
649
650// TextX is a skeleton test that can be filled in for debugging one-off cases.
651// Do not remove.
652func TestX(t *testing.T) {
653	const src = `
654package p
655func _() {}
656`
657	_, err := format([]byte(src), 0)
658	if err != nil {
659		t.Error(err)
660	}
661}
662
663func TestCommentedNode(t *testing.T) {
664	const (
665		input = `package main
666
667func foo() {
668	// comment inside func
669}
670
671// leading comment
672type bar int // comment2
673
674`
675
676		foo = `func foo() {
677	// comment inside func
678}`
679
680		bar = `// leading comment
681type bar int	// comment2
682`
683	)
684
685	fset := token.NewFileSet()
686	f, err := parser.ParseFile(fset, "input.go", input, parser.ParseComments)
687	if err != nil {
688		t.Fatal(err)
689	}
690
691	var buf bytes.Buffer
692
693	err = Fprint(&buf, fset, &CommentedNode{Node: f.Decls[0], Comments: f.Comments})
694	if err != nil {
695		t.Fatal(err)
696	}
697
698	if buf.String() != foo {
699		t.Errorf("got %q, want %q", buf.String(), foo)
700	}
701
702	buf.Reset()
703
704	err = Fprint(&buf, fset, &CommentedNode{Node: f.Decls[1], Comments: f.Comments})
705	if err != nil {
706		t.Fatal(err)
707	}
708
709	if buf.String() != bar {
710		t.Errorf("got %q, want %q", buf.String(), bar)
711	}
712}
713