From 51aacb140279dbef8da334580f2b6105110a7604 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 28 Sep 2018 10:56:40 -0400 Subject: [PATCH] go/analysis: add command-line help The format of Analyzer.Doc is now specified as a short title followed by a longer description. This allows us to build a nice self-documenting command-line interface. Much of the documentation in vet's doc.go and file-level comments can now be displayed to the user. Change-Id: I462343e97ac9b743284aaa3e06e7a81d11e9593f Reviewed-on: https://go-review.googlesource.com/138396 Reviewed-by: Michael Matloob --- go/analysis/analysis.go | 2 + go/analysis/multichecker/multichecker.go | 90 +++++++++++++++++++++- go/analysis/passes/findcall/findcall.go | 9 ++- go/analysis/singlechecker/singlechecker.go | 22 +++++- 4 files changed, 118 insertions(+), 5 deletions(-) diff --git a/go/analysis/analysis.go b/go/analysis/analysis.go index 21fcb372..84a76f4f 100644 --- a/go/analysis/analysis.go +++ b/go/analysis/analysis.go @@ -32,6 +32,8 @@ type Analyzer struct { Name string // Doc is the documentation for the analyzer. + // The part before the first "\n\n" is the title + // (no capital or period, max ~60 letters). Doc string // Flags defines any flags accepted by the analyzer. diff --git a/go/analysis/multichecker/multichecker.go b/go/analysis/multichecker/multichecker.go index 562839d7..ec7243a0 100644 --- a/go/analysis/multichecker/multichecker.go +++ b/go/analysis/multichecker/multichecker.go @@ -5,12 +5,21 @@ package multichecker import ( "flag" + "fmt" "log" + "os" + "sort" + "strings" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/internal/checker" ) +const usage = `Analyze is a tool for static analysis of Go programs. + +Usage: analyze [-flag] [package] +` + func Main(analyzers ...*analysis.Analyzer) { if err := analysis.Validate(analyzers); err != nil { log.Fatal(err) @@ -47,7 +56,86 @@ func Main(analyzers ...*analysis.Analyzer) { analyzers = keep } - if err := checker.Run(flag.Args(), analyzers); err != nil { + args := flag.Args() + if len(args) == 0 { + fmt.Fprintln(os.Stderr, usage) + fmt.Fprintln(os.Stderr, `Run 'analyze help' for more detail, + or 'analyze help name' for details and flags of a specific analyzer.`) + os.Exit(1) + } + + if args[0] == "help" { + help(analyzers, args[1:]) + os.Exit(0) + } + + if err := checker.Run(args, analyzers); err != nil { log.Fatal(err) } } + +func help(analyzers []*analysis.Analyzer, args []string) { + // No args: show summary of all analyzers. + if len(args) == 0 { + fmt.Println(usage) + fmt.Println("Registered analyzers:") + fmt.Println() + sort.Slice(analyzers, func(i, j int) bool { + return analyzers[i].Name < analyzers[j].Name + }) + for _, a := range analyzers { + title := strings.Split(a.Doc, "\n\n")[0] + fmt.Printf(" %-12s %s\n", a.Name, title) + } + fmt.Println("\nBy default all analyzers are run.") + fmt.Println("To select specific analyzers, use the -NAME.enable flag for each one.") + + // Show only the core command-line flags. + fmt.Println("\nCore flags:") + fmt.Println() + fs := flag.NewFlagSet("", flag.ExitOnError) + flag.VisitAll(func(f *flag.Flag) { + if !strings.Contains(f.Name, ".") { + fs.Var(f.Value, f.Name, f.Usage) + } + }) + fs.PrintDefaults() + + fmt.Println("\nTo see details and flags of a specific analyzer, run 'analyze help name'.") + + return + } + + // Show help on specific analyzer(s). +outer: + for _, arg := range args { + for _, a := range analyzers { + if a.Name == arg { + paras := strings.Split(a.Doc, "\n\n") + title := paras[0] + fmt.Printf("%s: %s\n", a.Name, title) + + // Show only the flags relating to this analysis, + // properly prefixed. + first := true + fs := flag.NewFlagSet(a.Name, flag.ExitOnError) + a.Flags.VisitAll(func(f *flag.Flag) { + if first { + first = false + fmt.Println("\nAnalyzer flags:") + fmt.Println() + } + fs.Var(f.Value, a.Name+"."+f.Name, f.Usage) + }) + fs.PrintDefaults() + + if len(paras) > 1 { + fmt.Printf("\n%s\n", strings.Join(paras[1:], "\n\n")) + } + + continue outer + } + } + log.Fatalf("Analyzer %q not registered", arg) + } +} diff --git a/go/analysis/passes/findcall/findcall.go b/go/analysis/passes/findcall/findcall.go index 28c4c62e..ce8a81dc 100644 --- a/go/analysis/passes/findcall/findcall.go +++ b/go/analysis/passes/findcall/findcall.go @@ -10,13 +10,16 @@ import ( ) var Analyzer = &analysis.Analyzer{ - Name: "findcall", - Doc: "find calls to a particular function", + Name: "findcall", + Doc: `find calls to a particular function + +The findcall analysis reports calls to functions or methods +of a particular name.`, Run: findcall, RunDespiteErrors: true, } -var name = "println" // --name flag +var name = "println" // -name flag func init() { Analyzer.Flags.StringVar(&name, "name", name, "name of the function to find") diff --git a/go/analysis/singlechecker/singlechecker.go b/go/analysis/singlechecker/singlechecker.go index d740b496..c66fb656 100644 --- a/go/analysis/singlechecker/singlechecker.go +++ b/go/analysis/singlechecker/singlechecker.go @@ -21,7 +21,10 @@ package singlechecker import ( "flag" + "fmt" "log" + "os" + "strings" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/internal/checker" @@ -42,9 +45,26 @@ func Main(a *analysis.Analyzer) { flag.Var(f.Value, f.Name, f.Usage) }) + flag.Usage = func() { + paras := strings.Split(a.Doc, "\n\n") + fmt.Fprintf(os.Stderr, "%s: %s\n\n", a.Name, paras[0]) + fmt.Printf("Usage: %s [-flag] [package]\n\n", a.Name) + if len(paras) > 1 { + fmt.Println(strings.Join(paras[1:], "\n\n")) + } + fmt.Println("\nFlags:") + flag.PrintDefaults() + } flag.Parse() - if err := checker.Run(flag.Args(), []*analysis.Analyzer{a}); err != nil { + args := flag.Args() + + if len(args) == 0 { + flag.Usage() + os.Exit(1) + } + + if err := checker.Run(args, []*analysis.Analyzer{a}); err != nil { log.Fatal(err) } }