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 }