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

View as plain text