aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/cmd/dist/build.go2
-rw-r--r--src/cmd/fix/doc.go20
-rw-r--r--src/cmd/fix/main.go66
-rw-r--r--src/cmd/go/alldocs.go60
-rw-r--r--src/cmd/go/internal/fix/fix.go86
-rw-r--r--src/cmd/go/internal/test/test.go1
-rw-r--r--src/cmd/go/internal/vet/vet.go371
-rw-r--r--src/cmd/go/internal/vet/vetflag.go166
-rw-r--r--src/cmd/go/internal/work/buildid.go7
-rw-r--r--src/cmd/go/internal/work/exec.go81
-rw-r--r--src/cmd/go/main.go3
-rw-r--r--src/cmd/go/testdata/script/chdir.txt4
-rw-r--r--src/cmd/go/testdata/script/vet_asm.txt8
-rw-r--r--src/cmd/go/testdata/script/vet_basic.txt125
-rw-r--r--src/cmd/go/testdata/script/vet_cache.txt24
-rw-r--r--src/cmd/vet/doc.go2
-rw-r--r--src/cmd/vet/main.go91
-rw-r--r--src/cmd/vet/testdata/print/print.go2
-rw-r--r--src/cmd/vet/vet_test.go16
19 files changed, 806 insertions, 329 deletions
diff --git a/src/cmd/dist/build.go b/src/cmd/dist/build.go
index 9a7951726f..2fcdb2d391 100644
--- a/src/cmd/dist/build.go
+++ b/src/cmd/dist/build.go
@@ -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
diff --git a/src/cmd/fix/doc.go b/src/cmd/fix/doc.go
deleted file mode 100644
index b3d6914471..0000000000
--- a/src/cmd/fix/doc.go
+++ /dev/null
@@ -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
diff --git a/src/cmd/fix/main.go b/src/cmd/fix/main.go
index 87cc0d6414..422fa82745 100644
--- a/src/cmd/fix/main.go
+++ b/src/cmd/fix/main.go
@@ -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")
+
+ unitchecker.Main(suite...) // (never returns)
+}
- os.Exit(0)
+// 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.
}
diff --git a/src/cmd/go/alldocs.go b/src/cmd/go/alldocs.go
index 51f2223283..67c0ecbe8b 100644
--- a/src/cmd/go/alldocs.go
+++ b/src/cmd/go/alldocs.go
@@ -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:
+//
+// -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:
+// 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'.
diff --git a/src/cmd/go/internal/fix/fix.go b/src/cmd/go/internal/fix/fix.go
deleted file mode 100644
index 8947da05c3..0000000000
--- a/src/cmd/go/internal/fix/fix.go
+++ /dev/null
@@ -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))
- }
-}
diff --git a/src/cmd/go/internal/test/test.go b/src/cmd/go/internal/test/test.go
index 15ffc618c6..e667bd64f7 100644
--- a/src/cmd/go/internal/test/test.go
+++ b/src/cmd/go/internal/test/test.go
@@ -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)
diff --git a/src/cmd/go/internal/vet/vet.go b/src/cmd/go/internal/vet/vet.go
index 3514be80fe..a2625b2118 100644
--- a/src/cmd/go/internal/vet/vet.go
+++ b/src/cmd/go/internal/vet/vet.go
@@ -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")
+ }
+ } 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")
+ }
}
- if vetTool != "" {
- var err error
- work.VetTool, err = filepath.Abs(vetTool)
- if err != nil {
- base.Fatalf("%v", err)
+
+ // 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"`
+}
diff --git a/src/cmd/go/internal/vet/vetflag.go b/src/cmd/go/internal/vet/vetflag.go
index d0bdb58a50..7ebd8c9bfd 100644
--- a/src/cmd/go/internal/vet/vetflag.go
+++ b/src/cmd/go/internal/vet/vetflag.go
@@ -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 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)
-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", "", "")
+ 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()
diff --git a/src/cmd/go/internal/work/buildid.go b/src/cmd/go/internal/work/buildid.go
index 88c24b11ac..584c1ac6f4 100644
--- a/src/cmd/go/internal/work/buildid.go
+++ b/src/cmd/go/internal/work/buildid.go
@@ -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
}
diff --git a/src/cmd/go/internal/work/exec.go b/src/cmd/go/internal/work/exec.go
index 72b9177c9d..fa6ddce24b 100644
--- a/src/cmd/go/internal/work/exec.go
+++ b/src/cmd/go/internal/work/exec.go
@@ -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,13 +1367,29 @@ 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
+
+ fmt.Fprintf(h, "stdout\n")
+ stdoutKey := cache.ActionID(h.Sum()) // for .stdout file
- if vcfg.VetxOnly && !cfg.BuildA {
+ // Check the cache; -a forces a rebuild.
+ if !cfg.BuildA {
c := cache.Default()
- if file, _, err := cache.GetFile(c, key); err == nil {
- a.built = file
- return 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)
+ }
}
}
@@ -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")
+ }
+
+ if err := sh.run(p.Dir, p.ImportPath, env, cfg.BuildToolexec, tool, vetFlags, a.Objdir+"vet.cfg"); err != nil {
+ return err
}
- 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.
+ // 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 {
diff --git a/src/cmd/go/main.go b/src/cmd/go/main.go
index e81969ca4a..8cdfd9196e 100644
--- a/src/cmd/go/main.go
+++ b/src/cmd/go/main.go
@@ -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,
diff --git a/src/cmd/go/testdata/script/chdir.txt b/src/cmd/go/testdata/script/chdir.txt
index a6feed6b45..41def410d5 100644
--- a/src/cmd/go/testdata/script/chdir.txt
+++ b/src/cmd/go/testdata/script/chdir.txt
@@ -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$'
diff --git a/src/cmd/go/testdata/script/vet_asm.txt b/src/cmd/go/testdata/script/vet_asm.txt
index 8aa69ce1a3..c046773a06 100644
--- a/src/cmd/go/testdata/script/vet_asm.txt
+++ b/src/cmd/go/testdata/script/vet_asm.txt
@@ -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'
diff --git a/src/cmd/go/testdata/script/vet_basic.txt b/src/cmd/go/testdata/script/vet_basic.txt
new file mode 100644
index 0000000000..5ae66438ea
--- /dev/null
+++ b/src/cmd/go/testdata/script/vet_basic.txt
@@ -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))
diff --git a/src/cmd/go/testdata/script/vet_cache.txt b/src/cmd/go/testdata/script/vet_cache.txt
new file mode 100644
index 0000000000..c84844000a
--- /dev/null
+++ b/src/cmd/go/testdata/script/vet_cache.txt
@@ -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!
diff --git a/src/cmd/vet/doc.go b/src/cmd/vet/doc.go
index 8e72c252ed..ca20884561 100644
--- a/src/cmd/vet/doc.go
+++ b/src/cmd/vet/doc.go
@@ -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
diff --git a/src/cmd/vet/main.go b/src/cmd/vet/main.go
index 49f4e2f342..e7164a46b0 100644
--- a/src/cmd/vet/main.go
+++ b/src/cmd/vet/main.go
@@ -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,52 +44,57 @@ 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(
- appends.Analyzer,
- asmdecl.Analyzer,
- assign.Analyzer,
- atomic.Analyzer,
- bools.Analyzer,
- buildtag.Analyzer,
- cgocall.Analyzer,
- composite.Analyzer,
- copylock.Analyzer,
- defers.Analyzer,
- directive.Analyzer,
- errorsas.Analyzer,
- framepointer.Analyzer,
- httpresponse.Analyzer,
- hostport.Analyzer,
- ifaceassert.Analyzer,
- loopclosure.Analyzer,
- lostcancel.Analyzer,
- nilfunc.Analyzer,
- printf.Analyzer,
- shift.Analyzer,
- sigchanyzer.Analyzer,
- slog.Analyzer,
- stdmethods.Analyzer,
- stdversion.Analyzer,
- stringintconv.Analyzer,
- structtag.Analyzer,
- tests.Analyzer,
- testinggoroutine.Analyzer,
- timeformat.Analyzer,
- unmarshal.Analyzer,
- unreachable.Analyzer,
- 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)
+ 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,
+ atomic.Analyzer,
+ bools.Analyzer,
+ buildtag.Analyzer,
+ cgocall.Analyzer,
+ composite.Analyzer,
+ copylock.Analyzer,
+ defers.Analyzer,
+ directive.Analyzer,
+ errorsas.Analyzer,
+ // fieldalignment.Analyzer omitted: too noisy
+ framepointer.Analyzer,
+ httpresponse.Analyzer,
+ hostport.Analyzer,
+ ifaceassert.Analyzer,
+ loopclosure.Analyzer,
+ lostcancel.Analyzer,
+ nilfunc.Analyzer,
+ printf.Analyzer,
+ // shadow.Analyzer omitted: too noisy
+ shift.Analyzer,
+ sigchanyzer.Analyzer,
+ slog.Analyzer,
+ stdmethods.Analyzer,
+ stdversion.Analyzer,
+ stringintconv.Analyzer,
+ structtag.Analyzer,
+ tests.Analyzer,
+ testinggoroutine.Analyzer,
+ timeformat.Analyzer,
+ unmarshal.Analyzer,
+ unreachable.Analyzer,
+ unsafeptr.Analyzer,
+ unusedresult.Analyzer,
+ waitgroup.Analyzer,
}
diff --git a/src/cmd/vet/testdata/print/print.go b/src/cmd/vet/testdata/print/print.go
index e00222c42b..3761da420b 100644
--- a/src/cmd/vet/testdata/print/print.go
+++ b/src/cmd/vet/testdata/print/print.go
@@ -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"
diff --git a/src/cmd/vet/vet_test.go b/src/cmd/vet/vet_test.go
index 54eabca938..0d509de528 100644
--- a/src/cmd/vet/vet_test.go
+++ b/src/cmd/vet/vet_test.go
@@ -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