package docsite import ( "go/ast" "go/parser" "go/token" "sort" "strconv" ) // TopLevelCommands returns every subcommand main.go dispatches on. // // It reads the switch statement rather than the usage text because the // switch is the CLI and the usage text is prose about it. A gate // against the prose would prove only that the docs agree with a second // copy of the docs, which is exactly the drift worth catching. // // Flags are deliberately not scanned. rastrillo's equivalent gate does // scan them, but its CLI is one small package; CARLOS declares flags // across the whole root package and the list would run to hundreds, // most of them things no reader ever types. func TopLevelCommands(mainFile string) ([]string, error) { fset := token.NewFileSet() file, err := parser.ParseFile(fset, mainFile, nil, parser.SkipObjectResolution) if err != nil { return nil, err } seen := map[string]bool{} var out []string ast.Inspect(file, func(n ast.Node) bool { sw, ok := n.(*ast.SwitchStmt) if !ok { return true } // Only the dispatch switch — the one on `cmd`. main.go has a // single string switch today, but a second one added later // would feed non-commands into the gate, and the obvious way // to make that gate pass again is to document things that are // not commands. tag, ok := sw.Tag.(*ast.Ident) if !ok || tag.Name != "cmd" { return true } for _, stmt := range sw.Body.List { clause, ok := stmt.(*ast.CaseClause) if !ok { continue } for _, expr := range clause.List { s, ok := stringLit(expr) if !ok || !isCommandWord(s) { continue } if !seen[s] { seen[s] = true out = append(out, s) } } } return true }) sort.Strings(out) return out, nil } // isCommandWord filters case arms down to real commands: flag aliases // like "-h" are documented as part of the command they belong to, and a // case arm is only a command if it looks like one. func isCommandWord(s string) bool { if s == "" { return false } for _, r := range s { if (r < 'a' || r > 'z') && r != '-' { return false } } return s[0] != '-' } func stringLit(e ast.Expr) (string, bool) { lit, ok := e.(*ast.BasicLit) if !ok || lit.Kind != token.STRING { return "", false } s, err := strconv.Unquote(lit.Value) if err != nil { return "", false } return s, true }