check_test.go raw

   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  // Except for manual tests, each package is type-checked twice, once without
 127  // use of Alias types, and once with Alias types.
 128  //
 129  // The srcs slice contains the file content for the files named in the
 130  // filenames slice. The colDelta parameter specifies the tolerance for position
 131  // mismatch when comparing errors. The manual parameter specifies whether this
 132  // is a 'manual' test.
 133  //
 134  // If provided, opts may be used to mutate the Config before type-checking.
 135  func testFiles(t *testing.T, filenames []string, srcs [][]byte, manual bool, opts ...func(*Config)) {
 136  	// Alias types are enabled by default
 137  	testFilesImpl(t, filenames, srcs, manual, opts...)
 138  	if !manual {
 139  		t.Setenv("GODEBUG", "gotypesalias=0")
 140  		testFilesImpl(t, filenames, srcs, manual, opts...)
 141  	}
 142  }
 143  
 144  func testFilesImpl(t *testing.T, filenames []string, srcs [][]byte, manual bool, opts ...func(*Config)) {
 145  	if len(filenames) == 0 {
 146  		t.Fatal("no source files")
 147  	}
 148  
 149  	// parse files
 150  	files, errlist := parseFiles(t, filenames, srcs, parser.AllErrors)
 151  	pkgName := "<no package>"
 152  	if len(files) > 0 {
 153  		pkgName = files[0].Name.Name
 154  	}
 155  	listErrors := manual && !*verifyErrors
 156  	if listErrors && len(errlist) > 0 {
 157  		t.Errorf("--- %s:", pkgName)
 158  		for _, err := range errlist {
 159  			t.Error(err)
 160  		}
 161  	}
 162  
 163  	// set up typechecker
 164  	var conf Config
 165  	*boolFieldAddr(&conf, "_Trace") = manual && testing.Verbose()
 166  	conf.Importer = defaultImporter(fset)
 167  	conf.Error = func(err error) {
 168  		if *haltOnError {
 169  			defer panic(err)
 170  		}
 171  		if listErrors {
 172  			t.Error(err)
 173  			return
 174  		}
 175  		// Ignore secondary error messages starting with "\t";
 176  		// they are clarifying messages for a primary error.
 177  		if !strings.Contains(err.Error(), ": \t") {
 178  			errlist = append(errlist, err)
 179  		}
 180  	}
 181  
 182  	// apply custom configuration
 183  	for _, opt := range opts {
 184  		opt(&conf)
 185  	}
 186  
 187  	// apply flag setting (overrides custom configuration)
 188  	var goexperiment, gotypesalias string
 189  	flags := flag.NewFlagSet("", flag.PanicOnError)
 190  	flags.StringVar(&conf.GoVersion, "lang", "", "")
 191  	flags.StringVar(&goexperiment, "goexperiment", "", "")
 192  	flags.BoolVar(&conf.FakeImportC, "fakeImportC", false, "")
 193  	flags.StringVar(&gotypesalias, "gotypesalias", "", "")
 194  	if err := parseFlags(srcs[0], flags); err != nil {
 195  		t.Fatal(err)
 196  	}
 197  
 198  	if goexperiment != "" {
 199  		revert := setGOEXPERIMENT(goexperiment)
 200  		defer revert()
 201  	}
 202  
 203  	// By default, gotypesalias is not set.
 204  	if gotypesalias != "" {
 205  		t.Setenv("GODEBUG", "gotypesalias="+gotypesalias)
 206  	}
 207  
 208  	// Provide Config.Info with all maps so that info recording is tested.
 209  	info := Info{
 210  		Types:        make(map[ast.Expr]TypeAndValue),
 211  		Instances:    make(map[*ast.Ident]Instance),
 212  		Defs:         make(map[*ast.Ident]Object),
 213  		Uses:         make(map[*ast.Ident]Object),
 214  		Implicits:    make(map[ast.Node]Object),
 215  		Selections:   make(map[*ast.SelectorExpr]*Selection),
 216  		Scopes:       make(map[ast.Node]*Scope),
 217  		FileVersions: make(map[*ast.File]string),
 218  	}
 219  
 220  	// typecheck
 221  	conf.Check(pkgName, fset, files, &info)
 222  	if listErrors {
 223  		return
 224  	}
 225  
 226  	// collect expected errors
 227  	errmap := make(map[string]map[int][]comment)
 228  	for i, filename := range filenames {
 229  		if m := commentMap(srcs[i], regexp.MustCompile("^ ERRORx? ")); len(m) > 0 {
 230  			errmap[filename] = m
 231  		}
 232  	}
 233  
 234  	// match against found errors
 235  	var indices []int // list indices of matching errors, reused for each error
 236  	for _, err := range errlist {
 237  		gotPos, gotMsg := unpackError(fset, err)
 238  
 239  		// find list of errors for the respective error line
 240  		filename := gotPos.Filename
 241  		filemap := errmap[filename]
 242  		line := gotPos.Line
 243  		var errList []comment
 244  		if filemap != nil {
 245  			errList = filemap[line]
 246  		}
 247  
 248  		// At least one of the errors in errList should match the current error.
 249  		indices = indices[:0]
 250  		for i, want := range errList {
 251  			pattern, substr := strings.CutPrefix(want.text, " ERROR ")
 252  			if !substr {
 253  				var found bool
 254  				pattern, found = strings.CutPrefix(want.text, " ERRORx ")
 255  				if !found {
 256  					panic("unreachable")
 257  				}
 258  			}
 259  			unquoted, err := strconv.Unquote(strings.TrimSpace(pattern))
 260  			if err != nil {
 261  				t.Errorf("%s:%d:%d: invalid ERROR pattern (cannot unquote %s)", filename, line, want.col, pattern)
 262  				continue
 263  			}
 264  			if substr {
 265  				if !strings.Contains(gotMsg, unquoted) {
 266  					continue
 267  				}
 268  			} else {
 269  				rx, err := regexp.Compile(unquoted)
 270  				if err != nil {
 271  					t.Errorf("%s:%d:%d: %v", filename, line, want.col, err)
 272  					continue
 273  				}
 274  				if !rx.MatchString(gotMsg) {
 275  					continue
 276  				}
 277  			}
 278  			indices = append(indices, i)
 279  		}
 280  		if len(indices) == 0 {
 281  			t.Errorf("%s: no error expected: %q", gotPos, gotMsg)
 282  			continue
 283  		}
 284  		// len(indices) > 0
 285  
 286  		// If there are multiple matching errors, select the one with the closest column position.
 287  		index := -1 // index of matching error
 288  		var delta int
 289  		for _, i := range indices {
 290  			if d := absDiff(gotPos.Column, errList[i].col); index < 0 || d < delta {
 291  				index, delta = i, d
 292  			}
 293  		}
 294  
 295  		// The closest column position must be within expected colDelta.
 296  		const colDelta = 0 // go/types errors are positioned correctly
 297  		if delta > colDelta {
 298  			t.Errorf("%s: got col = %d; want %d", gotPos, gotPos.Column, errList[index].col)
 299  		}
 300  
 301  		// eliminate from errList
 302  		if n := len(errList) - 1; n > 0 {
 303  			// not the last entry - slide entries down (don't reorder)
 304  			copy(errList[index:], errList[index+1:])
 305  			filemap[line] = errList[:n]
 306  		} else {
 307  			// last entry - remove errList from filemap
 308  			delete(filemap, line)
 309  		}
 310  
 311  		// if filemap is empty, eliminate from errmap
 312  		if len(filemap) == 0 {
 313  			delete(errmap, filename)
 314  		}
 315  	}
 316  
 317  	// there should be no expected errors left
 318  	if len(errmap) > 0 {
 319  		t.Errorf("--- %s: unreported errors:", pkgName)
 320  		for filename, filemap := range errmap {
 321  			for line, errList := range filemap {
 322  				for _, err := range errList {
 323  					t.Errorf("%s:%d:%d: %s", filename, line, err.col, err.text)
 324  				}
 325  			}
 326  		}
 327  	}
 328  }
 329  
 330  func readCode(err Error) errors.Code {
 331  	v := reflect.ValueOf(err)
 332  	return errors.Code(v.FieldByName("go116code").Int())
 333  }
 334  
 335  // boolFieldAddr(conf, name) returns the address of the boolean field conf.<name>.
 336  // For accessing unexported fields.
 337  func boolFieldAddr(conf *Config, name string) *bool {
 338  	v := reflect.Indirect(reflect.ValueOf(conf))
 339  	return (*bool)(v.FieldByName(name).Addr().UnsafePointer())
 340  }
 341  
 342  // stringFieldAddr(conf, name) returns the address of the string field conf.<name>.
 343  // For accessing unexported fields.
 344  func stringFieldAddr(conf *Config, name string) *string {
 345  	v := reflect.Indirect(reflect.ValueOf(conf))
 346  	return (*string)(v.FieldByName(name).Addr().UnsafePointer())
 347  }
 348  
 349  // setGOEXPERIMENT overwrites the existing buildcfg.Experiment with a new one
 350  // based on the provided goexperiment string. Calling the result function
 351  // (typically via defer), reverts buildcfg.Experiment to the prior value.
 352  // For testing use, only.
 353  func setGOEXPERIMENT(goexperiment string) func() {
 354  	exp, err := buildcfg.ParseGOEXPERIMENT(runtime.GOOS, runtime.GOARCH, goexperiment)
 355  	if err != nil {
 356  		panic(err)
 357  	}
 358  	old := buildcfg.Experiment
 359  	buildcfg.Experiment = *exp
 360  	return func() { buildcfg.Experiment = old }
 361  }
 362  
 363  // TestManual is for manual testing of a package - either provided
 364  // as a list of filenames belonging to the package, or a directory
 365  // name containing the package files - after the test arguments
 366  // (and a separating "--"). For instance, to test the package made
 367  // of the files foo.go and bar.go, use:
 368  //
 369  //	go test -run Manual -- foo.go bar.go
 370  //
 371  // If no source arguments are provided, the file testdata/manual.go
 372  // is used instead.
 373  // Provide the -verify flag to verify errors against ERROR comments
 374  // in the input files rather than having a list of errors reported.
 375  // The accepted Go language version can be controlled with the -lang
 376  // flag.
 377  func TestManual(t *testing.T) {
 378  	testenv.MustHaveGoBuild(t)
 379  
 380  	filenames := flag.Args()
 381  	if len(filenames) == 0 {
 382  		filenames = []string{filepath.FromSlash("testdata/manual.go")}
 383  	}
 384  
 385  	info, err := os.Stat(filenames[0])
 386  	if err != nil {
 387  		t.Fatalf("TestManual: %v", err)
 388  	}
 389  
 390  	DefPredeclaredTestFuncs()
 391  	if info.IsDir() {
 392  		if len(filenames) > 1 {
 393  			t.Fatal("TestManual: must have only one directory argument")
 394  		}
 395  		testDir(t, filenames[0], true)
 396  	} else {
 397  		testPkg(t, filenames, true)
 398  	}
 399  }
 400  
 401  func TestLongConstants(t *testing.T) {
 402  	format := `package longconst; const _ = %s /* ERROR "constant overflow" */; const _ = %s // ERROR "excessively long constant"`
 403  	src := fmt.Sprintf(format, strings.Repeat("1", 9999), strings.Repeat("1", 10001))
 404  	testFiles(t, []string{"longconst.go"}, [][]byte{[]byte(src)}, false)
 405  }
 406  
 407  func withSizes(sizes Sizes) func(*Config) {
 408  	return func(cfg *Config) {
 409  		cfg.Sizes = sizes
 410  	}
 411  }
 412  
 413  // TestIndexRepresentability tests that constant index operands must
 414  // be representable as int even if they already have a type that can
 415  // represent larger values.
 416  func TestIndexRepresentability(t *testing.T) {
 417  	const src = `package index; var s []byte; var _ = s[int64 /* ERRORx "int64\\(1\\) << 40 \\(.*\\) overflows int" */ (1) << 40]`
 418  	testFiles(t, []string{"index.go"}, [][]byte{[]byte(src)}, false, withSizes(&StdSizes{4, 4}))
 419  }
 420  
 421  func TestIssue47243_TypedRHS(t *testing.T) {
 422  	// The RHS of the shift expression below overflows uint on 32bit platforms,
 423  	// but this is OK as it is explicitly typed.
 424  	const src = `package issue47243; var a uint64; var _ = a << uint64(4294967296)` // uint64(1<<32)
 425  	testFiles(t, []string{"p.go"}, [][]byte{[]byte(src)}, false, withSizes(&StdSizes{4, 4}))
 426  }
 427  
 428  func TestCheck(t *testing.T) {
 429  	old := buildcfg.Experiment.RangeFunc
 430  	defer func() {
 431  		buildcfg.Experiment.RangeFunc = old
 432  	}()
 433  	buildcfg.Experiment.RangeFunc = true
 434  
 435  	DefPredeclaredTestFuncs()
 436  	testDirFiles(t, "../../internal/types/testdata/check", false)
 437  }
 438  func TestSpec(t *testing.T)      { testDirFiles(t, "../../internal/types/testdata/spec", false) }
 439  func TestExamples(t *testing.T)  { testDirFiles(t, "../../internal/types/testdata/examples", false) }
 440  func TestFixedbugs(t *testing.T) { testDirFiles(t, "../../internal/types/testdata/fixedbugs", false) }
 441  func TestLocal(t *testing.T)     { testDirFiles(t, "testdata/local", false) }
 442  
 443  func testDirFiles(t *testing.T, dir string, manual bool) {
 444  	testenv.MustHaveGoBuild(t)
 445  	dir = filepath.FromSlash(dir)
 446  
 447  	fis, err := os.ReadDir(dir)
 448  	if err != nil {
 449  		t.Error(err)
 450  		return
 451  	}
 452  
 453  	for _, fi := range fis {
 454  		path := filepath.Join(dir, fi.Name())
 455  
 456  		// If fi is a directory, its files make up a single package.
 457  		if fi.IsDir() {
 458  			testDir(t, path, manual)
 459  		} else {
 460  			t.Run(filepath.Base(path), func(t *testing.T) {
 461  				testPkg(t, []string{path}, manual)
 462  			})
 463  		}
 464  	}
 465  }
 466  
 467  func testDir(t *testing.T, dir string, manual bool) {
 468  	testenv.MustHaveGoBuild(t)
 469  
 470  	fis, err := os.ReadDir(dir)
 471  	if err != nil {
 472  		t.Error(err)
 473  		return
 474  	}
 475  
 476  	var filenames []string
 477  	for _, fi := range fis {
 478  		filenames = append(filenames, filepath.Join(dir, fi.Name()))
 479  	}
 480  
 481  	t.Run(filepath.Base(dir), func(t *testing.T) {
 482  		testPkg(t, filenames, manual)
 483  	})
 484  }
 485  
 486  func testPkg(t *testing.T, filenames []string, manual bool) {
 487  	srcs := make([][]byte, len(filenames))
 488  	for i, filename := range filenames {
 489  		src, err := os.ReadFile(filename)
 490  		if err != nil {
 491  			t.Fatalf("could not read %s: %v", filename, err)
 492  		}
 493  		srcs[i] = src
 494  	}
 495  	testFiles(t, filenames, srcs, manual)
 496  }
 497