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.
7package analysisflags
8
9import (
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.
26var (
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.
43func 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
146func 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
161func 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.
194func 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".
201type versionFlag struct{}
202
203func (versionFlag) IsBoolFlag() bool { return true }
204func (versionFlag) Get() interface{} { return nil }
205func (versionFlag) String() string   { return "" }
206func (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.
245type triState int
246
247const (
248	unset triState = iota
249	setTrue
250	setFalse
251)
252
253func 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
260func (ts *triState) Get() interface{} {
261	return *ts == setTrue
262}
263
264func (ts triState) isTrue() bool {
265	return ts == setTrue
266}
267
268func (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
283func (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
295func (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.
303var 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.
322func 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.
345type 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.
350type 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.
360type 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.
368type 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.
380type 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.
387func (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
441func (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}
448