1// Copyright 2013 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
5package main
6
7import (
8	"bytes"
9	"errors"
10	"fmt"
11	"internal/testenv"
12	"log"
13	"os"
14	"os/exec"
15	"path"
16	"path/filepath"
17	"regexp"
18	"strconv"
19	"strings"
20	"sync"
21	"testing"
22)
23
24// TestMain executes the test binary as the vet command if
25// GO_VETTEST_IS_VET is set, and runs the tests otherwise.
26func TestMain(m *testing.M) {
27	if os.Getenv("GO_VETTEST_IS_VET") != "" {
28		main()
29		os.Exit(0)
30	}
31
32	os.Setenv("GO_VETTEST_IS_VET", "1") // Set for subprocesses to inherit.
33	os.Exit(m.Run())
34}
35
36// vetPath returns the path to the "vet" binary to run.
37func vetPath(t testing.TB) string {
38	t.Helper()
39	testenv.MustHaveExec(t)
40
41	vetPathOnce.Do(func() {
42		vetExePath, vetPathErr = os.Executable()
43	})
44	if vetPathErr != nil {
45		t.Fatal(vetPathErr)
46	}
47	return vetExePath
48}
49
50var (
51	vetPathOnce sync.Once
52	vetExePath  string
53	vetPathErr  error
54)
55
56func vetCmd(t *testing.T, arg, pkg string) *exec.Cmd {
57	cmd := testenv.Command(t, testenv.GoToolPath(t), "vet", "-vettool="+vetPath(t), arg, path.Join("cmd/vet/testdata", pkg))
58	cmd.Env = os.Environ()
59	return cmd
60}
61
62func TestVet(t *testing.T) {
63	t.Parallel()
64	for _, pkg := range []string{
65		"appends",
66		"asm",
67		"assign",
68		"atomic",
69		"bool",
70		"buildtag",
71		"cgo",
72		"composite",
73		"copylock",
74		"deadcode",
75		"directive",
76		"httpresponse",
77		"lostcancel",
78		"method",
79		"nilfunc",
80		"print",
81		"shift",
82		"slog",
83		"structtag",
84		"testingpkg",
85		// "testtag" has its own test
86		"unmarshal",
87		"unsafeptr",
88		"unused",
89	} {
90		pkg := pkg
91		t.Run(pkg, func(t *testing.T) {
92			t.Parallel()
93
94			// Skip cgo test on platforms without cgo.
95			if pkg == "cgo" && !cgoEnabled(t) {
96				return
97			}
98
99			cmd := vetCmd(t, "-printfuncs=Warn,Warnf", pkg)
100
101			// The asm test assumes amd64.
102			if pkg == "asm" {
103				cmd.Env = append(cmd.Env, "GOOS=linux", "GOARCH=amd64")
104			}
105
106			dir := filepath.Join("testdata", pkg)
107			gos, err := filepath.Glob(filepath.Join(dir, "*.go"))
108			if err != nil {
109				t.Fatal(err)
110			}
111			asms, err := filepath.Glob(filepath.Join(dir, "*.s"))
112			if err != nil {
113				t.Fatal(err)
114			}
115			var files []string
116			files = append(files, gos...)
117			files = append(files, asms...)
118
119			errchk(cmd, files, t)
120		})
121	}
122
123	// The loopclosure analyzer (aka "rangeloop" before CL 140578)
124	// is a no-op for files whose version >= go1.22, so we use a
125	// go.mod file in the rangeloop directory to "downgrade".
126	//
127	// TOOD(adonovan): delete when go1.21 goes away.
128	t.Run("loopclosure", func(t *testing.T) {
129		cmd := testenv.Command(t, testenv.GoToolPath(t), "vet", "-vettool="+vetPath(t), ".")
130		cmd.Env = append(os.Environ(), "GOWORK=off")
131		cmd.Dir = "testdata/rangeloop"
132		cmd.Stderr = new(strings.Builder) // all vet output goes to stderr
133		cmd.Run()
134		stderr := cmd.Stderr.(fmt.Stringer).String()
135
136		filename := filepath.FromSlash("testdata/rangeloop/rangeloop.go")
137
138		// Unlike the tests above, which runs vet in cmd/vet/, this one
139		// runs it in subdirectory, so the "full names" in the output
140		// are in fact short "./rangeloop.go".
141		// But we can't just pass "./rangeloop.go" as the "full name"
142		// argument to errorCheck as it does double duty as both a
143		// string that appears in the output, and as file name
144		// openable relative to the test directory, containing text
145		// expectations.
146		//
147		// So, we munge the file.
148		stderr = strings.ReplaceAll(stderr, filepath.FromSlash("./rangeloop.go"), filename)
149
150		if err := errorCheck(stderr, false, filename, filepath.Base(filename)); err != nil {
151			t.Errorf("error check failed: %s", err)
152			t.Log("vet stderr:\n", cmd.Stderr)
153		}
154	})
155
156	// The stdversion analyzer requires a lower-than-tip go
157	// version in its go.mod file for it to report anything.
158	// So again we use a testdata go.mod file to "downgrade".
159	t.Run("stdversion", func(t *testing.T) {
160		cmd := testenv.Command(t, testenv.GoToolPath(t), "vet", "-vettool="+vetPath(t), ".")
161		cmd.Env = append(os.Environ(), "GOWORK=off")
162		cmd.Dir = "testdata/stdversion"
163		cmd.Stderr = new(strings.Builder) // all vet output goes to stderr
164		cmd.Run()
165		stderr := cmd.Stderr.(fmt.Stringer).String()
166
167		filename := filepath.FromSlash("testdata/stdversion/stdversion.go")
168
169		// Unlike the tests above, which runs vet in cmd/vet/, this one
170		// runs it in subdirectory, so the "full names" in the output
171		// are in fact short "./rangeloop.go".
172		// But we can't just pass "./rangeloop.go" as the "full name"
173		// argument to errorCheck as it does double duty as both a
174		// string that appears in the output, and as file name
175		// openable relative to the test directory, containing text
176		// expectations.
177		//
178		// So, we munge the file.
179		stderr = strings.ReplaceAll(stderr, filepath.FromSlash("./stdversion.go"), filename)
180
181		if err := errorCheck(stderr, false, filename, filepath.Base(filename)); err != nil {
182			t.Errorf("error check failed: %s", err)
183			t.Log("vet stderr:\n", cmd.Stderr)
184		}
185	})
186}
187
188func cgoEnabled(t *testing.T) bool {
189	// Don't trust build.Default.CgoEnabled as it is false for
190	// cross-builds unless CGO_ENABLED is explicitly specified.
191	// That's fine for the builders, but causes commands like
192	// 'GOARCH=386 go test .' to fail.
193	// Instead, we ask the go command.
194	cmd := testenv.Command(t, testenv.GoToolPath(t), "list", "-f", "{{context.CgoEnabled}}")
195	out, _ := cmd.CombinedOutput()
196	return string(out) == "true\n"
197}
198
199func errchk(c *exec.Cmd, files []string, t *testing.T) {
200	output, err := c.CombinedOutput()
201	if _, ok := err.(*exec.ExitError); !ok {
202		t.Logf("vet output:\n%s", output)
203		t.Fatal(err)
204	}
205	fullshort := make([]string, 0, len(files)*2)
206	for _, f := range files {
207		fullshort = append(fullshort, f, filepath.Base(f))
208	}
209	err = errorCheck(string(output), false, fullshort...)
210	if err != nil {
211		t.Errorf("error check failed: %s", err)
212	}
213}
214
215// TestTags verifies that the -tags argument controls which files to check.
216func TestTags(t *testing.T) {
217	t.Parallel()
218	for tag, wantFile := range map[string]int{
219		"testtag":     1, // file1
220		"x testtag y": 1,
221		"othertag":    2,
222	} {
223		tag, wantFile := tag, wantFile
224		t.Run(tag, func(t *testing.T) {
225			t.Parallel()
226			t.Logf("-tags=%s", tag)
227			cmd := vetCmd(t, "-tags="+tag, "tagtest")
228			output, err := cmd.CombinedOutput()
229
230			want := fmt.Sprintf("file%d.go", wantFile)
231			dontwant := fmt.Sprintf("file%d.go", 3-wantFile)
232
233			// file1 has testtag and file2 has !testtag.
234			if !bytes.Contains(output, []byte(filepath.Join("tagtest", want))) {
235				t.Errorf("%s: %s was excluded, should be included", tag, want)
236			}
237			if bytes.Contains(output, []byte(filepath.Join("tagtest", dontwant))) {
238				t.Errorf("%s: %s was included, should be excluded", tag, dontwant)
239			}
240			if t.Failed() {
241				t.Logf("err=%s, output=<<%s>>", err, output)
242			}
243		})
244	}
245}
246
247// All declarations below were adapted from test/run.go.
248
249// errorCheck matches errors in outStr against comments in source files.
250// For each line of the source files which should generate an error,
251// there should be a comment of the form // ERROR "regexp".
252// If outStr has an error for a line which has no such comment,
253// this function will report an error.
254// Likewise if outStr does not have an error for a line which has a comment,
255// or if the error message does not match the <regexp>.
256// The <regexp> syntax is Perl but it's best to stick to egrep.
257//
258// Sources files are supplied as fullshort slice.
259// It consists of pairs: full path to source file and its base name.
260func errorCheck(outStr string, wantAuto bool, fullshort ...string) (err error) {
261	var errs []error
262	out := splitOutput(outStr, wantAuto)
263	// Cut directory name.
264	for i := range out {
265		for j := 0; j < len(fullshort); j += 2 {
266			full, short := fullshort[j], fullshort[j+1]
267			out[i] = strings.ReplaceAll(out[i], full, short)
268		}
269	}
270
271	var want []wantedError
272	for j := 0; j < len(fullshort); j += 2 {
273		full, short := fullshort[j], fullshort[j+1]
274		want = append(want, wantedErrors(full, short)...)
275	}
276	for _, we := range want {
277		var errmsgs []string
278		if we.auto {
279			errmsgs, out = partitionStrings("<autogenerated>", out)
280		} else {
281			errmsgs, out = partitionStrings(we.prefix, out)
282		}
283		if len(errmsgs) == 0 {
284			errs = append(errs, fmt.Errorf("%s:%d: missing error %q", we.file, we.lineNum, we.reStr))
285			continue
286		}
287		matched := false
288		n := len(out)
289		for _, errmsg := range errmsgs {
290			// Assume errmsg says "file:line: foo".
291			// Cut leading "file:line: " to avoid accidental matching of file name instead of message.
292			text := errmsg
293			if _, suffix, ok := strings.Cut(text, " "); ok {
294				text = suffix
295			}
296			if we.re.MatchString(text) {
297				matched = true
298			} else {
299				out = append(out, errmsg)
300			}
301		}
302		if !matched {
303			errs = append(errs, fmt.Errorf("%s:%d: no match for %#q in:\n\t%s", we.file, we.lineNum, we.reStr, strings.Join(out[n:], "\n\t")))
304			continue
305		}
306	}
307
308	if len(out) > 0 {
309		errs = append(errs, fmt.Errorf("Unmatched Errors:"))
310		for _, errLine := range out {
311			errs = append(errs, fmt.Errorf("%s", errLine))
312		}
313	}
314
315	if len(errs) == 0 {
316		return nil
317	}
318	if len(errs) == 1 {
319		return errs[0]
320	}
321	var buf strings.Builder
322	fmt.Fprintf(&buf, "\n")
323	for _, err := range errs {
324		fmt.Fprintf(&buf, "%s\n", err.Error())
325	}
326	return errors.New(buf.String())
327}
328
329func splitOutput(out string, wantAuto bool) []string {
330	// gc error messages continue onto additional lines with leading tabs.
331	// Split the output at the beginning of each line that doesn't begin with a tab.
332	// <autogenerated> lines are impossible to match so those are filtered out.
333	var res []string
334	for _, line := range strings.Split(out, "\n") {
335		line = strings.TrimSuffix(line, "\r") // normalize Windows output
336		if strings.HasPrefix(line, "\t") {
337			res[len(res)-1] += "\n" + line
338		} else if strings.HasPrefix(line, "go tool") || strings.HasPrefix(line, "#") || !wantAuto && strings.HasPrefix(line, "<autogenerated>") {
339			continue
340		} else if strings.TrimSpace(line) != "" {
341			res = append(res, line)
342		}
343	}
344	return res
345}
346
347// matchPrefix reports whether s starts with file name prefix followed by a :,
348// and possibly preceded by a directory name.
349func matchPrefix(s, prefix string) bool {
350	i := strings.Index(s, ":")
351	if i < 0 {
352		return false
353	}
354	j := strings.LastIndex(s[:i], "/")
355	s = s[j+1:]
356	if len(s) <= len(prefix) || s[:len(prefix)] != prefix {
357		return false
358	}
359	if s[len(prefix)] == ':' {
360		return true
361	}
362	return false
363}
364
365func partitionStrings(prefix string, strs []string) (matched, unmatched []string) {
366	for _, s := range strs {
367		if matchPrefix(s, prefix) {
368			matched = append(matched, s)
369		} else {
370			unmatched = append(unmatched, s)
371		}
372	}
373	return
374}
375
376type wantedError struct {
377	reStr   string
378	re      *regexp.Regexp
379	lineNum int
380	auto    bool // match <autogenerated> line
381	file    string
382	prefix  string
383}
384
385var (
386	errRx       = regexp.MustCompile(`// (?:GC_)?ERROR(NEXT)? (.*)`)
387	errAutoRx   = regexp.MustCompile(`// (?:GC_)?ERRORAUTO(NEXT)? (.*)`)
388	errQuotesRx = regexp.MustCompile(`"([^"]*)"`)
389	lineRx      = regexp.MustCompile(`LINE(([+-])(\d+))?`)
390)
391
392// wantedErrors parses expected errors from comments in a file.
393func wantedErrors(file, short string) (errs []wantedError) {
394	cache := make(map[string]*regexp.Regexp)
395
396	src, err := os.ReadFile(file)
397	if err != nil {
398		log.Fatal(err)
399	}
400	for i, line := range strings.Split(string(src), "\n") {
401		lineNum := i + 1
402		if strings.Contains(line, "////") {
403			// double comment disables ERROR
404			continue
405		}
406		var auto bool
407		m := errAutoRx.FindStringSubmatch(line)
408		if m != nil {
409			auto = true
410		} else {
411			m = errRx.FindStringSubmatch(line)
412		}
413		if m == nil {
414			continue
415		}
416		if m[1] == "NEXT" {
417			lineNum++
418		}
419		all := m[2]
420		mm := errQuotesRx.FindAllStringSubmatch(all, -1)
421		if mm == nil {
422			log.Fatalf("%s:%d: invalid errchk line: %s", file, lineNum, line)
423		}
424		for _, m := range mm {
425			replacedOnce := false
426			rx := lineRx.ReplaceAllStringFunc(m[1], func(m string) string {
427				if replacedOnce {
428					return m
429				}
430				replacedOnce = true
431				n := lineNum
432				if strings.HasPrefix(m, "LINE+") {
433					delta, _ := strconv.Atoi(m[5:])
434					n += delta
435				} else if strings.HasPrefix(m, "LINE-") {
436					delta, _ := strconv.Atoi(m[5:])
437					n -= delta
438				}
439				return fmt.Sprintf("%s:%d", short, n)
440			})
441			re := cache[rx]
442			if re == nil {
443				var err error
444				re, err = regexp.Compile(rx)
445				if err != nil {
446					log.Fatalf("%s:%d: invalid regexp \"%#q\" in ERROR line: %v", file, lineNum, rx, err)
447				}
448				cache[rx] = re
449			}
450			prefix := fmt.Sprintf("%s:%d", short, lineNum)
451			errs = append(errs, wantedError{
452				reStr:   rx,
453				re:      re,
454				prefix:  prefix,
455				auto:    auto,
456				lineNum: lineNum,
457				file:    short,
458			})
459		}
460	}
461
462	return
463}
464