Source file src/go/types/check_test.go

     1  // Copyright 2011 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  // This file implements a typechecker test harness. The packages specified
     6  // in tests are typechecked. Error messages reported by the typechecker are
     7  // compared against the errors expected in the test files.
     8  //
     9  // Expected errors are indicated in the test files by putting comments
    10  // of the form /* ERROR pattern */ or /* ERRORx pattern */ (or a similar
    11  // //-style line comment) immediately following the tokens where errors
    12  // are reported. There must be exactly one blank before and after the
    13  // ERROR/ERRORx indicator, and the pattern must be a properly quoted Go
    14  // string.
    15  //
    16  // The harness will verify that each ERROR pattern is a substring of the
    17  // error reported at that source position, and that each ERRORx pattern
    18  // is a regular expression matching the respective error.
    19  // Consecutive comments may be used to indicate multiple errors reported
    20  // at the same position.
    21  //
    22  // For instance, the following test source indicates that an "undeclared"
    23  // error should be reported for the undeclared variable x:
    24  //
    25  //	package p
    26  //	func f() {
    27  //		_ = x /* ERROR "undeclared" */ + 1
    28  //	}
    29  
    30  package types_test
    31  
    32  import (
    33  	"bytes"
    34  	"flag"
    35  	"fmt"
    36  	"go/ast"
    37  	"go/parser"
    38  	"go/scanner"
    39  	"go/token"
    40  	"internal/buildcfg"
    41  	"internal/testenv"
    42  	"internal/types/errors"
    43  	"os"
    44  	"path/filepath"
    45  	"reflect"
    46  	"regexp"
    47  	"runtime"
    48  	"strconv"
    49  	"strings"
    50  	"testing"
    51  
    52  	. "go/types"
    53  )
    54  
    55  var (
    56  	haltOnError  = flag.Bool("halt", false, "halt on error")
    57  	verifyErrors = flag.Bool("verify", false, "verify errors (rather than list them) in TestManual")
    58  )
    59  
    60  var fset = token.NewFileSet()
    61  
    62  func parseFiles(t *testing.T, filenames []string, srcs [][]byte, mode parser.Mode) ([]*ast.File, []error) {
    63  	var files []*ast.File
    64  	var errlist []error
    65  	for i, filename := range filenames {
    66  		file, err := parser.ParseFile(fset, filename, srcs[i], mode)
    67  		if file == nil {
    68  			t.Fatalf("%s: %s", filename, err)
    69  		}
    70  		files = append(files, file)
    71  		if err != nil {
    72  			if list, _ := err.(scanner.ErrorList); len(list) > 0 {
    73  				for _, err := range list {
    74  					errlist = append(errlist, err)
    75  				}
    76  			} else {
    77  				errlist = append(errlist, err)
    78  			}
    79  		}
    80  	}
    81  	return files, errlist
    82  }
    83  
    84  func unpackError(fset *token.FileSet, err error) (token.Position, string) {
    85  	switch err := err.(type) {
    86  	case *scanner.Error:
    87  		return err.Pos, err.Msg
    88  	case Error:
    89  		return fset.Position(err.Pos), err.Msg
    90  	}
    91  	panic("unreachable")
    92  }
    93  
    94  // absDiff returns the absolute difference between x and y.
    95  func absDiff(x, y int) int {
    96  	if x < y {
    97  		return y - x
    98  	}
    99  	return x - y
   100  }
   101  
   102  // parseFlags parses flags from the first line of the given source if the line
   103  // starts with "//" (line comment) followed by "-" (possibly with spaces
   104  // between). Otherwise the line is ignored.
   105  func parseFlags(src []byte, flags *flag.FlagSet) error {
   106  	// we must have a line comment that starts with a "-"
   107  	const prefix = "//"
   108  	if !bytes.HasPrefix(src, []byte(prefix)) {
   109  		return nil // first line is not a line comment
   110  	}
   111  	src = src[len(prefix):]
   112  	if i := bytes.Index(src, []byte("-")); i < 0 || len(bytes.TrimSpace(src[:i])) != 0 {
   113  		return nil // comment doesn't start with a "-"
   114  	}
   115  	end := bytes.Index(src, []byte("\n"))
   116  	const maxLen = 256
   117  	if end < 0 || end > maxLen {
   118  		return fmt.Errorf("flags comment line too long")
   119  	}
   120  
   121  	return flags.Parse(strings.Fields(string(src[:end])))
   122  }
   123  
   124  // testFiles type-checks the package consisting of the given files, and
   125  // compares the resulting errors with the ERROR annotations in the source.
   126  //
   127  // The srcs slice contains the file content for the files named in the
   128  // filenames slice. The colDelta parameter specifies the tolerance for position
   129  // mismatch when comparing errors. The manual parameter specifies whether this
   130  // is a 'manual' test.
   131  //
   132  // If provided, opts may be used to mutate the Config before type-checking.
   133  func testFiles(t *testing.T, filenames []string, srcs [][]byte, manual bool, opts ...func(*Config)) {
   134  	if len(filenames) == 0 {
   135  		t.Fatal("no source files")
   136  	}
   137  
   138  	// parse files
   139  	files, errlist := parseFiles(t, filenames, srcs, parser.AllErrors)
   140  	pkgName := "<no package>"
   141  	if len(files) > 0 {
   142  		pkgName = files[0].Name.Name
   143  	}
   144  	listErrors := manual && !*verifyErrors
   145  	if listErrors && len(errlist) > 0 {
   146  		t.Errorf("--- %s:", pkgName)
   147  		for _, err := range errlist {
   148  			t.Error(err)
   149  		}
   150  	}
   151  
   152  	// set up typechecker
   153  	var conf Config
   154  	*boolFieldAddr(&conf, "_Trace") = manual && testing.Verbose()
   155  	conf.Importer = defaultImporter(fset)
   156  	conf.Error = func(err error) {
   157  		if *haltOnError {
   158  			defer panic(err)
   159  		}
   160  		if listErrors {
   161  			t.Error(err)
   162  			return
   163  		}
   164  		// Ignore secondary error messages starting with "\t";
   165  		// they are clarifying messages for a primary error.
   166  		if !strings.Contains(err.Error(), ": \t") {
   167  			errlist = append(errlist, err)
   168  		}
   169  	}
   170  
   171  	// apply custom configuration
   172  	for _, opt := range opts {
   173  		opt(&conf)
   174  	}
   175  
   176  	// apply flag setting (overrides custom configuration)
   177  	var goexperiment string
   178  	flags := flag.NewFlagSet("", flag.PanicOnError)
   179  	flags.StringVar(&conf.GoVersion, "lang", "", "")
   180  	flags.StringVar(&goexperiment, "goexperiment", "", "")
   181  	flags.BoolVar(&conf.FakeImportC, "fakeImportC", false, "")
   182  	if err := parseFlags(srcs[0], flags); err != nil {
   183  		t.Fatal(err)
   184  	}
   185  
   186  	if goexperiment != "" {
   187  		revert := setGOEXPERIMENT(goexperiment)
   188  		defer revert()
   189  	}
   190  
   191  	// Provide Config.Info with all maps so that info recording is tested.
   192  	info := Info{
   193  		Types:        make(map[ast.Expr]TypeAndValue),
   194  		Instances:    make(map[*ast.Ident]Instance),
   195  		Defs:         make(map[*ast.Ident]Object),
   196  		Uses:         make(map[*ast.Ident]Object),
   197  		Implicits:    make(map[ast.Node]Object),
   198  		Selections:   make(map[*ast.SelectorExpr]*Selection),
   199  		Scopes:       make(map[ast.Node]*Scope),
   200  		FileVersions: make(map[*ast.File]string),
   201  	}
   202  
   203  	// typecheck
   204  	conf.Check(pkgName, fset, files, &info)
   205  	if listErrors {
   206  		return
   207  	}
   208  
   209  	// collect expected errors
   210  	errmap := make(map[string]map[int][]comment)
   211  	for i, filename := range filenames {
   212  		if m := commentMap(srcs[i], regexp.MustCompile("^ ERRORx? ")); len(m) > 0 {
   213  			errmap[filename] = m
   214  		}
   215  	}
   216  
   217  	// match against found errors
   218  	var indices []int // list indices of matching errors, reused for each error
   219  	for _, err := range errlist {
   220  		gotPos, gotMsg := unpackError(fset, err)
   221  
   222  		// find list of errors for the respective error line
   223  		filename := gotPos.Filename
   224  		filemap := errmap[filename]
   225  		line := gotPos.Line
   226  		var errList []comment
   227  		if filemap != nil {
   228  			errList = filemap[line]
   229  		}
   230  
   231  		// At least one of the errors in errList should match the current error.
   232  		indices = indices[:0]
   233  		for i, want := range errList {
   234  			pattern, substr := strings.CutPrefix(want.text, " ERROR ")
   235  			if !substr {
   236  				var found bool
   237  				pattern, found = strings.CutPrefix(want.text, " ERRORx ")
   238  				if !found {
   239  					panic("unreachable")
   240  				}
   241  			}
   242  			unquoted, err := strconv.Unquote(strings.TrimSpace(pattern))
   243  			if err != nil {
   244  				t.Errorf("%s:%d:%d: invalid ERROR pattern (cannot unquote %s)", filename, line, want.col, pattern)
   245  				continue
   246  			}
   247  			if substr {
   248  				if !strings.Contains(gotMsg, unquoted) {
   249  					continue
   250  				}
   251  			} else {
   252  				rx, err := regexp.Compile(unquoted)
   253  				if err != nil {
   254  					t.Errorf("%s:%d:%d: %v", filename, line, want.col, err)
   255  					continue
   256  				}
   257  				if !rx.MatchString(gotMsg) {
   258  					continue
   259  				}
   260  			}
   261  			indices = append(indices, i)
   262  		}
   263  		if len(indices) == 0 {
   264  			t.Errorf("%s: no error expected: %q", gotPos, gotMsg)
   265  			continue
   266  		}
   267  		// len(indices) > 0
   268  
   269  		// If there are multiple matching errors, select the one with the closest column position.
   270  		index := -1 // index of matching error
   271  		var delta int
   272  		for _, i := range indices {
   273  			if d := absDiff(gotPos.Column, errList[i].col); index < 0 || d < delta {
   274  				index, delta = i, d
   275  			}
   276  		}
   277  
   278  		// The closest column position must be within expected colDelta.
   279  		const colDelta = 0 // go/types errors are positioned correctly
   280  		if delta > colDelta {
   281  			t.Errorf("%s: got col = %d; want %d", gotPos, gotPos.Column, errList[index].col)
   282  		}
   283  
   284  		// eliminate from errList
   285  		if n := len(errList) - 1; n > 0 {
   286  			// not the last entry - slide entries down (don't reorder)
   287  			copy(errList[index:], errList[index+1:])
   288  			filemap[line] = errList[:n]
   289  		} else {
   290  			// last entry - remove errList from filemap
   291  			delete(filemap, line)
   292  		}
   293  
   294  		// if filemap is empty, eliminate from errmap
   295  		if len(filemap) == 0 {
   296  			delete(errmap, filename)
   297  		}
   298  	}
   299  
   300  	// there should be no expected errors left
   301  	if len(errmap) > 0 {
   302  		t.Errorf("--- %s: unreported errors:", pkgName)
   303  		for filename, filemap := range errmap {
   304  			for line, errList := range filemap {
   305  				for _, err := range errList {
   306  					t.Errorf("%s:%d:%d: %s", filename, line, err.col, err.text)
   307  				}
   308  			}
   309  		}
   310  	}
   311  }
   312  
   313  func readCode(err Error) errors.Code {
   314  	v := reflect.ValueOf(err)
   315  	return errors.Code(v.FieldByName("go116code").Int())
   316  }
   317  
   318  // boolFieldAddr(conf, name) returns the address of the boolean field conf.<name>.
   319  // For accessing unexported fields.
   320  func boolFieldAddr(conf *Config, name string) *bool {
   321  	v := reflect.Indirect(reflect.ValueOf(conf))
   322  	return (*bool)(v.FieldByName(name).Addr().UnsafePointer())
   323  }
   324  
   325  // stringFieldAddr(conf, name) returns the address of the string field conf.<name>.
   326  // For accessing unexported fields.
   327  func stringFieldAddr(conf *Config, name string) *string {
   328  	v := reflect.Indirect(reflect.ValueOf(conf))
   329  	return (*string)(v.FieldByName(name).Addr().UnsafePointer())
   330  }
   331  
   332  // setGOEXPERIMENT overwrites the existing buildcfg.Experiment with a new one
   333  // based on the provided goexperiment string. Calling the result function
   334  // (typically via defer), reverts buildcfg.Experiment to the prior value.
   335  // For testing use, only.
   336  func setGOEXPERIMENT(goexperiment string) func() {
   337  	exp, err := buildcfg.ParseGOEXPERIMENT(runtime.GOOS, runtime.GOARCH, goexperiment)
   338  	if err != nil {
   339  		panic(err)
   340  	}
   341  	old := buildcfg.Experiment
   342  	buildcfg.Experiment = *exp
   343  	return func() { buildcfg.Experiment = old }
   344  }
   345  
   346  // TestManual is for manual testing of a package - either provided
   347  // as a list of filenames belonging to the package, or a directory
   348  // name containing the package files - after the test arguments
   349  // (and a separating "--"). For instance, to test the package made
   350  // of the files foo.go and bar.go, use:
   351  //
   352  //	go test -run Manual -- foo.go bar.go
   353  //
   354  // If no source arguments are provided, the file testdata/manual.go
   355  // is used instead.
   356  // Provide the -verify flag to verify errors against ERROR comments
   357  // in the input files rather than having a list of errors reported.
   358  // The accepted Go language version can be controlled with the -lang
   359  // flag.
   360  func TestManual(t *testing.T) {
   361  	testenv.MustHaveGoBuild(t)
   362  
   363  	filenames := flag.Args()
   364  	if len(filenames) == 0 {
   365  		filenames = []string{filepath.FromSlash("testdata/manual.go")}
   366  	}
   367  
   368  	info, err := os.Stat(filenames[0])
   369  	if err != nil {
   370  		t.Fatalf("TestManual: %v", err)
   371  	}
   372  
   373  	DefPredeclaredTestFuncs()
   374  	if info.IsDir() {
   375  		if len(filenames) > 1 {
   376  			t.Fatal("TestManual: must have only one directory argument")
   377  		}
   378  		testDir(t, filenames[0], true)
   379  	} else {
   380  		testPkg(t, filenames, true)
   381  	}
   382  }
   383  
   384  func TestLongConstants(t *testing.T) {
   385  	format := `package longconst; const _ = %s /* ERROR "constant overflow" */; const _ = %s // ERROR "excessively long constant"`
   386  	src := fmt.Sprintf(format, strings.Repeat("1", 9999), strings.Repeat("1", 10001))
   387  	testFiles(t, []string{"longconst.go"}, [][]byte{[]byte(src)}, false)
   388  }
   389  
   390  func withSizes(sizes Sizes) func(*Config) {
   391  	return func(cfg *Config) {
   392  		cfg.Sizes = sizes
   393  	}
   394  }
   395  
   396  // TestIndexRepresentability tests that constant index operands must
   397  // be representable as int even if they already have a type that can
   398  // represent larger values.
   399  func TestIndexRepresentability(t *testing.T) {
   400  	const src = `package index; var s []byte; var _ = s[int64 /* ERRORx "int64\\(1\\) << 40 \\(.*\\) overflows int" */ (1) << 40]`
   401  	testFiles(t, []string{"index.go"}, [][]byte{[]byte(src)}, false, withSizes(&StdSizes{4, 4}))
   402  }
   403  
   404  func TestIssue47243_TypedRHS(t *testing.T) {
   405  	// The RHS of the shift expression below overflows uint on 32bit platforms,
   406  	// but this is OK as it is explicitly typed.
   407  	const src = `package issue47243; var a uint64; var _ = a << uint64(4294967296)` // uint64(1<<32)
   408  	testFiles(t, []string{"p.go"}, [][]byte{[]byte(src)}, false, withSizes(&StdSizes{4, 4}))
   409  }
   410  
   411  func TestCheck(t *testing.T) {
   412  	DefPredeclaredTestFuncs()
   413  	testDirFiles(t, "../../internal/types/testdata/check", false)
   414  }
   415  func TestSpec(t *testing.T)      { testDirFiles(t, "../../internal/types/testdata/spec", false) }
   416  func TestExamples(t *testing.T)  { testDirFiles(t, "../../internal/types/testdata/examples", false) }
   417  func TestFixedbugs(t *testing.T) { testDirFiles(t, "../../internal/types/testdata/fixedbugs", false) }
   418  func TestLocal(t *testing.T)     { testDirFiles(t, "testdata/local", false) }
   419  
   420  func testDirFiles(t *testing.T, dir string, manual bool) {
   421  	testenv.MustHaveGoBuild(t)
   422  	dir = filepath.FromSlash(dir)
   423  
   424  	fis, err := os.ReadDir(dir)
   425  	if err != nil {
   426  		t.Error(err)
   427  		return
   428  	}
   429  
   430  	for _, fi := range fis {
   431  		path := filepath.Join(dir, fi.Name())
   432  
   433  		// If fi is a directory, its files make up a single package.
   434  		if fi.IsDir() {
   435  			testDir(t, path, manual)
   436  		} else {
   437  			t.Run(filepath.Base(path), func(t *testing.T) {
   438  				testPkg(t, []string{path}, manual)
   439  			})
   440  		}
   441  	}
   442  }
   443  
   444  func testDir(t *testing.T, dir string, manual bool) {
   445  	testenv.MustHaveGoBuild(t)
   446  
   447  	fis, err := os.ReadDir(dir)
   448  	if err != nil {
   449  		t.Error(err)
   450  		return
   451  	}
   452  
   453  	var filenames []string
   454  	for _, fi := range fis {
   455  		filenames = append(filenames, filepath.Join(dir, fi.Name()))
   456  	}
   457  
   458  	t.Run(filepath.Base(dir), func(t *testing.T) {
   459  		testPkg(t, filenames, manual)
   460  	})
   461  }
   462  
   463  func testPkg(t *testing.T, filenames []string, manual bool) {
   464  	srcs := make([][]byte, len(filenames))
   465  	for i, filename := range filenames {
   466  		src, err := os.ReadFile(filename)
   467  		if err != nil {
   468  			t.Fatalf("could not read %s: %v", filename, err)
   469  		}
   470  		srcs[i] = src
   471  	}
   472  	testFiles(t, filenames, srcs, manual)
   473  }
   474  

View as plain text