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

     1  // Copyright 2017 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
     6  
     7  import (
     8  	"bytes"
     9  	"encoding/json"
    10  	"errors"
    11  	"flag"
    12  	"fmt"
    13  	"log"
    14  	"os"
    15  	"os/exec"
    16  	"path/filepath"
    17  	"strings"
    18  
    19  	"cmd/go/internal/base"
    20  	"cmd/go/internal/cmdflag"
    21  	"cmd/go/internal/work"
    22  )
    23  
    24  // go vet/fix flag processing
    25  var (
    26  	// We query the flags of the tool specified by -{vet,fix}tool
    27  	// and accept any of those flags plus any flag valid for 'go
    28  	// build'. The tool must support -flags, which prints a
    29  	// description of its flags in JSON to stdout.
    30  
    31  	// toolFlag specifies the vet/fix command to run.
    32  	// Any toolFlag that supports the (unpublished) vet
    33  	// command-line protocol may be supplied; see
    34  	// golang.org/x/tools/go/analysis/unitchecker for the
    35  	// sole implementation. It is also used by tests.
    36  	//
    37  	// The default behavior ("") runs 'go tool {vet,fix}'.
    38  	//
    39  	// Do not access this flag directly; use [parseToolFlag].
    40  	toolFlag    string // -{vet,fix}tool
    41  	diffFlag    bool   // -diff
    42  	jsonFlag    bool   // -json
    43  	contextFlag = -1   // -c=n
    44  )
    45  
    46  func addFlags(cmd *base.Command) {
    47  	// We run the compiler for export data.
    48  	// Suppress the build -json flag; we define our own.
    49  	work.AddBuildFlags(cmd, work.OmitJSONFlag)
    50  
    51  	cmd.Flag.StringVar(&toolFlag, cmd.Name()+"tool", "", "") // -vettool or -fixtool
    52  	cmd.Flag.BoolVar(&diffFlag, "diff", false, "print diff instead of applying it")
    53  	cmd.Flag.BoolVar(&jsonFlag, "json", false, "print diagnostics and fixes as JSON")
    54  	cmd.Flag.IntVar(&contextFlag, "c", -1, "display offending line with this many lines of context")
    55  }
    56  
    57  // parseToolFlag scans args for -{vet,fix}tool and returns the effective tool filename.
    58  func parseToolFlag(cmd *base.Command, args []string) string {
    59  	toolFlagName := cmd.Name() + "tool" // vettool or fixtool
    60  
    61  	// Extract -{vet,fix}tool by ad hoc flag processing:
    62  	// its value is needed even before we can declare
    63  	// the flags available during main flag processing.
    64  	for i, arg := range args {
    65  		if arg == "-"+toolFlagName || arg == "--"+toolFlagName {
    66  			if i+1 >= len(args) {
    67  				log.Fatalf("%s requires a filename", arg)
    68  			}
    69  			toolFlag = args[i+1]
    70  			break
    71  		} else if strings.HasPrefix(arg, "-"+toolFlagName+"=") ||
    72  			strings.HasPrefix(arg, "--"+toolFlagName+"=") {
    73  			toolFlag = arg[strings.IndexByte(arg, '=')+1:]
    74  			break
    75  		}
    76  	}
    77  
    78  	if toolFlag != "" {
    79  		tool, err := filepath.Abs(toolFlag)
    80  		if err != nil {
    81  			log.Fatal(err)
    82  		}
    83  		return tool
    84  	}
    85  
    86  	return base.Tool(cmd.Name()) // default to 'go tool vet|fix'
    87  }
    88  
    89  // toolFlags processes the command line, splitting it at the first non-flag
    90  // into the list of flags and list of packages.
    91  func toolFlags(cmd *base.Command, args []string) (passToTool, packageNames []string) {
    92  	tool := parseToolFlag(cmd, args)
    93  	work.VetTool = tool
    94  
    95  	// Query the tool for its flags.
    96  	out := new(bytes.Buffer)
    97  	toolcmd := exec.Command(tool, "-flags")
    98  	toolcmd.Stdout = out
    99  	if err := toolcmd.Run(); err != nil {
   100  		fmt.Fprintf(os.Stderr, "go: %s -flags failed: %v\n", tool, err)
   101  		base.SetExitStatus(2)
   102  		base.Exit()
   103  	}
   104  	var analysisFlags []struct {
   105  		Name  string
   106  		Bool  bool
   107  		Usage string
   108  	}
   109  	if err := json.Unmarshal(out.Bytes(), &analysisFlags); err != nil {
   110  		fmt.Fprintf(os.Stderr, "go: can't unmarshal JSON from %s -flags: %v", tool, err)
   111  		base.SetExitStatus(2)
   112  		base.Exit()
   113  	}
   114  
   115  	// Add tool's flags to cmd.Flag.
   116  	//
   117  	// Some flags, in particular -tags and -v, are known to the tool but
   118  	// also defined as build flags. This works fine, so we omit duplicates here.
   119  	// However some, like -x, are known to the build but not to the tool.
   120  	isToolFlag := make(map[string]bool, len(analysisFlags))
   121  	cf := cmd.Flag
   122  	for _, f := range analysisFlags {
   123  		// We reimplement the unitchecker's -c=n flag.
   124  		// Don't allow it to be passed through.
   125  		if f.Name == "c" {
   126  			continue
   127  		}
   128  		isToolFlag[f.Name] = true
   129  		if cf.Lookup(f.Name) == nil {
   130  			if f.Bool {
   131  				cf.Bool(f.Name, false, "")
   132  			} else {
   133  				cf.String(f.Name, "", "")
   134  			}
   135  		}
   136  	}
   137  
   138  	// Record the set of tool flags set by GOFLAGS. We want to pass them to
   139  	// the tool, but only if they aren't overridden by an explicit argument.
   140  	base.SetFromGOFLAGS(&cmd.Flag)
   141  	addFromGOFLAGS := map[string]bool{}
   142  	cmd.Flag.Visit(func(f *flag.Flag) {
   143  		if isToolFlag[f.Name] {
   144  			addFromGOFLAGS[f.Name] = true
   145  		}
   146  	})
   147  
   148  	explicitFlags := make([]string, 0, len(args))
   149  	for len(args) > 0 {
   150  		f, remainingArgs, err := cmdflag.ParseOne(&cmd.Flag, args)
   151  
   152  		if errors.Is(err, flag.ErrHelp) {
   153  			exitWithUsage(cmd)
   154  		}
   155  
   156  		if errors.Is(err, cmdflag.ErrFlagTerminator) {
   157  			// All remaining args must be package names, but the flag terminator is
   158  			// not included.
   159  			packageNames = remainingArgs
   160  			break
   161  		}
   162  
   163  		if _, ok := errors.AsType[cmdflag.NonFlagError](err); ok {
   164  			// Everything from here on out — including the argument we just consumed —
   165  			// must be a package name.
   166  			packageNames = args
   167  			break
   168  		}
   169  
   170  		if err != nil {
   171  			fmt.Fprintln(os.Stderr, err)
   172  			exitWithUsage(cmd)
   173  		}
   174  
   175  		if isToolFlag[f.Name] {
   176  			// Forward the raw arguments rather than cleaned equivalents, just in
   177  			// case the tool parses them idiosyncratically.
   178  			explicitFlags = append(explicitFlags, args[:len(args)-len(remainingArgs)]...)
   179  
   180  			// This flag has been overridden explicitly, so don't forward its implicit
   181  			// value from GOFLAGS.
   182  			delete(addFromGOFLAGS, f.Name)
   183  		}
   184  
   185  		args = remainingArgs
   186  	}
   187  
   188  	// Prepend arguments from GOFLAGS before other arguments.
   189  	cmd.Flag.Visit(func(f *flag.Flag) {
   190  		if addFromGOFLAGS[f.Name] {
   191  			passToTool = append(passToTool, fmt.Sprintf("-%s=%s", f.Name, f.Value))
   192  		}
   193  	})
   194  	passToTool = append(passToTool, explicitFlags...)
   195  	return passToTool, packageNames
   196  }
   197  
   198  func exitWithUsage(cmd *base.Command) {
   199  	fmt.Fprintf(os.Stderr, "usage: %s\n", cmd.UsageLine)
   200  	fmt.Fprintf(os.Stderr, "Run 'go help %s' for details.\n", cmd.LongName())
   201  
   202  	// This part is additional to what (*Command).Usage does:
   203  	tool := toolFlag
   204  	if tool == "" {
   205  		tool = "go tool " + cmd.Name()
   206  	}
   207  	fmt.Fprintf(os.Stderr, "Run '%s help' for a full list of flags and analyzers.\n", tool)
   208  	fmt.Fprintf(os.Stderr, "Run '%s -help' for an overview.\n", tool)
   209  
   210  	base.SetExitStatus(2)
   211  	base.Exit()
   212  }
   213  

View as plain text