| // 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; |
| exit with a non-zero status if the diff is not empty |
| |
| 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; |
| exit with a non-zero status if the diff is not empty |
| |
| 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) { |
| moduleLoader := modload.NewLoader() |
| // 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. |
| moduleLoader.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(moduleLoader) |
| |
| // 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 (and exit 1 if diffs). |
| // go fix [-diff] => cmd/fix -fix [-diff] Pass stdout through (and exit 1 if diffs). |
| // 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") |
| // In -diff mode, the tool prints unified diffs to stdout. |
| // Copy stdout through and exit non-zero if diffs were printed, |
| // consistent with gofmt -d and go mod tidy -diff. |
| work.VetHandleStdout = copyAndDetectDiff |
| } 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\n", 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\n", cmd.Name()) |
| } |
| } |
| |
| work.VetFlags = toolFlags |
| |
| pkgOpts := load.PackageOpts{ModResolveTests: true} |
| pkgs := load.PackagesAndErrors(moduleLoader, ctx, pkgOpts, pkgArgs) |
| load.CheckPackageErrors(pkgs) |
| if len(pkgs) == 0 { |
| base.Fatalf("no packages to %s", cmd.Name()) |
| } |
| |
| // Build action graph. |
| b := work.NewBuilder("", moduleLoader.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(moduleLoader, 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(moduleLoader, 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 |
| } |
| |
| // copyAndDetectDiff copies the tool's stdout to the go command's stdout |
| // and sets exit status 1 if any output was produced (meaning diffs exist). |
| // This is used in -diff mode to implement the convention that "go fix -diff" |
| // exits non-zero when the diff is not empty, consistent with gofmt -d |
| // and go mod tidy -diff. |
| func copyAndDetectDiff(r io.Reader) error { |
| stdouterrMu.Lock() |
| defer stdouterrMu.Unlock() |
| n, err := io.Copy(os.Stdout, r) |
| if err != nil { |
| return fmt.Errorf("copying diff output: %w", err) |
| } |
| if n > 0 { |
| base.SetExitStatus(1) |
| } |
| 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 stdouterrMu sync.Mutex // serializes concurrent writes to stdout and stderr |
| |
| func printJSONDiagnostic(analyzer string, diag jsonDiagnostic) { |
| stdouterrMu.Lock() |
| defer stdouterrMu.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"` |
| } |