...

Source file src/go/types/check_test.go

Documentation: go/types

		 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 error messages expected in the test files.
		 8  //
		 9  // Expected errors are indicated in the test files by putting a comment
		10  // of the form /* ERROR "rx" */ immediately following an offending token.
		11  // The harness will verify that an error matching the regular expression
		12  // rx is reported at that source position. Consecutive comments may be
		13  // used to indicate multiple errors for the same token position.
		14  //
		15  // For instance, the following test file indicates that a "not declared"
		16  // error should be reported for the undeclared variable x:
		17  //
		18  //	package p
		19  //	func f() {
		20  //		_ = x /* ERROR "not declared" */ + 1
		21  //	}
		22  
		23  // TODO(gri) Also collect strict mode errors of the form /* STRICT ... */
		24  //					 and test against strict mode.
		25  
		26  package types_test
		27  
		28  import (
		29  	"flag"
		30  	"fmt"
		31  	"go/ast"
		32  	"go/importer"
		33  	"go/internal/typeparams"
		34  	"go/parser"
		35  	"go/scanner"
		36  	"go/token"
		37  	"internal/testenv"
		38  	"os"
		39  	"path/filepath"
		40  	"regexp"
		41  	"strings"
		42  	"testing"
		43  
		44  	. "go/types"
		45  )
		46  
		47  var (
		48  	haltOnError	= flag.Bool("halt", false, "halt on error")
		49  	verifyErrors = flag.Bool("verify", false, "verify errors (rather than list them) in TestManual")
		50  	goVersion		= flag.String("lang", "", "Go language version (e.g. \"go1.12\") for TestManual")
		51  )
		52  
		53  var fset = token.NewFileSet()
		54  
		55  // Positioned errors are of the form filename:line:column: message .
		56  var posMsgRx = regexp.MustCompile(`^(.*:[0-9]+:[0-9]+): *(.*)`)
		57  
		58  // splitError splits an error's error message into a position string
		59  // and the actual error message. If there's no position information,
		60  // pos is the empty string, and msg is the entire error message.
		61  //
		62  func splitError(err error) (pos, msg string) {
		63  	msg = err.Error()
		64  	if m := posMsgRx.FindStringSubmatch(msg); len(m) == 3 {
		65  		pos = m[1]
		66  		msg = m[2]
		67  	}
		68  	return
		69  }
		70  
		71  func parseFiles(t *testing.T, filenames []string, srcs [][]byte, mode parser.Mode) ([]*ast.File, []error) {
		72  	var files []*ast.File
		73  	var errlist []error
		74  	for i, filename := range filenames {
		75  		file, err := parser.ParseFile(fset, filename, srcs[i], mode)
		76  		if file == nil {
		77  			t.Fatalf("%s: %s", filename, err)
		78  		}
		79  		files = append(files, file)
		80  		if err != nil {
		81  			if list, _ := err.(scanner.ErrorList); len(list) > 0 {
		82  				for _, err := range list {
		83  					errlist = append(errlist, err)
		84  				}
		85  			} else {
		86  				errlist = append(errlist, err)
		87  			}
		88  		}
		89  	}
		90  	return files, errlist
		91  }
		92  
		93  // ERROR comments must start with text `ERROR "rx"` or `ERROR rx` where
		94  // rx is a regular expression that matches the expected error message.
		95  // Space around "rx" or rx is ignored. Use the form `ERROR HERE "rx"`
		96  // for error messages that are located immediately after rather than
		97  // at a token's position.
		98  //
		99  var errRx = regexp.MustCompile(`^ *ERROR *(HERE)? *"?([^"]*)"?`)
	 100  
	 101  // errMap collects the regular expressions of ERROR comments found
	 102  // in files and returns them as a map of error positions to error messages.
	 103  //
	 104  // srcs must be a slice of the same length as files, containing the original
	 105  // source for the parsed AST.
	 106  func errMap(t *testing.T, files []*ast.File, srcs [][]byte) map[string][]string {
	 107  	// map of position strings to lists of error message patterns
	 108  	errmap := make(map[string][]string)
	 109  
	 110  	for i, file := range files {
	 111  		tok := fset.File(file.Package)
	 112  		src := srcs[i]
	 113  		var s scanner.Scanner
	 114  		s.Init(tok, src, nil, scanner.ScanComments)
	 115  		var prev token.Pos // position of last non-comment, non-semicolon token
	 116  		var here token.Pos // position immediately after the token at position prev
	 117  
	 118  	scanFile:
	 119  		for {
	 120  			pos, tok, lit := s.Scan()
	 121  			switch tok {
	 122  			case token.EOF:
	 123  				break scanFile
	 124  			case token.COMMENT:
	 125  				if lit[1] == '*' {
	 126  					lit = lit[:len(lit)-2] // strip trailing */
	 127  				}
	 128  				if s := errRx.FindStringSubmatch(lit[2:]); len(s) == 3 {
	 129  					pos := prev
	 130  					if s[1] == "HERE" {
	 131  						pos = here
	 132  					}
	 133  					p := fset.Position(pos).String()
	 134  					errmap[p] = append(errmap[p], strings.TrimSpace(s[2]))
	 135  				}
	 136  			case token.SEMICOLON:
	 137  				// ignore automatically inserted semicolon
	 138  				if lit == "\n" {
	 139  					continue scanFile
	 140  				}
	 141  				fallthrough
	 142  			default:
	 143  				prev = pos
	 144  				var l int // token length
	 145  				if tok.IsLiteral() {
	 146  					l = len(lit)
	 147  				} else {
	 148  					l = len(tok.String())
	 149  				}
	 150  				here = prev + token.Pos(l)
	 151  			}
	 152  		}
	 153  	}
	 154  
	 155  	return errmap
	 156  }
	 157  
	 158  func eliminate(t *testing.T, errmap map[string][]string, errlist []error) {
	 159  	for _, err := range errlist {
	 160  		pos, gotMsg := splitError(err)
	 161  		list := errmap[pos]
	 162  		index := -1 // list index of matching message, if any
	 163  		// we expect one of the messages in list to match the error at pos
	 164  		for i, wantRx := range list {
	 165  			rx, err := regexp.Compile(wantRx)
	 166  			if err != nil {
	 167  				t.Errorf("%s: %v", pos, err)
	 168  				continue
	 169  			}
	 170  			if rx.MatchString(gotMsg) {
	 171  				index = i
	 172  				break
	 173  			}
	 174  		}
	 175  		if index >= 0 {
	 176  			// eliminate from list
	 177  			if n := len(list) - 1; n > 0 {
	 178  				// not the last entry - swap in last element and shorten list by 1
	 179  				list[index] = list[n]
	 180  				errmap[pos] = list[:n]
	 181  			} else {
	 182  				// last entry - remove list from map
	 183  				delete(errmap, pos)
	 184  			}
	 185  		} else {
	 186  			t.Errorf("%s: no error expected: %q", pos, gotMsg)
	 187  		}
	 188  	}
	 189  }
	 190  
	 191  // goVersionRx matches a Go version string using '_', e.g. "go1_12".
	 192  var goVersionRx = regexp.MustCompile(`^go[1-9][0-9]*_(0|[1-9][0-9]*)$`)
	 193  
	 194  // asGoVersion returns a regular Go language version string
	 195  // if s is a Go version string using '_' rather than '.' to
	 196  // separate the major and minor version numbers (e.g. "go1_12").
	 197  // Otherwise it returns the empty string.
	 198  func asGoVersion(s string) string {
	 199  	if goVersionRx.MatchString(s) {
	 200  		return strings.Replace(s, "_", ".", 1)
	 201  	}
	 202  	return ""
	 203  }
	 204  
	 205  func checkFiles(t *testing.T, sizes Sizes, goVersion string, filenames []string, srcs [][]byte, manual bool, imp Importer) {
	 206  	if len(filenames) == 0 {
	 207  		t.Fatal("no source files")
	 208  	}
	 209  
	 210  	if strings.HasSuffix(filenames[0], ".go2") && !typeparams.Enabled {
	 211  		t.Skip("type params are not enabled")
	 212  	}
	 213  	if strings.HasSuffix(filenames[0], ".go1") && typeparams.Enabled {
	 214  		t.Skip("type params are enabled")
	 215  	}
	 216  
	 217  	mode := parser.AllErrors
	 218  	if !strings.HasSuffix(filenames[0], ".go2") {
	 219  		mode |= typeparams.DisallowParsing
	 220  	}
	 221  
	 222  	// parse files and collect parser errors
	 223  	files, errlist := parseFiles(t, filenames, srcs, mode)
	 224  
	 225  	pkgName := "<no package>"
	 226  	if len(files) > 0 {
	 227  		pkgName = files[0].Name.Name
	 228  	}
	 229  
	 230  	// if no Go version is given, consider the package name
	 231  	if goVersion == "" {
	 232  		goVersion = asGoVersion(pkgName)
	 233  	}
	 234  
	 235  	listErrors := manual && !*verifyErrors
	 236  	if listErrors && len(errlist) > 0 {
	 237  		t.Errorf("--- %s:", pkgName)
	 238  		for _, err := range errlist {
	 239  			t.Error(err)
	 240  		}
	 241  	}
	 242  
	 243  	// typecheck and collect typechecker errors
	 244  	var conf Config
	 245  	conf.Sizes = sizes
	 246  	SetGoVersion(&conf, goVersion)
	 247  
	 248  	// special case for importC.src
	 249  	if len(filenames) == 1 {
	 250  		if strings.HasSuffix(filenames[0], "importC.src") {
	 251  			conf.FakeImportC = true
	 252  		}
	 253  	}
	 254  
	 255  	conf.Importer = imp
	 256  	if imp == nil {
	 257  		conf.Importer = importer.Default()
	 258  	}
	 259  	conf.Error = func(err error) {
	 260  		if *haltOnError {
	 261  			defer panic(err)
	 262  		}
	 263  		if listErrors {
	 264  			t.Error(err)
	 265  			return
	 266  		}
	 267  		// Ignore secondary error messages starting with "\t";
	 268  		// they are clarifying messages for a primary error.
	 269  		if !strings.Contains(err.Error(), ": \t") {
	 270  			errlist = append(errlist, err)
	 271  		}
	 272  	}
	 273  	conf.Check(pkgName, fset, files, nil)
	 274  
	 275  	if listErrors {
	 276  		return
	 277  	}
	 278  
	 279  	for _, err := range errlist {
	 280  		err, ok := err.(Error)
	 281  		if !ok {
	 282  			continue
	 283  		}
	 284  		code := readCode(err)
	 285  		if code == 0 {
	 286  			t.Errorf("missing error code: %v", err)
	 287  		}
	 288  	}
	 289  
	 290  	// match and eliminate errors;
	 291  	// we are expecting the following errors
	 292  	errmap := errMap(t, files, srcs)
	 293  	eliminate(t, errmap, errlist)
	 294  
	 295  	// there should be no expected errors left
	 296  	if len(errmap) > 0 {
	 297  		t.Errorf("--- %s: %d source positions with expected (but not reported) errors:", pkgName, len(errmap))
	 298  		for pos, list := range errmap {
	 299  			for _, rx := range list {
	 300  				t.Errorf("%s: %q", pos, rx)
	 301  			}
	 302  		}
	 303  	}
	 304  }
	 305  
	 306  // TestManual is for manual testing of input files, provided as a list
	 307  // of arguments after the test arguments (and a separating "--"). For
	 308  // instance, to check the files foo.go and bar.go, use:
	 309  //
	 310  // 	go test -run Manual -- foo.go bar.go
	 311  //
	 312  // Provide the -verify flag to verify errors against ERROR comments in
	 313  // the input files rather than having a list of errors reported.
	 314  // The accepted Go language version can be controlled with the -lang flag.
	 315  func TestManual(t *testing.T) {
	 316  	filenames := flag.Args()
	 317  	if len(filenames) == 0 {
	 318  		return
	 319  	}
	 320  	testenv.MustHaveGoBuild(t)
	 321  	DefPredeclaredTestFuncs()
	 322  	testPkg(t, filenames, *goVersion, true)
	 323  }
	 324  
	 325  func TestLongConstants(t *testing.T) {
	 326  	format := "package longconst\n\nconst _ = %s\nconst _ = %s // ERROR excessively long constant"
	 327  	src := fmt.Sprintf(format, strings.Repeat("1", 9999), strings.Repeat("1", 10001))
	 328  	checkFiles(t, nil, "", []string{"longconst.go"}, [][]byte{[]byte(src)}, false, nil)
	 329  }
	 330  
	 331  // TestIndexRepresentability tests that constant index operands must
	 332  // be representable as int even if they already have a type that can
	 333  // represent larger values.
	 334  func TestIndexRepresentability(t *testing.T) {
	 335  	const src = "package index\n\nvar s []byte\nvar _ = s[int64 /* ERROR \"int64\\(1\\) << 40 \\(.*\\) overflows int\" */ (1) << 40]"
	 336  	checkFiles(t, &StdSizes{4, 4}, "", []string{"index.go"}, [][]byte{[]byte(src)}, false, nil)
	 337  }
	 338  
	 339  func TestIssue46453(t *testing.T) {
	 340  	if typeparams.Enabled {
	 341  		t.Skip("type params are enabled")
	 342  	}
	 343  	const src = "package p\ntype _ comparable // ERROR \"undeclared name: comparable\""
	 344  	checkFiles(t, nil, "", []string{"issue46453.go"}, [][]byte{[]byte(src)}, false, nil)
	 345  }
	 346  
	 347  func TestIssue47243_TypedRHS(t *testing.T) {
	 348  	// The RHS of the shift expression below overflows uint on 32bit platforms,
	 349  	// but this is OK as it is explicitly typed.
	 350  	const src = "package issue47243\n\nvar a uint64; var _ = a << uint64(4294967296)" // uint64(1<<32)
	 351  	checkFiles(t, &StdSizes{4, 4}, "", []string{"p.go"}, [][]byte{[]byte(src)}, false, nil)
	 352  }
	 353  
	 354  func TestCheck(t *testing.T)		 { DefPredeclaredTestFuncs(); testDir(t, "check") }
	 355  func TestExamples(t *testing.T)	{ testDir(t, "examples") }
	 356  func TestFixedbugs(t *testing.T) { testDir(t, "fixedbugs") }
	 357  
	 358  func testDir(t *testing.T, dir string) {
	 359  	testenv.MustHaveGoBuild(t)
	 360  
	 361  	dir = filepath.Join("testdata", dir)
	 362  	fis, err := os.ReadDir(dir)
	 363  	if err != nil {
	 364  		t.Error(err)
	 365  		return
	 366  	}
	 367  
	 368  	for _, fi := range fis {
	 369  		path := filepath.Join(dir, fi.Name())
	 370  
	 371  		// if fi is a directory, its files make up a single package
	 372  		var filenames []string
	 373  		if fi.IsDir() {
	 374  			fis, err := os.ReadDir(path)
	 375  			if err != nil {
	 376  				t.Error(err)
	 377  				continue
	 378  			}
	 379  			for _, fi := range fis {
	 380  				filenames = append(filenames, filepath.Join(path, fi.Name()))
	 381  			}
	 382  		} else {
	 383  			filenames = []string{path}
	 384  		}
	 385  		t.Run(filepath.Base(path), func(t *testing.T) {
	 386  			testPkg(t, filenames, "", false)
	 387  		})
	 388  	}
	 389  }
	 390  
	 391  // TODO(rFindley) reconcile the different test setup in go/types with types2.
	 392  func testPkg(t *testing.T, filenames []string, goVersion string, manual bool) {
	 393  	srcs := make([][]byte, len(filenames))
	 394  	for i, filename := range filenames {
	 395  		src, err := os.ReadFile(filename)
	 396  		if err != nil {
	 397  			t.Fatalf("could not read %s: %v", filename, err)
	 398  		}
	 399  		srcs[i] = src
	 400  	}
	 401  	checkFiles(t, nil, goVersion, filenames, srcs, manual, nil)
	 402  }
	 403  

View as plain text