Documentation
¶
Overview ¶
Package docscheck checks that the command examples in a tool's documentation name commands the tool can actually run.
Documentation drifts from a CLI silently. Split a command into a group and every page using the old form starts instructing readers to run something that errors — and nothing notices, because no test compares the two.
What it flags, and what it does not ¶
Only one shape is a finding: inside a fenced code block, a command group followed by a concrete token that is not one of its subcommands.
krites reset IMG_2043.CR2 // finding — `reset` is a group, `IMG_2043.CR2` is not a subcommand krites reset frame IMG.CR2 // fine — resolves to a leaf krites reset // fine — a bare group is a synopsis, not an instruction krites xmp <subcommand> // fine — a placeholder is a synopsis krites config --help // fine — a flag is not a subcommand
That narrowness is deliberate, and it is what makes the check usable. The obvious rule — flag any invocation that resolves to a node with children — reports fifteen findings on a corpus whose real defect count is three, because a group's own reference page names the group in its title, its heading and its prose. Restricting to fenced blocks removes twelve of those; ignoring bare groups and placeholders removes the rest.
It also removes what looks like the hard problem. Some groups run standalone and some error without a subcommand, so it seems the checker must tell them apart. It does not: a group handed a token that is not one of its subcommands fails either way. There is no allowlist of runnable parents to maintain, and no need to execute anything to find out — which matters, because a command's exit status depends on how its binary was built, while its shape does not.
Supplying the tree ¶
The tree comes from the caller. This module deliberately imports no CLI framework — TestDependencyFootprint enforces that, along with an allowlist holding its entire runtime graph — so a Cobra-based tool converts its own root command in a few lines:
func tree(c *cobra.Command) docscheck.Command {
n := docscheck.Command{Name: c.Name()}
for _, sub := range c.Commands() {
n.Sub = append(n.Sub, tree(sub))
}
return n
}
Taking the tree from the running program rather than from a list is the point: a hand-maintained list of command names is the same drift problem one level down.
Example ¶
A tool checks its own documentation in a test. The tree comes from the running program — in a Cobra tool, converted from the root command — so it cannot drift from what the binary actually accepts.
package main
import (
"fmt"
"testing/fstest"
"gitlab.com/phpboyscout/go/docscheck"
)
func main() {
root := docscheck.Command{
Name: "krites",
Sub: []docscheck.Command{
{Name: "cull"},
{Name: "reset", Sub: []docscheck.Command{{Name: "frame"}, {Name: "shoot"}}},
},
}
docs := fstest.MapFS{
"docs/cull.md": &fstest.MapFile{Data: []byte(
"Undo a frame:\n\n```bash\nkrites reset IMG_2043.CR2\n```\n",
)},
}
c, err := docscheck.New(root)
if err != nil {
panic(err)
}
rep, err := c.Walk(docs, "docs")
if err != nil {
panic(err)
}
fmt.Println(rep)
}
Output: docs/cull.md:4: `krites reset IMG_2043.CR2` — "IMG_2043.CR2" is not a subcommand of `krites reset` (have: frame, shoot) 1 file(s), 1 documented invocation(s), 1 problem(s)
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrNoTool is returned when the root command has no name. ErrNoTool = errors.NewSentinel("docscheck.no_tool", "root command has no name") // ErrNoCommands is returned when the root command has no subcommands, which // means the tree was never populated. ErrNoCommands = errors.NewSentinel("docscheck.no_commands", "root command has no subcommands") // ErrNoFiles is returned when a walk matched no files. ErrNoFiles = errors.NewSentinel("docscheck.no_files", "no files matched") )
Errors returned when a check cannot be performed. A checker that cannot see its source of truth must say so rather than report a clean run — a silent pass is how a guard like this stops working without anyone noticing.
Functions ¶
This section is empty.
Types ¶
type Checker ¶
type Checker struct {
// contains filtered or unexported fields
}
Checker scans documentation for invocations of one tool.
func New ¶
New returns a Checker for the given command tree. The root's Name is the tool name as a reader types it.
type Command ¶
Command is a node in a tool's command tree: a name and its subcommands. The root's Name is the tool's own name, as a reader would type it.
Callers build this from their CLI framework's own tree — see the package documentation. Nothing here imports a framework.
type Finding ¶
type Finding struct {
// File is the name the content was scanned under.
File string
// Line is the 1-indexed line the invocation appears on.
Line int
// Command is the command as resolved against the tree, e.g. "krites reset".
Command string
// Token is the token that broke it — present in the docs, absent from the tree.
Token string
// Options are the subcommands Command does accept, in sorted order.
Options []string
}
Finding is one documented invocation that cannot run as written.
type Report ¶
type Report struct {
// Findings are the invocations that cannot run, in file then line order.
Findings []Finding
// Files is how many files were scanned.
Files int
// Invocations is how many fenced invocations of the tool were resolved.
// A run that scans files but resolves nothing usually means the tool name
// is wrong, not that the documentation is clean.
Invocations int
}
Report is the outcome of walking a set of files.