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