Black Lives Matter. Support the Equal Justice Initiative.

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