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