carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package docsite
2
3import (
4 "go/ast"
5 "go/parser"
6 "go/token"
7 "sort"
8 "strconv"
9)
10
11// TopLevelCommands returns every subcommand main.go dispatches on.
12//
13// It reads the switch statement rather than the usage text because the
14// switch is the CLI and the usage text is prose about it. A gate
15// against the prose would prove only that the docs agree with a second
16// copy of the docs, which is exactly the drift worth catching.
17//
18// Flags are deliberately not scanned. rastrillo's equivalent gate does
19// scan them, but its CLI is one small package; CARLOS declares flags
20// across the whole root package and the list would run to hundreds,
21// most of them things no reader ever types.
22func TopLevelCommands(mainFile string) ([]string, error) {
23 fset := token.NewFileSet()
24 file, err := parser.ParseFile(fset, mainFile, nil, parser.SkipObjectResolution)
25 if err != nil {
26 return nil, err
27 }
28
29 seen := map[string]bool{}
30 var out []string
31 ast.Inspect(file, func(n ast.Node) bool {
32 sw, ok := n.(*ast.SwitchStmt)
33 if !ok {
34 return true
35 }
36 // Only the dispatch switch — the one on `cmd`. main.go has a
37 // single string switch today, but a second one added later
38 // would feed non-commands into the gate, and the obvious way
39 // to make that gate pass again is to document things that are
40 // not commands.
41 tag, ok := sw.Tag.(*ast.Ident)
42 if !ok || tag.Name != "cmd" {
43 return true
44 }
45 for _, stmt := range sw.Body.List {
46 clause, ok := stmt.(*ast.CaseClause)
47 if !ok {
48 continue
49 }
50 for _, expr := range clause.List {
51 s, ok := stringLit(expr)
52 if !ok || !isCommandWord(s) {
53 continue
54 }
55 if !seen[s] {
56 seen[s] = true
57 out = append(out, s)
58 }
59 }
60 }
61 return true
62 })
63 sort.Strings(out)
64 return out, nil
65}
66
67// isCommandWord filters case arms down to real commands: flag aliases
68// like "-h" are documented as part of the command they belong to, and a
69// case arm is only a command if it looks like one.
70func isCommandWord(s string) bool {
71 if s == "" {
72 return false
73 }
74 for _, r := range s {
75 if (r < 'a' || r > 'z') && r != '-' {
76 return false
77 }
78 }
79 return s[0] != '-'
80}
81
82func stringLit(e ast.Expr) (string, bool) {
83 lit, ok := e.(*ast.BasicLit)
84 if !ok || lit.Kind != token.STRING {
85 return "", false
86 }
87 s, err := strconv.Unquote(lit.Value)
88 if err != nil {
89 return "", false
90 }
91 return s, true
92}
93