cmd/go: unify "go fix" and "go vet"

This change unifies the fix and vet subcommands; they use the
same run function, action graph, and external tool (-vettool
for go vet and -fixtool for go fix). go fix runs the tool
with the -fix flag, whereas although go vet also supports
-fix, it is not the default. The two tools have different
(overlapping) suites of analyzers.

The high-level parts are fully parameterized over the
vet/fix distinction; the lower-level parts (the action
graph) continue to use only the "vet" terminology.
The cmd/{vet,fix} executable is referred to as the "tool".

The tool is generally invoked in -json mode, regardless
of whether -json was requested, so that the tool produces
a cacheable JSON blob on stdout. When the go user did not
request -json, this blob is parsed and printed to stderr
by logic in the go vet command. (Formerly the tool would
print diagnostics to stderr, but this interacts poorly
with the build cache.)

go fix's legacy -fix=fixer,... flag is now a no-op that
prints a warning that the flag is obsolete.

The unitchecker's -c=n flag (to display n lines of context
around each diagnostic) is reimplemented in go vet based
on the JSON information, to avoid reliance on the stderr
output of the tool.

cmd/fix is added to dist's prebuilt set of tools since
go fix cannot build it dynamically (though ideally
it would).

Updates #71859
For #75432

Change-Id: I0a84746720b59d05d662ed57826747c5598dca44
Reviewed-on: https://go-review.googlesource.com/c/go/+/700795
Reviewed-by: Michael Matloob <matloob@google.com>
Auto-Submit: Alan Donovan <adonovan@google.com>
Reviewed-by: Michael Matloob <matloob@golang.org>
TryBot-Bypass: Alan Donovan <adonovan@google.com>
This commit is contained in:
Alan Donovan 2025-09-26 13:33:09 -04:00
parent 955a5a0dc5
commit 6f4c63ba63
20 changed files with 817 additions and 330 deletions

View file

@ -7,5 +7,15 @@
a replacement for `go tool doc`: it takes the same flags and arguments and
has the same behavior.
<!-- go.dev/issue/75432 -->
The `go fix` command, following the pattern of `go vet` in Go 1.10,
now uses the Go analysis framework (`golang.org/x/tools/go/analysis`).
This means the same analyzers that provide diagnostics in `go vet`
can be used to suggest and apply fixes in `go fix`.
The `go fix` command's historical fixers, all of which were obsolete,
have been removed and replaced by a suite of new analyzers that
offer fixes to use newer features of the language and library.
<!-- I'll write a blog post that discusses this at length. --adonovan -->
### Cgo {#cgo}

View file

@ -1397,7 +1397,7 @@ var (
binExesIncludedInDistpack = []string{"cmd/go", "cmd/gofmt"}
// Keep in sync with the filter in cmd/distpack/pack.go.
toolsIncludedInDistpack = []string{"cmd/asm", "cmd/cgo", "cmd/compile", "cmd/cover", "cmd/link", "cmd/preprofile", "cmd/vet"}
toolsIncludedInDistpack = []string{"cmd/asm", "cmd/cgo", "cmd/compile", "cmd/cover", "cmd/fix", "cmd/link", "cmd/preprofile", "cmd/vet"}
// We could install all tools in "cmd", but is unnecessary because we will
// remove them in distpack, so instead install the tools that will actually

View file

@ -1,20 +0,0 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/*
Fix finds Go programs that use old APIs and rewrites them to use
newer ones. After you update to a new Go release, fix helps make
the necessary changes to your programs.
Usage:
go tool fix [ignored...]
This tool is currently in transition. All its historical fixers were
long obsolete and have been removed, so it is currently a no-op. In
due course the tool will integrate with the Go analysis framework
(golang.org/x/tools/go/analysis) and run a modern suite of fix
algorithms; see https://go.dev/issue/71859.
*/
package main

View file

@ -1,31 +1,59 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/*
Fix is a tool executed by "go fix" to update Go programs that use old
features of the language and library and rewrite them to use newer
ones. After you update to a new Go release, fix helps make the
necessary changes to your programs.
See the documentation for "go fix" for how to run this command.
You can provide an alternative tool using "go fix -fixtool=..."
Run "go tool fix help" to see the list of analyzers supported by this
program.
See [golang.org/x/tools/go/analysis] for information on how to write
an analyzer that can suggest fixes.
*/
package main
import (
"flag"
"fmt"
"os"
)
"cmd/internal/objabi"
"cmd/internal/telemetry/counter"
var (
_ = flag.Bool("diff", false, "obsolete, no effect")
_ = flag.String("go", "", "obsolete, no effect")
_ = flag.String("r", "", "obsolete, no effect")
_ = flag.String("force", "", "obsolete, no effect")
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/buildtag"
"golang.org/x/tools/go/analysis/passes/hostport"
"golang.org/x/tools/go/analysis/unitchecker"
)
func usage() {
fmt.Fprintf(os.Stderr, "usage: go tool fix [-diff] [-r ignored] [-force ignored] ...\n")
flag.PrintDefaults()
os.Exit(2)
}
func main() {
flag.Usage = usage
flag.Parse()
// Keep consistent with cmd/vet/main.go!
counter.Open()
objabi.AddVersionFlag()
counter.Inc("fix/invocations")
os.Exit(0)
unitchecker.Main(suite...) // (never returns)
}
// The fix suite analyzers produce fixes that are safe to apply.
// (Diagnostics may not describe actual problems,
// but their fixes must be unambiguously safe to apply.)
var suite = []*analysis.Analyzer{
buildtag.Analyzer,
hostport.Analyzer,
// TODO(adonovan): now the modernize (proposal #75266) and
// inline (proposal #75267) analyzers are published, revendor
// x/tools and add them here.
//
// TODO(adonovan):add any other vet analyzers whose fixes are always safe.
// Candidates to audit: sigchanyzer, printf, assign, unreachable.
// Rejected:
// - composites: some types (e.g. PointXY{1,2}) don't want field names.
// - timeformat: flipping MM/DD is a behavior change, but the code
// could potentially be a workaround for another bug.
// - stringintconv: offers two fixes, user input required to choose.
// - fieldalignment: poor signal/noise; fix could be a regression.
}

View file

@ -18,7 +18,7 @@
// clean remove object files and cached files
// doc show documentation for package or symbol
// env print Go environment information
// fix update packages to use new APIs
// fix apply fixes suggested by static checkers
// fmt gofmt (reformat) package sources
// generate generate Go files by processing source
// get add dependencies to current module and install them
@ -495,22 +495,34 @@
//
// For more about environment variables, see 'go help environment'.
//
// # Update packages to use new APIs
// # Apply fixes suggested by static checkers
//
// Usage:
//
// go fix [-fix list] [packages]
// go fix [build flags] [-fixtool prog] [fix flags] [packages]
//
// Fix runs the Go fix command on the packages named by the import paths.
// Fix runs the Go fix tool (cmd/vet) on the named packages
// and applies suggested fixes.
//
// The -fix flag sets a comma-separated list of fixes to run.
// The default is all known fixes.
// (Its value is passed to 'go tool fix -r'.)
// It supports these flags:
//
// -diff
// instead of applying each fix, print the patch as a unified diff
//
// The -fixtool=prog flag selects a different analysis tool with
// alternative or additional fixes; see the documentation for go vet's
// -vettool flag for details.
//
// For more about fix, see 'go doc cmd/fix'.
// For more about specifying packages, see 'go help packages'.
//
// To run fix with other options, run 'go tool fix'.
// For a list of fixers and their flags, see 'go tool fix help'.
//
// For details of a specific fixer such as 'hostport',
// see 'go tool fix help hostport'.
//
// The build flags supported by go fix are those that control package resolution
// and execution, such as -C, -n, -x, -v, -tags, and -toolexec.
// For more about these flags, see 'go help build'.
//
// See also: go fmt, go vet.
//
@ -2014,20 +2026,34 @@
//
// go vet [build flags] [-vettool prog] [vet flags] [packages]
//
// Vet runs the Go vet command on the packages named by the import paths.
// Vet runs the Go vet tool (cmd/vet) on the named packages
// and reports diagnostics.
//
// For more about vet and its flags, see 'go doc cmd/vet'.
// For more about specifying packages, see 'go help packages'.
// For a list of checkers and their flags, see 'go tool vet help'.
// For details of a specific checker such as 'printf', see 'go tool vet help printf'.
// It supports these flags:
//
// The -vettool=prog flag selects a different analysis tool with alternative
// or additional checks.
// For example, the 'shadow' analyzer can be built and run using these commands:
// -c int
// display offending line with this many lines of context (default -1)
// -json
// emit JSON output
// -fix
// instead of printing each diagnostic, apply its first fix (if any)
// -diff
// instead of applying each fix, print the patch as a unified diff
//
// The -vettool=prog flag selects a different analysis tool with
// alternative or additional checks. For example, the 'shadow' analyzer
// can be built and run using these commands:
//
// go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest
// go vet -vettool=$(which shadow)
//
// Alternative vet tools should be built atop golang.org/x/tools/go/analysis/unitchecker,
// which handles the interaction with go vet.
//
// For more about specifying packages, see 'go help packages'.
// For a list of checkers and their flags, see 'go tool vet help'.
// For details of a specific checker such as 'printf', see 'go tool vet help printf'.
//
// The build flags supported by go vet are those that control package resolution
// and execution, such as -C, -n, -x, -v, -tags, and -toolexec.
// For more about these flags, see 'go help build'.

View file

@ -1,86 +0,0 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package fix implements the “go fix” command.
package fix
import (
"cmd/go/internal/base"
"cmd/go/internal/cfg"
"cmd/go/internal/load"
"cmd/go/internal/modload"
"cmd/go/internal/str"
"cmd/go/internal/work"
"context"
"fmt"
"go/build"
"os"
"path/filepath"
)
var CmdFix = &base.Command{
UsageLine: "go fix [-fix list] [packages]",
Short: "update packages to use new APIs",
Long: `
Fix runs the Go fix command on the packages named by the import paths.
The -fix flag sets a comma-separated list of fixes to run.
The default is all known fixes.
(Its value is passed to 'go tool fix -r'.)
For more about fix, see 'go doc cmd/fix'.
For more about specifying packages, see 'go help packages'.
To run fix with other options, run 'go tool fix'.
See also: go fmt, go vet.
`,
}
var fixes = CmdFix.Flag.String("fix", "", "comma-separated list of fixes to apply")
func init() {
work.AddBuildFlags(CmdFix, work.OmitBuildOnlyFlags)
CmdFix.Run = runFix // fix cycle
}
func runFix(ctx context.Context, cmd *base.Command, args []string) {
pkgs := load.PackagesAndErrors(ctx, load.PackageOpts{}, args)
w := 0
for _, pkg := range pkgs {
if pkg.Error != nil {
base.Errorf("%v", pkg.Error)
continue
}
pkgs[w] = pkg
w++
}
pkgs = pkgs[:w]
printed := false
for _, pkg := range pkgs {
if modload.Enabled() && pkg.Module != nil && !pkg.Module.Main {
if !printed {
fmt.Fprintf(os.Stderr, "go: not fixing packages in dependency modules\n")
printed = true
}
continue
}
// Use pkg.gofiles instead of pkg.Dir so that
// the command only applies to this package,
// not to packages in subdirectories.
files := base.RelPaths(pkg.InternalAllGoFiles())
goVersion := ""
if pkg.Module != nil {
goVersion = "go" + pkg.Module.GoVersion
} else if pkg.Standard {
goVersion = build.Default.ReleaseTags[len(build.Default.ReleaseTags)-1]
}
var fixArg []string
if *fixes != "" {
fixArg = []string{"-r=" + *fixes}
}
base.Run(str.StringList(cfg.BuildToolexec, filepath.Join(cfg.GOROOTbin, "go"), "tool", "fix", "-go="+goVersion, fixArg, files))
}
}

View file

@ -707,6 +707,7 @@ func runTest(ctx context.Context, cmd *base.Command, args []string) {
work.BuildInit()
work.VetFlags = testVet.flags
work.VetExplicit = testVet.explicit
work.VetTool = base.Tool("vet")
pkgOpts := load.PackageOpts{ModResolveTests: true}
pkgs = load.PackagesAndErrors(ctx, pkgOpts, pkgArgs)

View file

@ -2,13 +2,20 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package vet implements the “go vet” command.
// Package vet implements the “go vet” and “go fix” commands.
package vet
import (
"context"
"encoding/json"
"errors"
"fmt"
"path/filepath"
"io"
"os"
"slices"
"strconv"
"strings"
"sync"
"cmd/go/internal/base"
"cmd/go/internal/cfg"
@ -18,30 +25,39 @@ import (
"cmd/go/internal/work"
)
// Break init loop.
func init() {
CmdVet.Run = runVet
}
var CmdVet = &base.Command{
CustomFlags: true,
UsageLine: "go vet [build flags] [-vettool prog] [vet flags] [packages]",
Short: "report likely mistakes in packages",
Long: `
Vet runs the Go vet command on the packages named by the import paths.
Vet runs the Go vet tool (cmd/vet) on the named packages
and reports diagnostics.
For more about vet and its flags, see 'go doc cmd/vet'.
For more about specifying packages, see 'go help packages'.
For a list of checkers and their flags, see 'go tool vet help'.
For details of a specific checker such as 'printf', see 'go tool vet help printf'.
It supports these flags:
The -vettool=prog flag selects a different analysis tool with alternative
or additional checks.
For example, the 'shadow' analyzer can be built and run using these commands:
-c int
display offending line with this many lines of context (default -1)
-json
emit JSON output
-fix
instead of printing each diagnostic, apply its first fix (if any)
-diff
instead of applying each fix, print the patch as a unified diff
The -vettool=prog flag selects a different analysis tool with
alternative or additional checks. For example, the 'shadow' analyzer
can be built and run using these commands:
go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest
go vet -vettool=$(which shadow)
Alternative vet tools should be built atop golang.org/x/tools/go/analysis/unitchecker,
which handles the interaction with go vet.
For more about specifying packages, see 'go help packages'.
For a list of checkers and their flags, see 'go tool vet help'.
For details of a specific checker such as 'printf', see 'go tool vet help printf'.
The build flags supported by go vet are those that control package resolution
and execution, such as -C, -n, -x, -v, -tags, and -toolexec.
For more about these flags, see 'go help build'.
@ -50,9 +66,64 @@ See also: go fmt, go fix.
`,
}
func runVet(ctx context.Context, cmd *base.Command, args []string) {
vetFlags, pkgArgs := vetFlags(args)
modload.InitWorkfile() // The vet command does custom flag processing; initialize workspaces after that.
var CmdFix = &base.Command{
CustomFlags: true,
UsageLine: "go fix [build flags] [-fixtool prog] [fix flags] [packages]",
Short: "apply fixes suggested by static checkers",
Long: `
Fix runs the Go fix tool (cmd/vet) on the named packages
and applies suggested fixes.
It supports these flags:
-diff
instead of applying each fix, print the patch as a unified diff
The -fixtool=prog flag selects a different analysis tool with
alternative or additional fixes; see the documentation for go vet's
-vettool flag for details.
For more about specifying packages, see 'go help packages'.
For a list of fixers and their flags, see 'go tool fix help'.
For details of a specific fixer such as 'hostport',
see 'go tool fix help hostport'.
The build flags supported by go fix are those that control package resolution
and execution, such as -C, -n, -x, -v, -tags, and -toolexec.
For more about these flags, see 'go help build'.
See also: go fmt, go vet.
`,
}
func init() {
// avoid initialization cycle
CmdVet.Run = run
CmdFix.Run = run
addFlags(CmdVet)
addFlags(CmdFix)
}
var (
// "go vet -fix" causes fixes to be applied.
vetFixFlag = CmdVet.Flag.Bool("fix", false, "apply the first fix (if any) for each diagnostic")
// The "go fix -fix=name,..." flag is an obsolete flag formerly
// used to pass a list of names to the old "cmd/fix -r".
fixFixFlag = CmdFix.Flag.String("fix", "", "obsolete; no effect")
)
// run implements both "go vet" and "go fix".
func run(ctx context.Context, cmd *base.Command, args []string) {
// Compute flags for the vet/fix tool (e.g. cmd/{vet,fix}).
toolFlags, pkgArgs := toolFlags(cmd, args)
// The vet/fix commands do custom flag processing;
// initialize workspaces after that.
modload.InitWorkfile()
if cfg.DebugTrace != "" {
var close func() error
@ -72,23 +143,84 @@ func runVet(ctx context.Context, cmd *base.Command, args []string) {
defer span.Done()
work.BuildInit()
work.VetFlags = vetFlags
if len(vetFlags) > 0 {
work.VetExplicit = true
// Flag theory:
//
// All flags supported by unitchecker are accepted by go {vet,fix}.
// Some arise from each analyzer in the tool (both to enable it
// and to configure it), whereas others [-V -c -diff -fix -flags -json]
// are core to unitchecker itself.
//
// Most are passed through to toolFlags, but not all:
// * -V and -flags are used by the handshake in the [toolFlags] function;
// * these old flags have no effect: [-all -source -tags -v]; and
// * the [-c -fix -diff -json] flags are handled specially
// as described below:
//
// command args tool args
// go vet => cmd/vet -json Parse stdout, print diagnostics to stderr.
// go vet -json => cmd/vet -json Pass stdout through.
// go vet -fix [-diff] => cmd/vet -fix [-diff] Pass stdout through.
// go fix [-diff] => cmd/fix -fix [-diff] Pass stdout through.
// go fix -json => cmd/fix -json Pass stdout through.
//
// Notes:
// * -diff requires "go vet -fix" or "go fix", and no -json.
// * -json output is the same in "vet" and "fix" modes,
// and describes both diagnostics and fixes (but does not apply them).
// * -c=n is supported by the unitchecker, but we reimplement it
// here (see printDiagnostics), and do not pass the flag through.
work.VetExplicit = len(toolFlags) > 0
if cmd.Name() == "fix" || *vetFixFlag {
// fix mode: 'go fix' or 'go vet -fix'
if jsonFlag {
if diffFlag {
base.Fatalf("-json and -diff cannot be used together")
}
if vetTool != "" {
var err error
work.VetTool, err = filepath.Abs(vetTool)
if err != nil {
base.Fatalf("%v", err)
} else {
toolFlags = append(toolFlags, "-fix")
if diffFlag {
toolFlags = append(toolFlags, "-diff")
}
}
if contextFlag != -1 {
base.Fatalf("-c flag cannot be used when applying fixes")
}
} else {
// vet mode: 'go vet' without -fix
if !jsonFlag {
// Post-process the JSON diagnostics on stdout and format
// it as "file:line: message" diagnostics on stderr.
// (JSON reliably frames diagnostics, fixes, and errors so
// that we don't have to parse stderr or interpret non-zero
// exit codes, and interacts better with the action cache.)
toolFlags = append(toolFlags, "-json")
work.VetHandleStdout = printJSONDiagnostics
}
if diffFlag {
base.Fatalf("go vet -diff flag requires -fix")
}
}
// Implement legacy "go fix -fix=name,..." flag.
if *fixFixFlag != "" {
fmt.Fprintf(os.Stderr, "go %s: the -fix=%s flag is obsolete and has no effect", cmd.Name(), *fixFixFlag)
// The buildtag fixer is now implemented by cmd/fix.
if slices.Contains(strings.Split(*fixFixFlag, ","), "buildtag") {
fmt.Fprintf(os.Stderr, "go %s: to enable the buildtag check, use -buildtag", cmd.Name())
}
}
work.VetFlags = toolFlags
pkgOpts := load.PackageOpts{ModResolveTests: true}
pkgs := load.PackagesAndErrors(ctx, pkgOpts, pkgArgs)
load.CheckPackageErrors(pkgs)
if len(pkgs) == 0 {
base.Fatalf("no packages to vet")
base.Fatalf("no packages to %s", cmd.Name())
}
b := work.NewBuilder("")
@ -98,7 +230,23 @@ func runVet(ctx context.Context, cmd *base.Command, args []string) {
}
}()
root := &work.Action{Mode: "go vet"}
// To avoid file corruption from duplicate application of
// fixes (in fix mode), and duplicate reporting of diagnostics
// (in vet mode), we must run the tool only once for each
// source file. We achieve that by running on ptest (below)
// instead of p.
//
// As a side benefit, this also allows analyzers to make
// "closed world" assumptions and report diagnostics (such as
// "this symbol is unused") that might be false if computed
// from just the primary package p, falsified by the
// additional declarations in test files.
//
// We needn't worry about intermediate test variants, as they
// will only be executed in VetxOnly mode, for facts but not
// diagnostics.
root := &work.Action{Mode: "go " + cmd.Name()}
for _, p := range pkgs {
_, ptest, pxtest, perr := load.TestPackagesFor(ctx, pkgOpts, p, nil)
if perr != nil {
@ -106,10 +254,11 @@ func runVet(ctx context.Context, cmd *base.Command, args []string) {
continue
}
if len(ptest.GoFiles) == 0 && len(ptest.CgoFiles) == 0 && pxtest == nil {
base.Errorf("go: can't vet %s: no Go files in %s", p.ImportPath, p.Dir)
base.Errorf("go: can't %s %s: no Go files in %s", cmd.Name(), p.ImportPath, p.Dir)
continue
}
if len(ptest.GoFiles) > 0 || len(ptest.CgoFiles) > 0 {
// The test package includes all the files of primary package.
root.Deps = append(root.Deps, b.VetAction(work.ModeBuild, work.ModeBuild, ptest))
}
if pxtest != nil {
@ -118,3 +267,167 @@ func runVet(ctx context.Context, cmd *base.Command, args []string) {
}
b.Do(ctx, root)
}
// printJSONDiagnostics parses JSON (from the tool's stdout) and
// prints it (to stderr) in "file:line: message" form.
// It also ensures that we exit nonzero if there were diagnostics.
func printJSONDiagnostics(r io.Reader) error {
stdout, err := io.ReadAll(r)
if err != nil {
return err
}
if len(stdout) > 0 {
// unitchecker emits a JSON map of the form:
// output maps Package ID -> Analyzer.Name -> (error | []Diagnostic);
var tree jsonTree
if err := json.Unmarshal([]byte(stdout), &tree); err != nil {
return fmt.Errorf("parsing JSON: %v", err)
}
for _, units := range tree {
for analyzer, msg := range units {
if msg[0] == '[' {
// []Diagnostic
var diags []jsonDiagnostic
if err := json.Unmarshal([]byte(msg), &diags); err != nil {
return fmt.Errorf("parsing JSON diagnostics: %v", err)
}
for _, diag := range diags {
base.SetExitStatus(1)
printJSONDiagnostic(analyzer, diag)
}
} else {
// error
var e jsonError
if err := json.Unmarshal([]byte(msg), &e); err != nil {
return fmt.Errorf("parsing JSON error: %v", err)
}
base.SetExitStatus(1)
return errors.New(e.Err)
}
}
}
}
return nil
}
var stderrMu sync.Mutex // serializes concurrent writes to stdout
func printJSONDiagnostic(analyzer string, diag jsonDiagnostic) {
stderrMu.Lock()
defer stderrMu.Unlock()
type posn struct {
file string
line, col int
}
parsePosn := func(s string) (_ posn, _ bool) {
colon2 := strings.LastIndexByte(s, ':')
if colon2 < 0 {
return
}
colon1 := strings.LastIndexByte(s[:colon2], ':')
if colon1 < 0 {
return
}
line, err := strconv.Atoi(s[colon1+len(":") : colon2])
if err != nil {
return
}
col, err := strconv.Atoi(s[colon2+len(":"):])
if err != nil {
return
}
return posn{s[:colon1], line, col}, true
}
print := func(start, end, message string) {
if posn, ok := parsePosn(start); ok {
// The (*work.Shell).reportCmd method relativizes the
// prefix of each line of the subprocess's stdout;
// but filenames in JSON aren't at the start of the line,
// so we need to apply ShortPath here too.
fmt.Fprintf(os.Stderr, "%s:%d:%d: %v\n", base.ShortPath(posn.file), posn.line, posn.col, message)
} else {
fmt.Fprintf(os.Stderr, "%s: %v\n", start, message)
}
// -c=n: show offending line plus N lines of context.
// (Duplicates logic in unitchecker; see analysisflags.PrintPlain.)
if contextFlag >= 0 {
if end == "" {
end = start
}
var (
startPosn, ok1 = parsePosn(start)
endPosn, ok2 = parsePosn(end)
)
if ok1 && ok2 {
// TODO(adonovan): respect overlays (like unitchecker does).
data, _ := os.ReadFile(startPosn.file)
lines := strings.Split(string(data), "\n")
for i := startPosn.line - contextFlag; i <= endPosn.line+contextFlag; i++ {
if 1 <= i && i <= len(lines) {
fmt.Fprintf(os.Stderr, "%d\t%s\n", i, lines[i-1])
}
}
}
}
}
// TODO(adonovan): append " [analyzer]" to message. But we must first relax
// x/tools/go/analysis/internal/versiontest.TestVettool and revendor; sigh.
_ = analyzer
print(diag.Posn, diag.End, diag.Message)
for _, rel := range diag.Related {
print(rel.Posn, rel.End, "\t"+rel.Message)
}
}
// -- JSON schema --
// (populated by golang.org/x/tools/go/analysis/internal/analysisflags/flags.go)
// A jsonTree is a mapping from package ID to analysis name to result.
// Each result is either a jsonError or a list of jsonDiagnostic.
type jsonTree map[string]map[string]json.RawMessage
type jsonError struct {
Err string `json:"error"`
}
// A TextEdit describes the replacement of a portion of a file.
// Start and End are zero-based half-open indices into the original byte
// sequence of the file, and New is the new text.
type jsonTextEdit struct {
Filename string `json:"filename"`
Start int `json:"start"`
End int `json:"end"`
New string `json:"new"`
}
// A jsonSuggestedFix describes an edit that should be applied as a whole or not
// at all. It might contain multiple TextEdits/text_edits if the SuggestedFix
// consists of multiple non-contiguous edits.
type jsonSuggestedFix struct {
Message string `json:"message"`
Edits []jsonTextEdit `json:"edits"`
}
// A jsonDiagnostic describes the json schema of an analysis.Diagnostic.
type jsonDiagnostic struct {
Category string `json:"category,omitempty"`
Posn string `json:"posn"` // e.g. "file.go:line:column"
End string `json:"end"`
Message string `json:"message"`
SuggestedFixes []jsonSuggestedFix `json:"suggested_fixes,omitempty"`
Related []jsonRelatedInformation `json:"related,omitempty"`
}
// A jsonRelated describes a secondary position and message related to
// a primary diagnostic.
type jsonRelatedInformation struct {
Posn string `json:"posn"` // e.g. "file.go:line:column"
End string `json:"end"`
Message string `json:"message"`
}

View file

@ -21,70 +21,83 @@ import (
"cmd/go/internal/work"
)
// go vet flag processing
//
// We query the flags of the tool specified by -vettool and accept any
// of those flags plus any flag valid for 'go build'. The tool must
// support -flags, which prints a description of its flags in JSON to
// stdout.
// go vet/fix flag processing
var (
// We query the flags of the tool specified by -{vet,fix}tool
// and accept any of those flags plus any flag valid for 'go
// build'. The tool must support -flags, which prints a
// description of its flags in JSON to stdout.
// vetTool specifies the vet command to run.
// Any tool that supports the (still unpublished) vet
// command-line protocol may be supplied; see
// golang.org/x/tools/go/analysis/unitchecker for one
// implementation. It is also used by tests.
//
// The default behavior (vetTool=="") runs 'go tool vet'.
var vetTool string // -vettool
// toolFlag specifies the vet/fix command to run.
// Any toolFlag that supports the (unpublished) vet
// command-line protocol may be supplied; see
// golang.org/x/tools/go/analysis/unitchecker for the
// sole implementation. It is also used by tests.
//
// The default behavior ("") runs 'go tool {vet,fix}'.
//
// Do not access this flag directly; use [parseToolFlag].
toolFlag string // -{vet,fix}tool
diffFlag bool // -diff
jsonFlag bool // -json
contextFlag = -1 // -c=n
)
func init() {
// For now, we omit the -json flag for vet because we could plausibly
// support -json specific to the vet command in the future (perhaps using
// the same format as build -json).
work.AddBuildFlags(CmdVet, work.OmitJSONFlag)
CmdVet.Flag.StringVar(&vetTool, "vettool", "", "")
func addFlags(cmd *base.Command) {
// We run the compiler for export data.
// Suppress the build -json flag; we define our own.
work.AddBuildFlags(cmd, work.OmitJSONFlag)
cmd.Flag.StringVar(&toolFlag, cmd.Name()+"tool", "", "") // -vettool or -fixtool
cmd.Flag.BoolVar(&diffFlag, "diff", false, "print diff instead of applying it")
cmd.Flag.BoolVar(&jsonFlag, "json", false, "print diagnostics and fixes as JSON")
cmd.Flag.IntVar(&contextFlag, "c", -1, "display offending line with this many lines of context")
}
func parseVettoolFlag(args []string) {
// Extract -vettool by ad hoc flag processing:
// parseToolFlag scans args for -{vet,fix}tool and returns the effective tool filename.
func parseToolFlag(cmd *base.Command, args []string) string {
toolFlagName := cmd.Name() + "tool" // vettool or fixtool
// Extract -{vet,fix}tool by ad hoc flag processing:
// its value is needed even before we can declare
// the flags available during main flag processing.
for i, arg := range args {
if arg == "-vettool" || arg == "--vettool" {
if arg == "-"+toolFlagName || arg == "--"+toolFlagName {
if i+1 >= len(args) {
log.Fatalf("%s requires a filename", arg)
}
vetTool = args[i+1]
return
} else if strings.HasPrefix(arg, "-vettool=") ||
strings.HasPrefix(arg, "--vettool=") {
vetTool = arg[strings.IndexByte(arg, '=')+1:]
return
toolFlag = args[i+1]
break
} else if strings.HasPrefix(arg, "-"+toolFlagName+"=") ||
strings.HasPrefix(arg, "--"+toolFlagName+"=") {
toolFlag = arg[strings.IndexByte(arg, '=')+1:]
break
}
}
}
// vetFlags processes the command line, splitting it at the first non-flag
// into the list of flags and list of packages.
func vetFlags(args []string) (passToVet, packageNames []string) {
parseVettoolFlag(args)
// Query the vet command for its flags.
var tool string
if vetTool == "" {
tool = base.Tool("vet")
} else {
var err error
tool, err = filepath.Abs(vetTool)
if toolFlag != "" {
tool, err := filepath.Abs(toolFlag)
if err != nil {
log.Fatal(err)
}
return tool
}
return base.Tool(cmd.Name()) // default to 'go tool vet|fix'
}
// toolFlags processes the command line, splitting it at the first non-flag
// into the list of flags and list of packages.
func toolFlags(cmd *base.Command, args []string) (passToTool, packageNames []string) {
tool := parseToolFlag(cmd, args)
work.VetTool = tool
// Query the tool for its flags.
out := new(bytes.Buffer)
vetcmd := exec.Command(tool, "-flags")
vetcmd.Stdout = out
if err := vetcmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "go: can't execute %s -flags: %v\n", tool, err)
toolcmd := exec.Command(tool, "-flags")
toolcmd.Stdout = out
if err := toolcmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "go: %s -flags failed: %v\n", tool, err)
base.SetExitStatus(2)
base.Exit()
}
@ -99,15 +112,20 @@ func vetFlags(args []string) (passToVet, packageNames []string) {
base.Exit()
}
// Add vet's flags to CmdVet.Flag.
// Add tool's flags to cmd.Flag.
//
// Some flags, in particular -tags and -v, are known to vet but
// Some flags, in particular -tags and -v, are known to the tool but
// also defined as build flags. This works fine, so we omit duplicates here.
// However some, like -x, are known to the build but not to vet.
isVetFlag := make(map[string]bool, len(analysisFlags))
cf := CmdVet.Flag
// However some, like -x, are known to the build but not to the tool.
isToolFlag := make(map[string]bool, len(analysisFlags))
cf := cmd.Flag
for _, f := range analysisFlags {
isVetFlag[f.Name] = true
// We reimplement the unitchecker's -c=n flag.
// Don't allow it to be passed through.
if f.Name == "c" {
continue
}
isToolFlag[f.Name] = true
if cf.Lookup(f.Name) == nil {
if f.Bool {
cf.Bool(f.Name, false, "")
@ -117,22 +135,22 @@ func vetFlags(args []string) (passToVet, packageNames []string) {
}
}
// Record the set of vet tool flags set by GOFLAGS. We want to pass them to
// the vet tool, but only if they aren't overridden by an explicit argument.
base.SetFromGOFLAGS(&CmdVet.Flag)
// Record the set of tool flags set by GOFLAGS. We want to pass them to
// the tool, but only if they aren't overridden by an explicit argument.
base.SetFromGOFLAGS(&cmd.Flag)
addFromGOFLAGS := map[string]bool{}
CmdVet.Flag.Visit(func(f *flag.Flag) {
if isVetFlag[f.Name] {
cmd.Flag.Visit(func(f *flag.Flag) {
if isToolFlag[f.Name] {
addFromGOFLAGS[f.Name] = true
}
})
explicitFlags := make([]string, 0, len(args))
for len(args) > 0 {
f, remainingArgs, err := cmdflag.ParseOne(&CmdVet.Flag, args)
f, remainingArgs, err := cmdflag.ParseOne(&cmd.Flag, args)
if errors.Is(err, flag.ErrHelp) {
exitWithUsage()
exitWithUsage(cmd)
}
if errors.Is(err, cmdflag.ErrFlagTerminator) {
@ -151,12 +169,12 @@ func vetFlags(args []string) (passToVet, packageNames []string) {
if err != nil {
fmt.Fprintln(os.Stderr, err)
exitWithUsage()
exitWithUsage(cmd)
}
if isVetFlag[f.Name] {
if isToolFlag[f.Name] {
// Forward the raw arguments rather than cleaned equivalents, just in
// case the vet tool parses them idiosyncratically.
// case the tool parses them idiosyncratically.
explicitFlags = append(explicitFlags, args[:len(args)-len(remainingArgs)]...)
// This flag has been overridden explicitly, so don't forward its implicit
@ -168,26 +186,26 @@ func vetFlags(args []string) (passToVet, packageNames []string) {
}
// Prepend arguments from GOFLAGS before other arguments.
CmdVet.Flag.Visit(func(f *flag.Flag) {
cmd.Flag.Visit(func(f *flag.Flag) {
if addFromGOFLAGS[f.Name] {
passToVet = append(passToVet, fmt.Sprintf("-%s=%s", f.Name, f.Value))
passToTool = append(passToTool, fmt.Sprintf("-%s=%s", f.Name, f.Value))
}
})
passToVet = append(passToVet, explicitFlags...)
return passToVet, packageNames
passToTool = append(passToTool, explicitFlags...)
return passToTool, packageNames
}
func exitWithUsage() {
fmt.Fprintf(os.Stderr, "usage: %s\n", CmdVet.UsageLine)
fmt.Fprintf(os.Stderr, "Run 'go help %s' for details.\n", CmdVet.LongName())
func exitWithUsage(cmd *base.Command) {
fmt.Fprintf(os.Stderr, "usage: %s\n", cmd.UsageLine)
fmt.Fprintf(os.Stderr, "Run 'go help %s' for details.\n", cmd.LongName())
// This part is additional to what (*Command).Usage does:
cmd := "go tool vet"
if vetTool != "" {
cmd = vetTool
tool := toolFlag
if tool == "" {
tool = "go tool " + cmd.Name()
}
fmt.Fprintf(os.Stderr, "Run '%s help' for a full list of flags and analyzers.\n", cmd)
fmt.Fprintf(os.Stderr, "Run '%s -help' for an overview.\n", cmd)
fmt.Fprintf(os.Stderr, "Run '%s help' for a full list of flags and analyzers.\n", tool)
fmt.Fprintf(os.Stderr, "Run '%s -help' for an overview.\n", tool)
base.SetExitStatus(2)
base.Exit()

View file

@ -148,9 +148,10 @@ func (b *Builder) toolID(name string) string {
path := base.Tool(name)
desc := "go tool " + name
// Special case: undocumented -vettool overrides usual vet,
// for testing vet or supplying an alternative analysis tool.
if name == "vet" && VetTool != "" {
// Special case: -{vet,fix}tool overrides usual cmd/{vet,fix}
// for testing or supplying an alternative analysis tool.
// (We use only "vet" terminology in the action graph.)
if name == "vet" {
path = VetTool
desc = VetTool
}

View file

@ -1265,7 +1265,8 @@ func buildVetConfig(a *Action, srcfiles []string) {
}
}
// VetTool is the path to an alternate vet tool binary.
// VetTool is the path to the effective vet or fix tool binary.
// The user may specify a non-default value using -{vet,fix}tool.
// The caller is expected to set it (if needed) before executing any vet actions.
var VetTool string
@ -1273,7 +1274,13 @@ var VetTool string
// The caller is expected to set them before executing any vet actions.
var VetFlags []string
// VetExplicit records whether the vet flags were set explicitly on the command line.
// VetHandleStdout determines how the stdout output of each vet tool
// invocation should be handled. The default behavior is to copy it to
// the go command's stdout, atomically.
var VetHandleStdout = copyToStdout
// VetExplicit records whether the vet flags (which may include
// -{vet,fix}tool) were set explicitly on the command line.
var VetExplicit bool
func (b *Builder) vet(ctx context.Context, a *Action) error {
@ -1296,6 +1303,7 @@ func (b *Builder) vet(ctx context.Context, a *Action) error {
sh := b.Shell(a)
// We use "vet" terminology even when building action graphs for go fix.
vcfg.VetxOnly = a.VetxOnly
vcfg.VetxOutput = a.Objdir + "vet.out"
vcfg.Stdout = a.Objdir + "vet.stdout"
@ -1322,7 +1330,7 @@ func (b *Builder) vet(ctx context.Context, a *Action) error {
// dependency tree turn on *more* analysis, as here.
// (The unsafeptr check does not write any facts for use by
// later vet runs, nor does unreachable.)
if a.Package.Goroot && !VetExplicit && VetTool == "" {
if a.Package.Goroot && !VetExplicit && VetTool == base.Tool("vet") {
// Turn off -unsafeptr checks.
// There's too much unsafe.Pointer code
// that vet doesn't like in low-level packages
@ -1359,14 +1367,30 @@ func (b *Builder) vet(ctx context.Context, a *Action) error {
vcfg.PackageVetx[a1.Package.ImportPath] = a1.built
}
}
key := cache.ActionID(h.Sum())
vetxKey := cache.ActionID(h.Sum()) // for .vetx file
if vcfg.VetxOnly && !cfg.BuildA {
fmt.Fprintf(h, "stdout\n")
stdoutKey := cache.ActionID(h.Sum()) // for .stdout file
// Check the cache; -a forces a rebuild.
if !cfg.BuildA {
c := cache.Default()
if file, _, err := cache.GetFile(c, key); err == nil {
if vcfg.VetxOnly {
if file, _, err := cache.GetFile(c, vetxKey); err == nil {
a.built = file
return nil
}
} else {
// Copy cached vet.std files to stdout.
if file, _, err := cache.GetFile(c, stdoutKey); err == nil {
f, err := os.Open(file)
if err != nil {
return err
}
defer f.Close() // ignore error (can't fail)
return VetHandleStdout(f)
}
}
}
js, err := json.MarshalIndent(vcfg, "", "\t")
@ -1387,31 +1411,46 @@ func (b *Builder) vet(ctx context.Context, a *Action) error {
p := a.Package
tool := VetTool
if tool == "" {
tool = base.Tool("vet")
panic("VetTool unset")
}
runErr := sh.run(p.Dir, p.ImportPath, env, cfg.BuildToolexec, tool, vetFlags, a.Objdir+"vet.cfg")
// If vet wrote export data, save it for input to future vets.
if err := sh.run(p.Dir, p.ImportPath, env, cfg.BuildToolexec, tool, vetFlags, a.Objdir+"vet.cfg"); err != nil {
return err
}
// Vet tool succeeded, possibly with facts and JSON stdout. Save both in cache.
// Save facts
if f, err := os.Open(vcfg.VetxOutput); err == nil {
defer f.Close() // ignore error
a.built = vcfg.VetxOutput
cache.Default().Put(key, f) // ignore error
f.Close() // ignore error
cache.Default().Put(vetxKey, f) // ignore error
}
// If vet wrote to stdout, copy it to go's stdout, atomically.
// Save stdout.
if f, err := os.Open(vcfg.Stdout); err == nil {
stdoutMu.Lock()
if _, err := io.Copy(os.Stdout, f); err != nil && runErr == nil {
runErr = fmt.Errorf("copying vet tool stdout: %w", err)
defer f.Close() // ignore error
if err := VetHandleStdout(f); err != nil {
return err
}
f.Close() // ignore error
stdoutMu.Unlock()
f.Seek(0, io.SeekStart) // ignore error
cache.Default().Put(stdoutKey, f) // ignore error
}
return runErr
return nil
}
var stdoutMu sync.Mutex // serializes concurrent writes (e.g. JSON values) to stdout
var stdoutMu sync.Mutex // serializes concurrent writes (of e.g. JSON values) to stdout
// copyToStdout copies the stream to stdout while holding the lock.
func copyToStdout(r io.Reader) error {
stdoutMu.Lock()
defer stdoutMu.Unlock()
if _, err := io.Copy(os.Stdout, r); err != nil {
return fmt.Errorf("copying vet tool stdout: %w", err)
}
return nil
}
// linkActionID computes the action ID for a link action.
func (b *Builder) linkActionID(a *Action) cache.ActionID {

View file

@ -24,7 +24,6 @@ import (
"cmd/go/internal/clean"
"cmd/go/internal/doc"
"cmd/go/internal/envcmd"
"cmd/go/internal/fix"
"cmd/go/internal/fmtcmd"
"cmd/go/internal/generate"
"cmd/go/internal/help"
@ -55,7 +54,7 @@ func init() {
clean.CmdClean,
doc.CmdDoc,
envcmd.CmdEnv,
fix.CmdFix,
vet.CmdFix,
fmtcmd.CmdFmt,
generate.CmdGenerate,
modget.CmdGet,

View file

@ -27,6 +27,10 @@ stderr 'strings\.test'
go vet -C ../strings -n
stderr strings_test
# go fix
go fix -C ../strings -n
stderr strings_test
# -C must be first on command line (as of Go 1.21)
! go test -n -C ../strings
stderr '^invalid value "../strings" for flag -C: -C flag must be first flag on command line$'

View file

@ -1,12 +1,12 @@
env GO111MODULE=off
# Issue 27665. Verify that "go vet" analyzes non-Go files.
[!GOARCH:amd64] skip
env GO111MODULE=off
env GOARCH=amd64
! go vet -asmdecl a
stderr 'f: invalid MOVW of x'
# -c flag shows context
# -c=n flag shows n lines of context
! go vet -c=2 -asmdecl a
stderr '...invalid MOVW...'
stderr '1 .*TEXT'

125
src/cmd/go/testdata/script/vet_basic.txt vendored Normal file
View file

@ -0,0 +1,125 @@
# Test basic features of "go vet"/"go fix" CLI.
#
# The example relies on two analyzers:
# - hostport (which is included in both the fix and vet suites), and
# - printf (which is only in the vet suite).
# Each reports one diagnostic with a fix.
# vet default flags print diagnostics to stderr. Diagnostic => nonzero exit.
! go vet example.com/x
stderr 'does not work with IPv6'
stderr 'non-constant format string in call to fmt.Sprintf'
# -hostport runs only one analyzer. Diagnostic => failure.
! go vet -hostport example.com/x
stderr 'does not work with IPv6'
! stderr 'non-constant format string'
# -timeformat runs only one analyzer. No diagnostics => success.
go vet -timeformat example.com/x
! stderr .
# JSON output includes diagnostics and fixes. Always success.
go vet -json example.com/x
! stderr .
stdout '"example.com/x": {'
stdout '"hostport":'
stdout '"message": "address format .* does not work with IPv6",'
stdout '"suggested_fixes":'
stdout '"message": "Replace fmt.Sprintf with net.JoinHostPort",'
# vet -fix -diff displays a diff.
go vet -fix -diff example.com/x
stdout '\-var _ = fmt.Sprintf\(s\)'
stdout '\+var _ = fmt.Sprintf\("%s", s\)'
stdout '\-var _, _ = net.Dial\("tcp", fmt.Sprintf\("%s:%d", s, 80\)\)'
stdout '\+var _, _ = net.Dial\("tcp", net.JoinHostPort\(s, "80"\)\)'
# vet -fix quietly applies the vet suite fixes.
cp x.go x.go.bak
go vet -fix example.com/x
grep 'fmt.Sprintf\("%s", s\)' x.go
grep 'net.JoinHostPort' x.go
! stderr .
cp x.go.bak x.go
! go vet -diff example.com/x
stderr 'go vet -diff flag requires -fix'
# go fix applies the fix suite fixes.
go fix example.com/x
grep 'net.JoinHostPort' x.go
! grep 'fmt.Sprintf\("%s", s\)' x.go
! stderr .
cp x.go.bak x.go
# Show diff of fixes from the fix suite.
go fix -diff example.com/x
! stdout '\-var _ = fmt.Sprintf\(s\)'
stdout '\-var _, _ = net.Dial\("tcp", fmt.Sprintf\("%s:%d", s, 80\)\)'
stdout '\+var _, _ = net.Dial\("tcp", net.JoinHostPort\(s, "80"\)\)'
# Show fix-suite fixes in JSON form.
go fix -json example.com/x
! stderr .
stdout '"example.com/x": {'
stdout '"hostport":'
stdout '"message": "address format .* does not work with IPv6",'
stdout '"suggested_fixes":'
stdout '"message": "Replace fmt.Sprintf with net.JoinHostPort",'
! stdout '"printf":'
! stdout '"message": "non-constant format string.*",'
! stdout '"message": "Insert.*%s.*format.string",'
# Show vet-suite fixes in JSON form.
go vet -fix -json example.com/x
! stderr .
stdout '"example.com/x": {'
stdout '"hostport":'
stdout '"message": "address format .* does not work with IPv6",'
stdout '"suggested_fixes":'
stdout '"message": "Replace fmt.Sprintf with net.JoinHostPort",'
stdout '"printf":'
stdout '"message": "non-constant format string.*",'
stdout '"suggested_fixes":'
stdout '"message": "Insert.*%s.*format.string",'
# Reject -diff + -json.
! go fix -diff -json example.com/x
stderr '-json and -diff cannot be used together'
# Legacy way of selecting fixers is a no-op.
go fix -fix=old1,old2 example.com/x
stderr 'go fix: the -fix=old1,old2 flag is obsolete and has no effect'
# -c=n flag shows n lines of context.
! go vet -c=2 -printf example.com/x
stderr 'x.go:12:21: non-constant format string in call to fmt.Sprintf'
! stderr '9 '
stderr '10 '
stderr '11 // This call...'
stderr '12 var _ = fmt.Sprintf\(s\)'
stderr '13 '
stderr '14 '
! stderr '15 '
-- go.mod --
module example.com/x
go 1.25
-- x.go --
package x
import (
"fmt"
"net"
)
var s string
// This call yields a "non-constant format string" diagnostic, with a fix (go vet only).
var _ = fmt.Sprintf(s)
// This call yields a hostport diagnostic, with a fix (go vet and go fix).
var _, _ = net.Dial("tcp", fmt.Sprintf("%s:%d", s, 80))

View file

@ -0,0 +1,24 @@
# Test that go vet's caching of vet tool actions replays
# the recorded stderr output even after a cache hit.
# Set up fresh GOCACHE.
env GOCACHE=$WORK/gocache
# First time is a cache miss.
! go vet example.com/a
stderr 'fmt.Sprint call has possible Printf formatting directive'
# Second time is assumed to be a cache hit for the stdout JSON,
# but we don't bother to assert it. Same diagnostics again.
! go vet example.com/a
stderr 'fmt.Sprint call has possible Printf formatting directive'
-- go.mod --
module example.com
-- a/a.go --
package a
import "fmt"
var _ = fmt.Sprint("%s") // oops!

View file

@ -40,6 +40,7 @@ To list the available checks, run "go tool vet help":
directive check Go toolchain directives such as //go:debug
errorsas report passing non-pointer or non-error values to errors.As
framepointer report assembly that clobbers the frame pointer before saving it
hostport check format of addresses passed to net.Dial
httpresponse check for mistakes using HTTP responses
ifaceassert detect impossible interface-to-interface type assertions
loopclosure check references to loop variables from within nested functions
@ -50,6 +51,7 @@ To list the available checks, run "go tool vet help":
sigchanyzer check for unbuffered channel of os.Signal
slog check for invalid structured logging calls
stdmethods check signature of methods of well-known interfaces
stdversion report uses of too-new standard library symbols
stringintconv check for string(int) conversions
structtag check that struct field tags conform to reflect.StructTag.Get
testinggoroutine report calls to (*testing.T).Fatal from goroutines started by a test

View file

@ -7,10 +7,8 @@ package main
import (
"cmd/internal/objabi"
"cmd/internal/telemetry/counter"
"flag"
"golang.org/x/tools/go/analysis/unitchecker"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/appends"
"golang.org/x/tools/go/analysis/passes/asmdecl"
"golang.org/x/tools/go/analysis/passes/assign"
@ -46,14 +44,22 @@ import (
"golang.org/x/tools/go/analysis/passes/unsafeptr"
"golang.org/x/tools/go/analysis/passes/unusedresult"
"golang.org/x/tools/go/analysis/passes/waitgroup"
"golang.org/x/tools/go/analysis/unitchecker"
)
func main() {
// Keep consistent with cmd/fix/main.go!
counter.Open()
objabi.AddVersionFlag()
counter.Inc("vet/invocations")
unitchecker.Main(
unitchecker.Main(suite...) // (never returns)
}
// The vet suite analyzers report diagnostics.
// (Diagnostics must describe real problems, but need not
// suggest fixes, and fixes are not necessarily safe to apply.)
var suite = []*analysis.Analyzer{
appends.Analyzer,
asmdecl.Analyzer,
assign.Analyzer,
@ -66,6 +72,7 @@ func main() {
defers.Analyzer,
directive.Analyzer,
errorsas.Analyzer,
// fieldalignment.Analyzer omitted: too noisy
framepointer.Analyzer,
httpresponse.Analyzer,
hostport.Analyzer,
@ -74,6 +81,7 @@ func main() {
lostcancel.Analyzer,
nilfunc.Analyzer,
printf.Analyzer,
// shadow.Analyzer omitted: too noisy
shift.Analyzer,
sigchanyzer.Analyzer,
slog.Analyzer,
@ -89,9 +97,4 @@ func main() {
unsafeptr.Analyzer,
unusedresult.Analyzer,
waitgroup.Analyzer,
)
// It's possible that unitchecker will exit early. In
// those cases the flags won't be counted.
counter.CountFlags("vet/flag:", *flag.CommandLine)
}

View file

@ -162,7 +162,7 @@ func PrintfTests() {
Printf("hi") // ok
const format = "%s %s\n"
Printf(format, "hi", "there")
Printf(format, "hi") // ERROR "Printf format %s reads arg #2, but call has 1 arg$"
Printf(format, "hi") // ERROR "Printf format %s reads arg #2, but call has 1 arg"
Printf("%s %d %.3v %q", "str", 4) // ERROR "Printf format %.3v reads arg #3, but call has 2 args"
f := new(ptrStringer)
f.Warn(0, "%s", "hello", 3) // ERROR "Warn call has possible Printf formatting directive %s"

View file

@ -28,7 +28,8 @@ func TestMain(m *testing.M) {
os.Exit(0)
}
os.Setenv("GO_VETTEST_IS_VET", "1") // Set for subprocesses to inherit.
// Set for subprocesses to inherit.
os.Setenv("GO_VETTEST_IS_VET", "1") // ignore error
os.Exit(m.Run())
}
@ -115,7 +116,7 @@ func TestVet(t *testing.T) {
cmd.Env = append(os.Environ(), "GOWORK=off")
cmd.Dir = "testdata/rangeloop"
cmd.Stderr = new(strings.Builder) // all vet output goes to stderr
cmd.Run()
cmd.Run() // ignore error
stderr := cmd.Stderr.(fmt.Stringer).String()
filename := filepath.FromSlash("testdata/rangeloop/rangeloop.go")
@ -134,7 +135,7 @@ func TestVet(t *testing.T) {
if err := errorCheck(stderr, false, filename, filepath.Base(filename)); err != nil {
t.Errorf("error check failed: %s", err)
t.Log("vet stderr:\n", cmd.Stderr)
t.Logf("vet stderr:\n<<%s>>", cmd.Stderr)
}
})
@ -146,7 +147,7 @@ func TestVet(t *testing.T) {
cmd.Env = append(os.Environ(), "GOWORK=off")
cmd.Dir = "testdata/stdversion"
cmd.Stderr = new(strings.Builder) // all vet output goes to stderr
cmd.Run()
cmd.Run() // ignore error
stderr := cmd.Stderr.(fmt.Stringer).String()
filename := filepath.FromSlash("testdata/stdversion/stdversion.go")
@ -165,7 +166,7 @@ func TestVet(t *testing.T) {
if err := errorCheck(stderr, false, filename, filepath.Base(filename)); err != nil {
t.Errorf("error check failed: %s", err)
t.Log("vet stderr:\n", cmd.Stderr)
t.Logf("vet stderr:\n<<%s>>", cmd.Stderr)
}
})
}
@ -184,7 +185,7 @@ func cgoEnabled(t *testing.T) bool {
func errchk(c *exec.Cmd, files []string, t *testing.T) {
output, err := c.CombinedOutput()
if _, ok := err.(*exec.ExitError); !ok {
t.Logf("vet output:\n%s", output)
t.Logf("vet output:\n<<%s>>", output)
t.Fatal(err)
}
fullshort := make([]string, 0, len(files)*2)
@ -205,7 +206,6 @@ func TestTags(t *testing.T) {
"x testtag y": 1,
"othertag": 2,
} {
tag, wantFile := tag, wantFile
t.Run(tag, func(t *testing.T) {
t.Parallel()
t.Logf("-tags=%s", tag)
@ -266,7 +266,7 @@ func errorCheck(outStr string, wantAuto bool, fullshort ...string) (err error) {
errmsgs, out = partitionStrings(we.prefix, out)
}
if len(errmsgs) == 0 {
errs = append(errs, fmt.Errorf("%s:%d: missing error %q", we.file, we.lineNum, we.reStr))
errs = append(errs, fmt.Errorf("%s:%d: missing error %q (prefix: %s)", we.file, we.lineNum, we.reStr, we.prefix))
continue
}
matched := false