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