github.com/cockroachdb/tools@v0.0.0-20230222021103-a6d27438930d/go/analysis/internal/analysisflags/flags.go (about)

     1  // Copyright 2018 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 analysisflags defines helpers for processing flags of
     6  // analysis driver tools.
     7  package analysisflags
     8  
     9  import (
    10  	"crypto/sha256"
    11  	"encoding/gob"
    12  	"encoding/json"
    13  	"flag"
    14  	"fmt"
    15  	"go/token"
    16  	"io"
    17  	"io/ioutil"
    18  	"log"
    19  	"os"
    20  	"strconv"
    21  	"strings"
    22  
    23  	"golang.org/x/tools/go/analysis"
    24  )
    25  
    26  // flags common to all {single,multi,unit}checkers.
    27  var (
    28  	JSON    = false // -json
    29  	Context = -1    // -c=N: if N>0, display offending line plus N lines of context
    30  )
    31  
    32  // Parse creates a flag for each of the analyzer's flags,
    33  // including (in multi mode) a flag named after the analyzer,
    34  // parses the flags, then filters and returns the list of
    35  // analyzers enabled by flags.
    36  //
    37  // The result is intended to be passed to unitchecker.Run or checker.Run.
    38  // Use in unitchecker.Run will gob.Register all fact types for the returned
    39  // graph of analyzers but of course not the ones only reachable from
    40  // dropped analyzers. To avoid inconsistency about which gob types are
    41  // registered from run to run, Parse itself gob.Registers all the facts
    42  // only reachable from dropped analyzers.
    43  // This is not a particularly elegant API, but this is an internal package.
    44  func Parse(analyzers []*analysis.Analyzer, multi bool) []*analysis.Analyzer {
    45  	// Connect each analysis flag to the command line as -analysis.flag.
    46  	enabled := make(map[*analysis.Analyzer]*triState)
    47  	for _, a := range analyzers {
    48  		var prefix string
    49  
    50  		// Add -NAME flag to enable it.
    51  		if multi {
    52  			prefix = a.Name + "."
    53  
    54  			enable := new(triState)
    55  			enableUsage := "enable " + a.Name + " analysis"
    56  			flag.Var(enable, a.Name, enableUsage)
    57  			enabled[a] = enable
    58  		}
    59  
    60  		a.Flags.VisitAll(func(f *flag.Flag) {
    61  			if !multi && flag.Lookup(f.Name) != nil {
    62  				log.Printf("%s flag -%s would conflict with driver; skipping", a.Name, f.Name)
    63  				return
    64  			}
    65  
    66  			name := prefix + f.Name
    67  			flag.Var(f.Value, name, f.Usage)
    68  		})
    69  	}
    70  
    71  	// standard flags: -flags, -V.
    72  	printflags := flag.Bool("flags", false, "print analyzer flags in JSON")
    73  	addVersionFlag()
    74  
    75  	// flags common to all checkers
    76  	flag.BoolVar(&JSON, "json", JSON, "emit JSON output")
    77  	flag.IntVar(&Context, "c", Context, `display offending line with this many lines of context`)
    78  
    79  	// Add shims for legacy vet flags to enable existing
    80  	// scripts that run vet to continue to work.
    81  	_ = flag.Bool("source", false, "no effect (deprecated)")
    82  	_ = flag.Bool("v", false, "no effect (deprecated)")
    83  	_ = flag.Bool("all", false, "no effect (deprecated)")
    84  	_ = flag.String("tags", "", "no effect (deprecated)")
    85  	for old, new := range vetLegacyFlags {
    86  		newFlag := flag.Lookup(new)
    87  		if newFlag != nil && flag.Lookup(old) == nil {
    88  			flag.Var(newFlag.Value, old, "deprecated alias for -"+new)
    89  		}
    90  	}
    91  
    92  	flag.Parse() // (ExitOnError)
    93  
    94  	// -flags: print flags so that go vet knows which ones are legitimate.
    95  	if *printflags {
    96  		printFlags()
    97  		os.Exit(0)
    98  	}
    99  
   100  	everything := expand(analyzers)
   101  
   102  	// If any -NAME flag is true,  run only those analyzers. Otherwise,
   103  	// if any -NAME flag is false, run all but those analyzers.
   104  	if multi {
   105  		var hasTrue, hasFalse bool
   106  		for _, ts := range enabled {
   107  			switch *ts {
   108  			case setTrue:
   109  				hasTrue = true
   110  			case setFalse:
   111  				hasFalse = true
   112  			}
   113  		}
   114  
   115  		var keep []*analysis.Analyzer
   116  		if hasTrue {
   117  			for _, a := range analyzers {
   118  				if *enabled[a] == setTrue {
   119  					keep = append(keep, a)
   120  				}
   121  			}
   122  			analyzers = keep
   123  		} else if hasFalse {
   124  			for _, a := range analyzers {
   125  				if *enabled[a] != setFalse {
   126  					keep = append(keep, a)
   127  				}
   128  			}
   129  			analyzers = keep
   130  		}
   131  	}
   132  
   133  	// Register fact types of skipped analyzers
   134  	// in case we encounter them in imported files.
   135  	kept := expand(analyzers)
   136  	for a := range everything {
   137  		if !kept[a] {
   138  			for _, f := range a.FactTypes {
   139  				gob.Register(f)
   140  			}
   141  		}
   142  	}
   143  
   144  	return analyzers
   145  }
   146  
   147  func expand(analyzers []*analysis.Analyzer) map[*analysis.Analyzer]bool {
   148  	seen := make(map[*analysis.Analyzer]bool)
   149  	var visitAll func([]*analysis.Analyzer)
   150  	visitAll = func(analyzers []*analysis.Analyzer) {
   151  		for _, a := range analyzers {
   152  			if !seen[a] {
   153  				seen[a] = true
   154  				visitAll(a.Requires)
   155  			}
   156  		}
   157  	}
   158  	visitAll(analyzers)
   159  	return seen
   160  }
   161  
   162  func printFlags() {
   163  	type jsonFlag struct {
   164  		Name  string
   165  		Bool  bool
   166  		Usage string
   167  	}
   168  	var flags []jsonFlag = nil
   169  	flag.VisitAll(func(f *flag.Flag) {
   170  		// Don't report {single,multi}checker debugging
   171  		// flags or fix as these have no effect on unitchecker
   172  		// (as invoked by 'go vet').
   173  		switch f.Name {
   174  		case "debug", "cpuprofile", "memprofile", "trace", "fix":
   175  			return
   176  		}
   177  
   178  		b, ok := f.Value.(interface{ IsBoolFlag() bool })
   179  		isBool := ok && b.IsBoolFlag()
   180  		flags = append(flags, jsonFlag{f.Name, isBool, f.Usage})
   181  	})
   182  	data, err := json.MarshalIndent(flags, "", "\t")
   183  	if err != nil {
   184  		log.Fatal(err)
   185  	}
   186  	os.Stdout.Write(data)
   187  }
   188  
   189  // addVersionFlag registers a -V flag that, if set,
   190  // prints the executable version and exits 0.
   191  //
   192  // If the -V flag already exists — for example, because it was already
   193  // registered by a call to cmd/internal/objabi.AddVersionFlag — then
   194  // addVersionFlag does nothing.
   195  func addVersionFlag() {
   196  	if flag.Lookup("V") == nil {
   197  		flag.Var(versionFlag{}, "V", "print version and exit")
   198  	}
   199  }
   200  
   201  // versionFlag minimally complies with the -V protocol required by "go vet".
   202  type versionFlag struct{}
   203  
   204  func (versionFlag) IsBoolFlag() bool { return true }
   205  func (versionFlag) Get() interface{} { return nil }
   206  func (versionFlag) String() string   { return "" }
   207  func (versionFlag) Set(s string) error {
   208  	if s != "full" {
   209  		log.Fatalf("unsupported flag value: -V=%s (use -V=full)", s)
   210  	}
   211  
   212  	// This replicates the minimal subset of
   213  	// cmd/internal/objabi.AddVersionFlag, which is private to the
   214  	// go tool yet forms part of our command-line interface.
   215  	// TODO(adonovan): clarify the contract.
   216  
   217  	// Print the tool version so the build system can track changes.
   218  	// Formats:
   219  	//   $progname version devel ... buildID=...
   220  	//   $progname version go1.9.1
   221  	progname, err := os.Executable()
   222  	if err != nil {
   223  		return err
   224  	}
   225  	f, err := os.Open(progname)
   226  	if err != nil {
   227  		log.Fatal(err)
   228  	}
   229  	h := sha256.New()
   230  	if _, err := io.Copy(h, f); err != nil {
   231  		log.Fatal(err)
   232  	}
   233  	f.Close()
   234  	fmt.Printf("%s version devel comments-go-here buildID=%02x\n",
   235  		progname, string(h.Sum(nil)))
   236  	os.Exit(0)
   237  	return nil
   238  }
   239  
   240  // A triState is a boolean that knows whether
   241  // it has been set to either true or false.
   242  // It is used to identify whether a flag appears;
   243  // the standard boolean flag cannot
   244  // distinguish missing from unset.
   245  // It also satisfies flag.Value.
   246  type triState int
   247  
   248  const (
   249  	unset triState = iota
   250  	setTrue
   251  	setFalse
   252  )
   253  
   254  func triStateFlag(name string, value triState, usage string) *triState {
   255  	flag.Var(&value, name, usage)
   256  	return &value
   257  }
   258  
   259  // triState implements flag.Value, flag.Getter, and flag.boolFlag.
   260  // They work like boolean flags: we can say vet -printf as well as vet -printf=true
   261  func (ts *triState) Get() interface{} {
   262  	return *ts == setTrue
   263  }
   264  
   265  func (ts triState) isTrue() bool {
   266  	return ts == setTrue
   267  }
   268  
   269  func (ts *triState) Set(value string) error {
   270  	b, err := strconv.ParseBool(value)
   271  	if err != nil {
   272  		// This error message looks poor but package "flag" adds
   273  		// "invalid boolean value %q for -NAME: %s"
   274  		return fmt.Errorf("want true or false")
   275  	}
   276  	if b {
   277  		*ts = setTrue
   278  	} else {
   279  		*ts = setFalse
   280  	}
   281  	return nil
   282  }
   283  
   284  func (ts *triState) String() string {
   285  	switch *ts {
   286  	case unset:
   287  		return "true"
   288  	case setTrue:
   289  		return "true"
   290  	case setFalse:
   291  		return "false"
   292  	}
   293  	panic("not reached")
   294  }
   295  
   296  func (ts triState) IsBoolFlag() bool {
   297  	return true
   298  }
   299  
   300  // Legacy flag support
   301  
   302  // vetLegacyFlags maps flags used by legacy vet to their corresponding
   303  // new names. The old names will continue to work.
   304  var vetLegacyFlags = map[string]string{
   305  	// Analyzer name changes
   306  	"bool":       "bools",
   307  	"buildtags":  "buildtag",
   308  	"methods":    "stdmethods",
   309  	"rangeloops": "loopclosure",
   310  
   311  	// Analyzer flags
   312  	"compositewhitelist":  "composites.whitelist",
   313  	"printfuncs":          "printf.funcs",
   314  	"shadowstrict":        "shadow.strict",
   315  	"unusedfuncs":         "unusedresult.funcs",
   316  	"unusedstringmethods": "unusedresult.stringmethods",
   317  }
   318  
   319  // ---- output helpers common to all drivers ----
   320  
   321  // PrintPlain prints a diagnostic in plain text form,
   322  // with context specified by the -c flag.
   323  func PrintPlain(fset *token.FileSet, diag analysis.Diagnostic) {
   324  	posn := fset.Position(diag.Pos)
   325  	fmt.Fprintf(os.Stderr, "%s: %s\n", posn, diag.Message)
   326  
   327  	// -c=N: show offending line plus N lines of context.
   328  	if Context >= 0 {
   329  		posn := fset.Position(diag.Pos)
   330  		end := fset.Position(diag.End)
   331  		if !end.IsValid() {
   332  			end = posn
   333  		}
   334  		data, _ := ioutil.ReadFile(posn.Filename)
   335  		lines := strings.Split(string(data), "\n")
   336  		for i := posn.Line - Context; i <= end.Line+Context; i++ {
   337  			if 1 <= i && i <= len(lines) {
   338  				fmt.Fprintf(os.Stderr, "%d\t%s\n", i, lines[i-1])
   339  			}
   340  		}
   341  	}
   342  }
   343  
   344  // A JSONTree is a mapping from package ID to analysis name to result.
   345  // Each result is either a jsonError or a list of JSONDiagnostic.
   346  type JSONTree map[string]map[string]interface{}
   347  
   348  // A TextEdit describes the replacement of a portion of a file.
   349  // Start and End are zero-based half-open indices into the original byte
   350  // sequence of the file, and New is the new text.
   351  type JSONTextEdit struct {
   352  	Filename string `json:"filename"`
   353  	Start    int    `json:"start"`
   354  	End      int    `json:"end"`
   355  	New      string `json:"new"`
   356  }
   357  
   358  // A JSONSuggestedFix describes an edit that should be applied as a whole or not
   359  // at all. It might contain multiple TextEdits/text_edits if the SuggestedFix
   360  // consists of multiple non-contiguous edits.
   361  type JSONSuggestedFix struct {
   362  	Message string         `json:"message"`
   363  	Edits   []JSONTextEdit `json:"edits"`
   364  }
   365  
   366  // A JSONDiagnostic can be used to encode and decode analysis.Diagnostics to and
   367  // from JSON.
   368  // TODO(matloob): Should the JSON diagnostics contain ranges?
   369  // If so, how should they be formatted?
   370  type JSONDiagnostic struct {
   371  	Category       string             `json:"category,omitempty"`
   372  	Posn           string             `json:"posn"`
   373  	Message        string             `json:"message"`
   374  	SuggestedFixes []JSONSuggestedFix `json:"suggested_fixes,omitempty"`
   375  }
   376  
   377  // Add adds the result of analysis 'name' on package 'id'.
   378  // The result is either a list of diagnostics or an error.
   379  func (tree JSONTree) Add(fset *token.FileSet, id, name string, diags []analysis.Diagnostic, err error) {
   380  	var v interface{}
   381  	if err != nil {
   382  		type jsonError struct {
   383  			Err string `json:"error"`
   384  		}
   385  		v = jsonError{err.Error()}
   386  	} else if len(diags) > 0 {
   387  		diagnostics := make([]JSONDiagnostic, 0, len(diags))
   388  		for _, f := range diags {
   389  			var fixes []JSONSuggestedFix
   390  			for _, fix := range f.SuggestedFixes {
   391  				var edits []JSONTextEdit
   392  				for _, edit := range fix.TextEdits {
   393  					edits = append(edits, JSONTextEdit{
   394  						Filename: fset.Position(edit.Pos).Filename,
   395  						Start:    fset.Position(edit.Pos).Offset,
   396  						End:      fset.Position(edit.End).Offset,
   397  						New:      string(edit.NewText),
   398  					})
   399  				}
   400  				fixes = append(fixes, JSONSuggestedFix{
   401  					Message: fix.Message,
   402  					Edits:   edits,
   403  				})
   404  			}
   405  			jdiag := JSONDiagnostic{
   406  				Category:       f.Category,
   407  				Posn:           fset.Position(f.Pos).String(),
   408  				Message:        f.Message,
   409  				SuggestedFixes: fixes,
   410  			}
   411  			diagnostics = append(diagnostics, jdiag)
   412  		}
   413  		v = diagnostics
   414  	}
   415  	if v != nil {
   416  		m, ok := tree[id]
   417  		if !ok {
   418  			m = make(map[string]interface{})
   419  			tree[id] = m
   420  		}
   421  		m[name] = v
   422  	}
   423  }
   424  
   425  func (tree JSONTree) Print() {
   426  	data, err := json.MarshalIndent(tree, "", "\t")
   427  	if err != nil {
   428  		log.Panicf("internal error: JSON marshaling failed: %v", err)
   429  	}
   430  	fmt.Printf("%s\n", data)
   431  }