Source file src/cmd/go/internal/vet/vet.go

     1  // Copyright 2011 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package vet implements the “go vet” and “go fix” commands.
     6  package vet
     7  
     8  import (
     9  	"archive/zip"
    10  	"bytes"
    11  	"context"
    12  	"encoding/json"
    13  	"errors"
    14  	"fmt"
    15  	"io"
    16  	"os"
    17  	"slices"
    18  	"strconv"
    19  	"strings"
    20  	"sync"
    21  
    22  	"cmd/go/internal/base"
    23  	"cmd/go/internal/cfg"
    24  	"cmd/go/internal/load"
    25  	"cmd/go/internal/modload"
    26  	"cmd/go/internal/trace"
    27  	"cmd/go/internal/work"
    28  )
    29  
    30  var CmdVet = &base.Command{
    31  	CustomFlags: true,
    32  	UsageLine:   "go vet [build flags] [-vettool prog] [vet flags] [packages]",
    33  	Short:       "report likely mistakes in packages",
    34  	Long: `
    35  Vet runs the Go vet tool (cmd/vet) on the named packages
    36  and reports diagnostics.
    37  
    38  It supports these flags:
    39  
    40    -c int
    41  	display offending line with this many lines of context (default -1)
    42    -json
    43  	emit JSON output
    44    -fix
    45  	instead of printing each diagnostic, apply its first fix (if any)
    46    -diff
    47  	instead of applying each fix, print the patch as a unified diff
    48  
    49  The -vettool=prog flag selects a different analysis tool with
    50  alternative or additional checks. For example, the 'shadow' analyzer
    51  can be built and run using these commands:
    52  
    53    go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest
    54    go vet -vettool=$(which shadow)
    55  
    56  Alternative vet tools should be built atop golang.org/x/tools/go/analysis/unitchecker,
    57  which handles the interaction with go vet.
    58  
    59  The default vet tool is 'go tool vet' or cmd/vet.
    60  For help on its checkers and their flags, run 'go tool vet help'.
    61  For details of a specific checker such as 'printf', see 'go tool vet help printf'.
    62  
    63  For more about specifying packages, see 'go help packages'.
    64  
    65  The build flags supported by go vet are those that control package resolution
    66  and execution, such as -C, -n, -x, -v, -tags, and -toolexec.
    67  For more about these flags, see 'go help build'.
    68  
    69  See also: go fmt, go fix.
    70  	`,
    71  }
    72  
    73  var CmdFix = &base.Command{
    74  	CustomFlags: true,
    75  	UsageLine:   "go fix [build flags] [-fixtool prog] [fix flags] [packages]",
    76  	Short:       "apply fixes suggested by static checkers",
    77  	Long: `
    78  Fix runs the Go fix tool (cmd/fix) on the named packages
    79  and applies suggested fixes.
    80  
    81  It supports these flags:
    82  
    83    -diff
    84  	instead of applying each fix, print the patch as a unified diff
    85  
    86  The -fixtool=prog flag selects a different analysis tool with
    87  alternative or additional fixers; see the documentation for go vet's
    88  -vettool flag for details.
    89  
    90  The default fix tool is 'go tool fix' or cmd/fix.
    91  For help on its fixers and their flags, run 'go tool fix help'.
    92  For details of a specific fixer such as 'hostport', see 'go tool fix help hostport'.
    93  
    94  For more about specifying packages, see 'go help packages'.
    95  
    96  The build flags supported by go fix are those that control package resolution
    97  and execution, such as -C, -n, -x, -v, -tags, and -toolexec.
    98  For more about these flags, see 'go help build'.
    99  
   100  See also: go fmt, go vet.
   101  	`,
   102  }
   103  
   104  func init() {
   105  	// avoid initialization cycle
   106  	CmdVet.Run = run
   107  	CmdFix.Run = run
   108  
   109  	addFlags(CmdVet)
   110  	addFlags(CmdFix)
   111  }
   112  
   113  var (
   114  	// "go vet -fix" causes fixes to be applied.
   115  	vetFixFlag = CmdVet.Flag.Bool("fix", false, "apply the first fix (if any) for each diagnostic")
   116  
   117  	// The "go fix -fix=name,..." flag is an obsolete flag formerly
   118  	// used to pass a list of names to the old "cmd/fix -r".
   119  	fixFixFlag = CmdFix.Flag.String("fix", "", "obsolete; no effect")
   120  )
   121  
   122  // run implements both "go vet" and "go fix".
   123  
   124  func run(ctx context.Context, cmd *base.Command, args []string) {
   125  	moduleLoaderState := modload.NewState()
   126  	// Compute flags for the vet/fix tool (e.g. cmd/{vet,fix}).
   127  	toolFlags, pkgArgs := toolFlags(cmd, args)
   128  
   129  	// The vet/fix commands do custom flag processing;
   130  	// initialize workspaces after that.
   131  	moduleLoaderState.InitWorkfile()
   132  
   133  	if cfg.DebugTrace != "" {
   134  		var close func() error
   135  		var err error
   136  		ctx, close, err = trace.Start(ctx, cfg.DebugTrace)
   137  		if err != nil {
   138  			base.Fatalf("failed to start trace: %v", err)
   139  		}
   140  		defer func() {
   141  			if err := close(); err != nil {
   142  				base.Fatalf("failed to stop trace: %v", err)
   143  			}
   144  		}()
   145  	}
   146  
   147  	ctx, span := trace.StartSpan(ctx, fmt.Sprint("Running ", cmd.Name(), " command"))
   148  	defer span.Done()
   149  
   150  	work.BuildInit(moduleLoaderState)
   151  
   152  	// Flag theory:
   153  	//
   154  	// All flags supported by unitchecker are accepted by go {vet,fix}.
   155  	// Some arise from each analyzer in the tool (both to enable it
   156  	// and to configure it), whereas others [-V -c -diff -fix -flags -json]
   157  	// are core to unitchecker itself.
   158  	//
   159  	// Most are passed through to toolFlags, but not all:
   160  	// * -V and -flags are used by the handshake in the [toolFlags] function;
   161  	// * these old flags have no effect: [-all -source -tags -v]; and
   162  	// * the [-c -fix -diff -json] flags are handled specially
   163  	//   as described below:
   164  	//
   165  	// command args                 tool args
   166  	// go vet               =>      cmd/vet -json           Parse stdout, print diagnostics to stderr.
   167  	// go vet -json         =>      cmd/vet -json           Pass stdout through.
   168  	// go vet -fix [-diff]  =>      cmd/vet -fix [-diff]    Pass stdout through.
   169  	// go fix [-diff]       =>      cmd/fix -fix [-diff]    Pass stdout through.
   170  	// go fix -json         =>      cmd/fix -json           Pass stdout through.
   171  	//
   172  	// Notes:
   173  	// * -diff requires "go vet -fix" or "go fix", and no -json.
   174  	// * -json output is the same in "vet" and "fix" modes,
   175  	//   and describes both diagnostics and fixes (but does not apply them).
   176  	// * -c=n is supported by the unitchecker, but we reimplement it
   177  	//   here (see printDiagnostics), and do not pass the flag through.
   178  
   179  	work.VetExplicit = len(toolFlags) > 0
   180  
   181  	applyFixes := false
   182  	if cmd.Name() == "fix" || *vetFixFlag {
   183  		// fix mode: 'go fix' or 'go vet -fix'
   184  		if jsonFlag {
   185  			if diffFlag {
   186  				base.Fatalf("-json and -diff cannot be used together")
   187  			}
   188  		} else {
   189  			toolFlags = append(toolFlags, "-fix")
   190  			if diffFlag {
   191  				toolFlags = append(toolFlags, "-diff")
   192  			} else {
   193  				applyFixes = true
   194  			}
   195  		}
   196  		if contextFlag != -1 {
   197  			base.Fatalf("-c flag cannot be used when applying fixes")
   198  		}
   199  	} else {
   200  		// vet mode: 'go vet' without -fix
   201  		if !jsonFlag {
   202  			// Post-process the JSON diagnostics on stdout and format
   203  			// it as "file:line: message" diagnostics on stderr.
   204  			// (JSON reliably frames diagnostics, fixes, and errors so
   205  			// that we don't have to parse stderr or interpret non-zero
   206  			// exit codes, and interacts better with the action cache.)
   207  			toolFlags = append(toolFlags, "-json")
   208  			work.VetHandleStdout = printJSONDiagnostics
   209  		}
   210  		if diffFlag {
   211  			base.Fatalf("go vet -diff flag requires -fix")
   212  		}
   213  	}
   214  
   215  	// Implement legacy "go fix -fix=name,..." flag.
   216  	if *fixFixFlag != "" {
   217  		fmt.Fprintf(os.Stderr, "go %s: the -fix=%s flag is obsolete and has no effect", cmd.Name(), *fixFixFlag)
   218  
   219  		// The buildtag fixer is now implemented by cmd/fix.
   220  		if slices.Contains(strings.Split(*fixFixFlag, ","), "buildtag") {
   221  			fmt.Fprintf(os.Stderr, "go %s: to enable the buildtag check, use -buildtag", cmd.Name())
   222  		}
   223  	}
   224  
   225  	work.VetFlags = toolFlags
   226  
   227  	pkgOpts := load.PackageOpts{ModResolveTests: true}
   228  	pkgs := load.PackagesAndErrors(moduleLoaderState, ctx, pkgOpts, pkgArgs)
   229  	load.CheckPackageErrors(pkgs)
   230  	if len(pkgs) == 0 {
   231  		base.Fatalf("no packages to %s", cmd.Name())
   232  	}
   233  
   234  	// Build action graph.
   235  	b := work.NewBuilder("", moduleLoaderState.VendorDirOrEmpty)
   236  	defer func() {
   237  		if err := b.Close(); err != nil {
   238  			base.Fatal(err)
   239  		}
   240  	}()
   241  
   242  	root := &work.Action{Mode: "go " + cmd.Name()}
   243  
   244  	addVetAction := func(p *load.Package) {
   245  		act := b.VetAction(moduleLoaderState, work.ModeBuild, work.ModeBuild, applyFixes, p)
   246  		root.Deps = append(root.Deps, act)
   247  	}
   248  
   249  	// To avoid file corruption from duplicate application of
   250  	// fixes (in fix mode), and duplicate reporting of diagnostics
   251  	// (in vet mode), we must run the tool only once for each
   252  	// source file. We achieve that by running on ptest (below)
   253  	// instead of p.
   254  	//
   255  	// As a side benefit, this also allows analyzers to make
   256  	// "closed world" assumptions and report diagnostics (such as
   257  	// "this symbol is unused") that might be false if computed
   258  	// from just the primary package p, falsified by the
   259  	// additional declarations in test files.
   260  	//
   261  	// We needn't worry about intermediate test variants, as they
   262  	// will only be executed in VetxOnly mode, for facts but not
   263  	// diagnostics.
   264  	for _, p := range pkgs {
   265  		// Don't apply fixes to vendored packages, including
   266  		// the GOROOT vendor packages that are part of std,
   267  		// or to packages from non-main modules (#76479).
   268  		if applyFixes {
   269  			if p.Standard && strings.HasPrefix(p.ImportPath, "vendor/") ||
   270  				p.Module != nil && !p.Module.Main {
   271  				continue
   272  			}
   273  		}
   274  		_, ptest, pxtest, perr := load.TestPackagesFor(moduleLoaderState, ctx, pkgOpts, p, nil)
   275  		if perr != nil {
   276  			base.Errorf("%v", perr.Error)
   277  			continue
   278  		}
   279  		if len(ptest.GoFiles) == 0 && len(ptest.CgoFiles) == 0 && pxtest == nil {
   280  			base.Errorf("go: can't %s %s: no Go files in %s", cmd.Name(), p.ImportPath, p.Dir)
   281  			continue
   282  		}
   283  		if len(ptest.GoFiles) > 0 || len(ptest.CgoFiles) > 0 {
   284  			// The test package includes all the files of primary package.
   285  			addVetAction(ptest)
   286  		}
   287  		if pxtest != nil {
   288  			addVetAction(pxtest)
   289  		}
   290  	}
   291  	b.Do(ctx, root)
   292  
   293  	// Apply fixes.
   294  	//
   295  	// We do this as a separate phase after the build to avoid
   296  	// races between source file updates and reads of those same
   297  	// files by concurrent actions of the ongoing build.
   298  	//
   299  	// If a file is fixed by multiple actions, they must be consistent.
   300  	if applyFixes {
   301  		contents := make(map[string][]byte)
   302  		// Gather the fixes.
   303  		for _, act := range root.Deps {
   304  			if act.FixArchive != "" {
   305  				if err := readZip(act.FixArchive, contents); err != nil {
   306  					base.Errorf("reading archive of fixes: %v", err)
   307  					return
   308  				}
   309  			}
   310  		}
   311  		// Apply them.
   312  		for filename, content := range contents {
   313  			if err := os.WriteFile(filename, content, 0644); err != nil {
   314  				base.Errorf("applying fix: %v", err)
   315  			}
   316  		}
   317  	}
   318  }
   319  
   320  // readZip reads the zipfile entries into the provided map.
   321  // It reports an error if updating the map would change an existing entry.
   322  func readZip(zipfile string, out map[string][]byte) error {
   323  	r, err := zip.OpenReader(zipfile)
   324  	if err != nil {
   325  		return err
   326  	}
   327  	defer r.Close() // ignore error
   328  	for _, f := range r.File {
   329  		rc, err := f.Open()
   330  		if err != nil {
   331  			return err
   332  		}
   333  		content, err := io.ReadAll(rc)
   334  		rc.Close() // ignore error
   335  		if err != nil {
   336  			return err
   337  		}
   338  		if prev, ok := out[f.Name]; ok && !bytes.Equal(prev, content) {
   339  			return fmt.Errorf("inconsistent fixes to file %v", f.Name)
   340  		}
   341  		out[f.Name] = content
   342  	}
   343  	return nil
   344  }
   345  
   346  // printJSONDiagnostics parses JSON (from the tool's stdout) and
   347  // prints it (to stderr) in "file:line: message" form.
   348  // It also ensures that we exit nonzero if there were diagnostics.
   349  func printJSONDiagnostics(r io.Reader) error {
   350  	stdout, err := io.ReadAll(r)
   351  	if err != nil {
   352  		return err
   353  	}
   354  	if len(stdout) > 0 {
   355  		// unitchecker emits a JSON map of the form:
   356  		// output maps Package ID -> Analyzer.Name -> (error | []Diagnostic);
   357  		var tree jsonTree
   358  		if err := json.Unmarshal(stdout, &tree); err != nil {
   359  			return fmt.Errorf("parsing JSON: %v", err)
   360  		}
   361  		for _, units := range tree {
   362  			for analyzer, msg := range units {
   363  				if msg[0] == '[' {
   364  					// []Diagnostic
   365  					var diags []jsonDiagnostic
   366  					if err := json.Unmarshal([]byte(msg), &diags); err != nil {
   367  						return fmt.Errorf("parsing JSON diagnostics: %v", err)
   368  					}
   369  					for _, diag := range diags {
   370  						base.SetExitStatus(1)
   371  						printJSONDiagnostic(analyzer, diag)
   372  					}
   373  				} else {
   374  					// error
   375  					var e jsonError
   376  					if err := json.Unmarshal([]byte(msg), &e); err != nil {
   377  						return fmt.Errorf("parsing JSON error: %v", err)
   378  					}
   379  
   380  					base.SetExitStatus(1)
   381  					return errors.New(e.Err)
   382  				}
   383  			}
   384  		}
   385  	}
   386  	return nil
   387  }
   388  
   389  var stderrMu sync.Mutex // serializes concurrent writes to stdout
   390  
   391  func printJSONDiagnostic(analyzer string, diag jsonDiagnostic) {
   392  	stderrMu.Lock()
   393  	defer stderrMu.Unlock()
   394  
   395  	type posn struct {
   396  		file      string
   397  		line, col int
   398  	}
   399  	parsePosn := func(s string) (_ posn, _ bool) {
   400  		colon2 := strings.LastIndexByte(s, ':')
   401  		if colon2 < 0 {
   402  			return
   403  		}
   404  		colon1 := strings.LastIndexByte(s[:colon2], ':')
   405  		if colon1 < 0 {
   406  			return
   407  		}
   408  		line, err := strconv.Atoi(s[colon1+len(":") : colon2])
   409  		if err != nil {
   410  			return
   411  		}
   412  		col, err := strconv.Atoi(s[colon2+len(":"):])
   413  		if err != nil {
   414  			return
   415  		}
   416  		return posn{s[:colon1], line, col}, true
   417  	}
   418  
   419  	print := func(start, end, message string) {
   420  		if posn, ok := parsePosn(start); ok {
   421  			// The (*work.Shell).reportCmd method relativizes the
   422  			// prefix of each line of the subprocess's stdout;
   423  			// but filenames in JSON aren't at the start of the line,
   424  			// so we need to apply ShortPath here too.
   425  			fmt.Fprintf(os.Stderr, "%s:%d:%d: %v\n", base.ShortPath(posn.file), posn.line, posn.col, message)
   426  		} else {
   427  			fmt.Fprintf(os.Stderr, "%s: %v\n", start, message)
   428  		}
   429  
   430  		// -c=n: show offending line plus N lines of context.
   431  		// (Duplicates logic in unitchecker; see analysisflags.PrintPlain.)
   432  		if contextFlag >= 0 {
   433  			if end == "" {
   434  				end = start
   435  			}
   436  			var (
   437  				startPosn, ok1 = parsePosn(start)
   438  				endPosn, ok2   = parsePosn(end)
   439  			)
   440  			if ok1 && ok2 {
   441  				// TODO(adonovan): respect overlays (like unitchecker does).
   442  				data, _ := os.ReadFile(startPosn.file)
   443  				lines := strings.Split(string(data), "\n")
   444  				for i := startPosn.line - contextFlag; i <= endPosn.line+contextFlag; i++ {
   445  					if 1 <= i && i <= len(lines) {
   446  						fmt.Fprintf(os.Stderr, "%d\t%s\n", i, lines[i-1])
   447  					}
   448  				}
   449  			}
   450  		}
   451  	}
   452  
   453  	// TODO(adonovan): append  " [analyzer]" to message. But we must first relax
   454  	// x/tools/go/analysis/internal/versiontest.TestVettool and revendor; sigh.
   455  	_ = analyzer
   456  	print(diag.Posn, diag.End, diag.Message)
   457  	for _, rel := range diag.Related {
   458  		print(rel.Posn, rel.End, "\t"+rel.Message)
   459  	}
   460  }
   461  
   462  // -- JSON schema --
   463  
   464  // (populated by golang.org/x/tools/go/analysis/internal/analysisflags/flags.go)
   465  
   466  // A jsonTree is a mapping from package ID to analysis name to result.
   467  // Each result is either a jsonError or a list of jsonDiagnostic.
   468  type jsonTree map[string]map[string]json.RawMessage
   469  
   470  type jsonError struct {
   471  	Err string `json:"error"`
   472  }
   473  
   474  // A jsonTextEdit describes the replacement of a portion of a file.
   475  // Start and End are zero-based half-open indices into the original byte
   476  // sequence of the file, and New is the new text.
   477  type jsonTextEdit struct {
   478  	Filename string `json:"filename"`
   479  	Start    int    `json:"start"`
   480  	End      int    `json:"end"`
   481  	New      string `json:"new"`
   482  }
   483  
   484  // A jsonSuggestedFix describes an edit that should be applied as a whole or not
   485  // at all. It might contain multiple TextEdits/text_edits if the SuggestedFix
   486  // consists of multiple non-contiguous edits.
   487  type jsonSuggestedFix struct {
   488  	Message string         `json:"message"`
   489  	Edits   []jsonTextEdit `json:"edits"`
   490  }
   491  
   492  // A jsonDiagnostic describes the json schema of an analysis.Diagnostic.
   493  type jsonDiagnostic struct {
   494  	Category       string                   `json:"category,omitempty"`
   495  	Posn           string                   `json:"posn"` // e.g. "file.go:line:column"
   496  	End            string                   `json:"end"`
   497  	Message        string                   `json:"message"`
   498  	SuggestedFixes []jsonSuggestedFix       `json:"suggested_fixes,omitempty"`
   499  	Related        []jsonRelatedInformation `json:"related,omitempty"`
   500  }
   501  
   502  // A jsonRelatedInformation describes a secondary position and message related to
   503  // a primary diagnostic.
   504  type jsonRelatedInformation struct {
   505  	Posn    string `json:"posn"` // e.g. "file.go:line:column"
   506  	End     string `json:"end"`
   507  	Message string `json:"message"`
   508  }
   509  

View as plain text