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  }