| 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", 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 := os.Args[0] |
| 222 | f, err := os.Open(progname) |
| 223 | if err != nil { |
| 224 | log.Fatal(err) |
| 225 | } |
| 226 | h := sha256.New() |
| 227 | if _, err := io.Copy(h, f); err != nil { |
| 228 | log.Fatal(err) |
| 229 | } |
| 230 | f.Close() |
| 231 | fmt.Printf("%s version devel comments-go-here buildID=%02x\n", |
| 232 | progname, string(h.Sum(nil))) |
| 233 | os.Exit(0) |
| 234 | return nil |
| 235 | } |
| 236 | |
| 237 | // A triState is a boolean that knows whether |
| 238 | // it has been set to either true or false. |
| 239 | // It is used to identify whether a flag appears; |
| 240 | // the standard boolean flag cannot |
| 241 | // distinguish missing from unset. |
| 242 | // It also satisfies flag.Value. |
| 243 | type triState int |
| 244 | |
| 245 | const ( |
| 246 | unset triState = iota |
| 247 | setTrue |
| 248 | setFalse |
| 249 | ) |
| 250 | |
| 251 | func triStateFlag(name string, value triState, usage string) *triState { |
| 252 | flag.Var(&value, name, usage) |
| 253 | return &value |
| 254 | } |
| 255 | |
| 256 | // triState implements flag.Value, flag.Getter, and flag.boolFlag. |
| 257 | // They work like boolean flags: we can say vet -printf as well as vet -printf=true |
| 258 | func (ts *triState) Get() interface{} { |
| 259 | return *ts == setTrue |
| 260 | } |
| 261 | |
| 262 | func (ts triState) isTrue() bool { |
| 263 | return ts == setTrue |
| 264 | } |
| 265 | |
| 266 | func (ts *triState) Set(value string) error { |
| 267 | b, err := strconv.ParseBool(value) |
| 268 | if err != nil { |
| 269 | // This error message looks poor but package "flag" adds |
| 270 | // "invalid boolean value %q for -NAME: %s" |
| 271 | return fmt.Errorf("want true or false") |
| 272 | } |
| 273 | if b { |
| 274 | *ts = setTrue |
| 275 | } else { |
| 276 | *ts = setFalse |
| 277 | } |
| 278 | return nil |
| 279 | } |
| 280 | |
| 281 | func (ts *triState) String() string { |
| 282 | switch *ts { |
| 283 | case unset: |
| 284 | return "true" |
| 285 | case setTrue: |
| 286 | return "true" |
| 287 | case setFalse: |
| 288 | return "false" |
| 289 | } |
| 290 | panic("not reached") |
| 291 | } |
| 292 | |
| 293 | func (ts triState) IsBoolFlag() bool { |
| 294 | return true |
| 295 | } |
| 296 | |
| 297 | // Legacy flag support |
| 298 | |
| 299 | // vetLegacyFlags maps flags used by legacy vet to their corresponding |
| 300 | // new names. The old names will continue to work. |
| 301 | var vetLegacyFlags = map[string]string{ |
| 302 | // Analyzer name changes |
| 303 | "bool": "bools", |
| 304 | "buildtags": "buildtag", |
| 305 | "methods": "stdmethods", |
| 306 | "rangeloops": "loopclosure", |
| 307 | |
| 308 | // Analyzer flags |
| 309 | "compositewhitelist": "composites.whitelist", |
| 310 | "printfuncs": "printf.funcs", |
| 311 | "shadowstrict": "shadow.strict", |
| 312 | "unusedfuncs": "unusedresult.funcs", |
| 313 | "unusedstringmethods": "unusedresult.stringmethods", |
| 314 | } |
| 315 | |
| 316 | // ---- output helpers common to all drivers ---- |
| 317 | |
| 318 | // PrintPlain prints a diagnostic in plain text form, |
| 319 | // with context specified by the -c flag. |
| 320 | func PrintPlain(fset *token.FileSet, diag analysis.Diagnostic) { |
| 321 | posn := fset.Position(diag.Pos) |
| 322 | fmt.Fprintf(os.Stderr, "%s: %s\n", posn, diag.Message) |
| 323 | |
| 324 | // -c=N: show offending line plus N lines of context. |
| 325 | if Context >= 0 { |
| 326 | posn := fset.Position(diag.Pos) |
| 327 | end := fset.Position(diag.End) |
| 328 | if !end.IsValid() { |
| 329 | end = posn |
| 330 | } |
| 331 | data, _ := ioutil.ReadFile(posn.Filename) |
| 332 | lines := strings.Split(string(data), "\n") |
| 333 | for i := posn.Line - Context; i <= end.Line+Context; i++ { |
| 334 | if 1 <= i && i <= len(lines) { |
| 335 | fmt.Fprintf(os.Stderr, "%d\t%s\n", i, lines[i-1]) |
| 336 | } |
| 337 | } |
| 338 | } |
| 339 | } |
| 340 | |
| 341 | // A JSONTree is a mapping from package ID to analysis name to result. |
| 342 | // Each result is either a jsonError or a list of JSONDiagnostic. |
| 343 | type JSONTree map[string]map[string]interface{} |
| 344 | |
| 345 | // A TextEdit describes the replacement of a portion of a file. |
| 346 | // Start and End are zero-based half-open indices into the original byte |
| 347 | // sequence of the file, and New is the new text. |
| 348 | type JSONTextEdit struct { |
| 349 | Filename string `json:"filename"` |
| 350 | Start int `json:"start"` |
| 351 | End int `json:"end"` |
| 352 | New string `json:"new"` |
| 353 | } |
| 354 | |
| 355 | // A JSONSuggestedFix describes an edit that should be applied as a whole or not |
| 356 | // at all. It might contain multiple TextEdits/text_edits if the SuggestedFix |
| 357 | // consists of multiple non-contiguous edits. |
| 358 | type JSONSuggestedFix struct { |
| 359 | Message string `json:"message"` |
| 360 | Edits []JSONTextEdit `json:"edits"` |
| 361 | } |
| 362 | |
| 363 | // A JSONDiagnostic can be used to encode and decode analysis.Diagnostics to and |
| 364 | // from JSON. |
| 365 | // TODO(matloob): Should the JSON diagnostics contain ranges? |
| 366 | // If so, how should they be formatted? |
| 367 | type JSONDiagnostic struct { |
| 368 | Category string `json:"category,omitempty"` |
| 369 | Posn string `json:"posn"` |
| 370 | Message string `json:"message"` |
| 371 | SuggestedFixes []JSONSuggestedFix `json:"suggested_fixes,omitempty"` |
| 372 | } |
| 373 | |
| 374 | // Add adds the result of analysis 'name' on package 'id'. |
| 375 | // The result is either a list of diagnostics or an error. |
| 376 | func (tree JSONTree) Add(fset *token.FileSet, id, name string, diags []analysis.Diagnostic, err error) { |
| 377 | var v interface{} |
| 378 | if err != nil { |
| 379 | type jsonError struct { |
| 380 | Err string `json:"error"` |
| 381 | } |
| 382 | v = jsonError{err.Error()} |
| 383 | } else if len(diags) > 0 { |
| 384 | diagnostics := make([]JSONDiagnostic, 0, len(diags)) |
| 385 | for _, f := range diags { |
| 386 | var fixes []JSONSuggestedFix |
| 387 | for _, fix := range f.SuggestedFixes { |
| 388 | var edits []JSONTextEdit |
| 389 | for _, edit := range fix.TextEdits { |
| 390 | edits = append(edits, JSONTextEdit{ |
| 391 | Filename: fset.Position(edit.Pos).Filename, |
| 392 | Start: fset.Position(edit.Pos).Offset, |
| 393 | End: fset.Position(edit.End).Offset, |
| 394 | New: string(edit.NewText), |
| 395 | }) |
| 396 | } |
| 397 | fixes = append(fixes, JSONSuggestedFix{ |
| 398 | Message: fix.Message, |
| 399 | Edits: edits, |
| 400 | }) |
| 401 | } |
| 402 | jdiag := JSONDiagnostic{ |
| 403 | Category: f.Category, |
| 404 | Posn: fset.Position(f.Pos).String(), |
| 405 | Message: f.Message, |
| 406 | SuggestedFixes: fixes, |
| 407 | } |
| 408 | diagnostics = append(diagnostics, jdiag) |
| 409 | } |
| 410 | v = diagnostics |
| 411 | } |
| 412 | if v != nil { |
| 413 | m, ok := tree[id] |
| 414 | if !ok { |
| 415 | m = make(map[string]interface{}) |
| 416 | tree[id] = m |
| 417 | } |
| 418 | m[name] = v |
| 419 | } |
| 420 | } |
| 421 | |
| 422 | func (tree JSONTree) Print() { |
| 423 | data, err := json.MarshalIndent(tree, "", "\t") |
| 424 | if err != nil { |
| 425 | log.Panicf("internal error: JSON marshaling failed: %v", err) |
| 426 | } |
| 427 | fmt.Printf("%s\n", data) |
| 428 | } |
| 429 |
Members