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 26package types_test 27 28import ( 29 "flag" 30 "go/ast" 31 "go/parser" 32 "go/scanner" 33 "go/token" 34 "io/ioutil" 35 "regexp" 36 "strings" 37 "testing" 38 39 _ "llvm.org/llgo/third_party/gotools/go/gcimporter" 40 . "llvm.org/llgo/third_party/gotools/go/types" 41) 42 43var ( 44 listErrors = flag.Bool("list", false, "list errors") 45 testFiles = flag.String("files", "", "space-separated list of test files") 46) 47 48// The test filenames do not end in .go so that they are invisible 49// to gofmt since they contain comments that must not change their 50// positions relative to surrounding tokens. 51 52// Each tests entry is list of files belonging to the same package. 53var tests = [][]string{ 54 {"testdata/errors.src"}, 55 {"testdata/importdecl0a.src", "testdata/importdecl0b.src"}, 56 {"testdata/importdecl1a.src", "testdata/importdecl1b.src"}, 57 {"testdata/cycles.src"}, 58 {"testdata/cycles1.src"}, 59 {"testdata/cycles2.src"}, 60 {"testdata/cycles3.src"}, 61 {"testdata/cycles4.src"}, 62 {"testdata/init0.src"}, 63 {"testdata/init1.src"}, 64 {"testdata/init2.src"}, 65 {"testdata/decls0.src"}, 66 {"testdata/decls1.src"}, 67 {"testdata/decls2a.src", "testdata/decls2b.src"}, 68 {"testdata/decls3.src"}, 69 {"testdata/const0.src"}, 70 {"testdata/const1.src"}, 71 {"testdata/constdecl.src"}, 72 {"testdata/vardecl.src"}, 73 {"testdata/expr0.src"}, 74 {"testdata/expr1.src"}, 75 {"testdata/expr2.src"}, 76 {"testdata/expr3.src"}, 77 {"testdata/methodsets.src"}, 78 {"testdata/shifts.src"}, 79 {"testdata/builtins.src"}, 80 {"testdata/conversions.src"}, 81 {"testdata/stmt0.src"}, 82 {"testdata/stmt1.src"}, 83 {"testdata/gotos.src"}, 84 {"testdata/labels.src"}, 85 {"testdata/issues.src"}, 86 {"testdata/blank.src"}, 87} 88 89var fset = token.NewFileSet() 90 91// Positioned errors are of the form filename:line:column: message . 92var posMsgRx = regexp.MustCompile(`^(.*:[0-9]+:[0-9]+): *(.*)`) 93 94// splitError splits an error's error message into a position string 95// and the actual error message. If there's no position information, 96// pos is the empty string, and msg is the entire error message. 97// 98func splitError(err error) (pos, msg string) { 99 msg = err.Error() 100 if m := posMsgRx.FindStringSubmatch(msg); len(m) == 3 { 101 pos = m[1] 102 msg = m[2] 103 } 104 return 105} 106 107func parseFiles(t *testing.T, filenames []string) ([]*ast.File, []error) { 108 var files []*ast.File 109 var errlist []error 110 for _, filename := range filenames { 111 file, err := parser.ParseFile(fset, filename, nil, parser.AllErrors) 112 if file == nil { 113 t.Fatalf("%s: %s", filename, err) 114 } 115 files = append(files, file) 116 if err != nil { 117 if list, _ := err.(scanner.ErrorList); len(list) > 0 { 118 for _, err := range list { 119 errlist = append(errlist, err) 120 } 121 } else { 122 errlist = append(errlist, err) 123 } 124 } 125 } 126 return files, errlist 127} 128 129// ERROR comments must start with text `ERROR "rx"` or `ERROR rx` where 130// rx is a regular expression that matches the expected error message. 131// Space around "rx" or rx is ignored. Use the form `ERROR HERE "rx"` 132// for error messages that are located immediately after rather than 133// at a token's position. 134// 135var errRx = regexp.MustCompile(`^ *ERROR *(HERE)? *"?([^"]*)"?`) 136 137// errMap collects the regular expressions of ERROR comments found 138// in files and returns them as a map of error positions to error messages. 139// 140func errMap(t *testing.T, testname string, files []*ast.File) map[string][]string { 141 // map of position strings to lists of error message patterns 142 errmap := make(map[string][]string) 143 144 for _, file := range files { 145 filename := fset.Position(file.Package).Filename 146 src, err := ioutil.ReadFile(filename) 147 if err != nil { 148 t.Fatalf("%s: could not read %s", testname, filename) 149 } 150 151 var s scanner.Scanner 152 s.Init(fset.AddFile(filename, -1, len(src)), src, nil, scanner.ScanComments) 153 var prev token.Pos // position of last non-comment, non-semicolon token 154 var here token.Pos // position immediately after the token at position prev 155 156 scanFile: 157 for { 158 pos, tok, lit := s.Scan() 159 switch tok { 160 case token.EOF: 161 break scanFile 162 case token.COMMENT: 163 if lit[1] == '*' { 164 lit = lit[:len(lit)-2] // strip trailing */ 165 } 166 if s := errRx.FindStringSubmatch(lit[2:]); len(s) == 3 { 167 pos := prev 168 if s[1] == "HERE" { 169 pos = here 170 } 171 p := fset.Position(pos).String() 172 errmap[p] = append(errmap[p], strings.TrimSpace(s[2])) 173 } 174 case token.SEMICOLON: 175 // ignore automatically inserted semicolon 176 if lit == "\n" { 177 continue scanFile 178 } 179 fallthrough 180 default: 181 prev = pos 182 var l int // token length 183 if tok.IsLiteral() { 184 l = len(lit) 185 } else { 186 l = len(tok.String()) 187 } 188 here = prev + token.Pos(l) 189 } 190 } 191 } 192 193 return errmap 194} 195 196func eliminate(t *testing.T, errmap map[string][]string, errlist []error) { 197 for _, err := range errlist { 198 pos, gotMsg := splitError(err) 199 list := errmap[pos] 200 index := -1 // list index of matching message, if any 201 // we expect one of the messages in list to match the error at pos 202 for i, wantRx := range list { 203 rx, err := regexp.Compile(wantRx) 204 if err != nil { 205 t.Errorf("%s: %v", pos, err) 206 continue 207 } 208 if rx.MatchString(gotMsg) { 209 index = i 210 break 211 } 212 } 213 if index >= 0 { 214 // eliminate from list 215 if n := len(list) - 1; n > 0 { 216 // not the last entry - swap in last element and shorten list by 1 217 list[index] = list[n] 218 errmap[pos] = list[:n] 219 } else { 220 // last entry - remove list from map 221 delete(errmap, pos) 222 } 223 } else { 224 t.Errorf("%s: no error expected: %q", pos, gotMsg) 225 } 226 } 227} 228 229func checkFiles(t *testing.T, testfiles []string) { 230 // parse files and collect parser errors 231 files, errlist := parseFiles(t, testfiles) 232 233 pkgName := "<no package>" 234 if len(files) > 0 { 235 pkgName = files[0].Name.Name 236 } 237 238 if *listErrors && len(errlist) > 0 { 239 t.Errorf("--- %s:", pkgName) 240 for _, err := range errlist { 241 t.Error(err) 242 } 243 } 244 245 // typecheck and collect typechecker errors 246 var conf Config 247 conf.Error = func(err error) { 248 if *listErrors { 249 t.Error(err) 250 return 251 } 252 // Ignore secondary error messages starting with "\t"; 253 // they are clarifying messages for a primary error. 254 if !strings.Contains(err.Error(), ": \t") { 255 errlist = append(errlist, err) 256 } 257 } 258 conf.Check(pkgName, fset, files, nil) 259 260 if *listErrors { 261 return 262 } 263 264 // match and eliminate errors; 265 // we are expecting the following errors 266 errmap := errMap(t, pkgName, files) 267 eliminate(t, errmap, errlist) 268 269 // there should be no expected errors left 270 if len(errmap) > 0 { 271 t.Errorf("--- %s: %d source positions with expected (but not reported) errors:", pkgName, len(errmap)) 272 for pos, list := range errmap { 273 for _, rx := range list { 274 t.Errorf("%s: %q", pos, rx) 275 } 276 } 277 } 278} 279 280func TestCheck(t *testing.T) { 281 // Declare builtins for testing. 282 DefPredeclaredTestFuncs() 283 284 // If explicit test files are specified, only check those. 285 if files := *testFiles; files != "" { 286 checkFiles(t, strings.Split(files, " ")) 287 return 288 } 289 290 // Otherwise, run all the tests. 291 for _, files := range tests { 292 checkFiles(t, files) 293 } 294} 295