diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/cmd/dist/build.go | 2 | ||||
| -rw-r--r-- | src/cmd/fix/doc.go | 20 | ||||
| -rw-r--r-- | src/cmd/fix/main.go | 66 | ||||
| -rw-r--r-- | src/cmd/go/alldocs.go | 60 | ||||
| -rw-r--r-- | src/cmd/go/internal/fix/fix.go | 86 | ||||
| -rw-r--r-- | src/cmd/go/internal/test/test.go | 1 | ||||
| -rw-r--r-- | src/cmd/go/internal/vet/vet.go | 371 | ||||
| -rw-r--r-- | src/cmd/go/internal/vet/vetflag.go | 166 | ||||
| -rw-r--r-- | src/cmd/go/internal/work/buildid.go | 7 | ||||
| -rw-r--r-- | src/cmd/go/internal/work/exec.go | 81 | ||||
| -rw-r--r-- | src/cmd/go/main.go | 3 | ||||
| -rw-r--r-- | src/cmd/go/testdata/script/chdir.txt | 4 | ||||
| -rw-r--r-- | src/cmd/go/testdata/script/vet_asm.txt | 8 | ||||
| -rw-r--r-- | src/cmd/go/testdata/script/vet_basic.txt | 125 | ||||
| -rw-r--r-- | src/cmd/go/testdata/script/vet_cache.txt | 24 | ||||
| -rw-r--r-- | src/cmd/vet/doc.go | 2 | ||||
| -rw-r--r-- | src/cmd/vet/main.go | 91 | ||||
| -rw-r--r-- | src/cmd/vet/testdata/print/print.go | 2 | ||||
| -rw-r--r-- | src/cmd/vet/vet_test.go | 16 |
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 |
