Source file src/cmd/vet/vet_test.go

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

View as plain text