// 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 vet implements the “go vet” and “go fix” commands. package vet import ( "archive/zip" "bytes" "context" "encoding/json" "errors" "fmt" "io" "os" "slices" "strconv" "strings" "sync" "cmd/go/internal/base" "cmd/go/internal/cfg" "cmd/go/internal/load" "cmd/go/internal/modload" "cmd/go/internal/trace" "cmd/go/internal/work" ) 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 tool (cmd/vet) on the named packages and reports diagnostics. 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: 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. The default vet tool is 'go tool vet' or cmd/vet. For help on its checkers and their flags, run 'go tool vet help'. For details of a specific checker such as 'printf', see 'go tool vet help printf'. For more about specifying packages, see 'go help packages'. 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'. See also: go fmt, go fix. `, } 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/fix) 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 fixers; see the documentation for go vet's -vettool flag for details. The default fix tool is 'go tool fix' or cmd/fix. For help on its fixers and their flags, run 'go tool fix help'. For details of a specific fixer such as 'hostport', see 'go tool fix help hostport'. For more about specifying packages, see 'go help packages'. 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) { moduleLoaderState := modload.NewState() // 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. moduleLoaderState.InitWorkfile() if cfg.DebugTrace != "" { var close func() error var err error ctx, close, err = trace.Start(ctx, cfg.DebugTrace) if err != nil { base.Fatalf("failed to start trace: %v", err) } defer func() { if err := close(); err != nil { base.Fatalf("failed to stop trace: %v", err) } }() } ctx, span := trace.StartSpan(ctx, fmt.Sprint("Running ", cmd.Name(), " command")) defer span.Done() work.BuildInit(moduleLoaderState) // 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 applyFixes := false 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") } else { applyFixes = true } } if contextFlag != -1 { base.Fatalf("-c flag cannot be used when applying fixes") } } else { // vet mode: 'go vet' without -fix if !jsonFlag { // Post-process the JSON diagnostics on stdout and format // it as "file:line: message" diagnostics on stderr. // (JSON reliably frames diagnostics, fixes, and errors so // that we don't have to parse stderr or interpret non-zero // exit codes, and interacts better with the action cache.) toolFlags = append(toolFlags, "-json") work.VetHandleStdout = printJSONDiagnostics } if diffFlag { base.Fatalf("go vet -diff flag requires -fix") } } // Implement legacy "go fix -fix=name,..." flag. if *fixFixFlag != "" { fmt.Fprintf(os.Stderr, "go %s: the -fix=%s flag is obsolete and has no effect", cmd.Name(), *fixFixFlag) // The buildtag fixer is now implemented by cmd/fix. if slices.Contains(strings.Split(*fixFixFlag, ","), "buildtag") { fmt.Fprintf(os.Stderr, "go %s: to enable the buildtag check, use -buildtag", cmd.Name()) } } work.VetFlags = toolFlags pkgOpts := load.PackageOpts{ModResolveTests: true} pkgs := load.PackagesAndErrors(moduleLoaderState, ctx, pkgOpts, pkgArgs) load.CheckPackageErrors(pkgs) if len(pkgs) == 0 { base.Fatalf("no packages to %s", cmd.Name()) } // Build action graph. b := work.NewBuilder("", moduleLoaderState.VendorDirOrEmpty) defer func() { if err := b.Close(); err != nil { base.Fatal(err) } }() root := &work.Action{Mode: "go " + cmd.Name()} addVetAction := func(p *load.Package) { act := b.VetAction(moduleLoaderState, work.ModeBuild, work.ModeBuild, applyFixes, p) root.Deps = append(root.Deps, act) } // 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. for _, p := range pkgs { // Don't apply fixes to vendored packages, including // the GOROOT vendor packages that are part of std, // or to packages from non-main modules (#76479). if applyFixes { if p.Standard && strings.HasPrefix(p.ImportPath, "vendor/") || p.Module != nil && !p.Module.Main { continue } } _, ptest, pxtest, perr := load.TestPackagesFor(moduleLoaderState, ctx, pkgOpts, p, nil) if perr != nil { base.Errorf("%v", perr.Error) continue } if len(ptest.GoFiles) == 0 && len(ptest.CgoFiles) == 0 && pxtest == nil { 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. addVetAction(ptest) } if pxtest != nil { addVetAction(pxtest) } } b.Do(ctx, root) // Apply fixes. // // We do this as a separate phase after the build to avoid // races between source file updates and reads of those same // files by concurrent actions of the ongoing build. // // If a file is fixed by multiple actions, they must be consistent. if applyFixes { contents := make(map[string][]byte) // Gather the fixes. for _, act := range root.Deps { if act.FixArchive != "" { if err := readZip(act.FixArchive, contents); err != nil { base.Errorf("reading archive of fixes: %v", err) return } } } // Apply them. for filename, content := range contents { if err := os.WriteFile(filename, content, 0644); err != nil { base.Errorf("applying fix: %v", err) } } } } // readZip reads the zipfile entries into the provided map. // It reports an error if updating the map would change an existing entry. func readZip(zipfile string, out map[string][]byte) error { r, err := zip.OpenReader(zipfile) if err != nil { return err } defer r.Close() // ignore error for _, f := range r.File { rc, err := f.Open() if err != nil { return err } content, err := io.ReadAll(rc) rc.Close() // ignore error if err != nil { return err } if prev, ok := out[f.Name]; ok && !bytes.Equal(prev, content) { return fmt.Errorf("inconsistent fixes to file %v", f.Name) } out[f.Name] = content } return nil } // 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(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 jsonTextEdit 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 jsonRelatedInformation 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"` }