doccheck

command
v0.2.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 3, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

Documentation

Overview

Command doccheck holds every exported symbol in the tree to a doc comment, and every godoc example to an output the test runner actually checks.

FR-66 and FR-68 both name CI as the thing that enforces them and, until this existed, nothing did. That is the same failure ci.sh's header describes: a requirement whose gate is a tool nobody runs is a requirement in name only. Both requirements are about drift rather than about a moment — a doc comment is written when a symbol lands and is forgotten when the next one does, and an example that nothing compiles stops describing the API the day after the API moves — so the check has to run on every change or it checks nothing.

The three rules, exactly as enforced

1. Every exported symbol carries a doc comment. Package-level types, funcs, methods, consts and vars; the exported fields of exported structs; and the methods of exported interfaces. A symbol is documented when it carries a comment of its own, when it carries a trailing line comment (fields, consts and vars only — `Foo int // the foo` is a doc comment as far as go doc is concerned), or when it is one name inside a parenthesized group whose declaration carries one. The group case is the local idiom and is better documentation than the alternative, not weaker: internal/protocol.Kind's eight iota values are described once, as a set, which is what they are.

2. Every package carries a package comment, and it opens with "Package <name>" or, for a command, "Command <name>". That is the convention go doc's synopsis depends on, and a package overview that does not name the package is not an overview.

3. A consumer-reachable package's overview is RUNNABLE, which is defined here as: the package declares a package-level `func Example()` carrying an `// Output:` comment. That is godoc's own package example — the block a reader sees directly under the overview — and the Output comment is what makes `go test` execute it and compare. An Example without one compiles and is never run, so it can assert a behaviour the library no longer has. Rule 4 holds every OTHER example to the same standard.

4. Every Example* function in the tree, in every module, has an `// Output:` comment. An empty one counts: `// Output:` with nothing after it tells go test to expect no output, and go test then runs the function. What does not count is no comment at all, which is FR-68's exact failure — documentation that compiles, never runs, and drifts silently.

Scope, and the argument for it

Every package in the tree is MEASURED, in every module. Rules 1 and 2 are ENFORCED on the packages of the published library — live, live/livetest and all of internal/**. Rule 3 is enforced on the consumer-reachable subset of those. Rule 4 is enforced everywhere, because an example that never runs costs the same wherever it sits.

The scope line was the module boundary, and it is now the same line drawn by path. FR-66 is a requirement about the library's documentation, and until the single-module fold the library was its own module: docs/guide/_samples, test/routers, test/sampling, test/memory, bench/apps/*/gotth and tools/ each had their own go.mod so that what they need could not reach a consumer's build list, and the go.mod walk below told them apart from the library with no path in it. They are all in one module now, and none of them became the library by being folded into it, so notLibrary names them. The same boundary decides what tools/apisurface calls surface.

The examples moved out of this tree altogether — they are at examples/gotth/, a sibling of pkg/ — and for one landing that put them outside -root and outside this gate entirely, which is how rule 4 stopped covering them: an example application whose Example function lost its Output comment would have compiled, never run, and passed.

They are walked again through -reported-root, which is repeatable and names a tree that is MEASURED with rule 4 enforced and rules 1 to 3 not. That is the scope the three examples had before they moved, arrived at the same way: each carried its own go.mod, the walk below answered "not the published module", and they landed in scopeReported. It is not a relaxation to fit them — it is the second reason above, which is at its strongest here. The dashboard example's wire types are JSON payload structs whose fields are capitalised because encoding/json will not marshal them otherwise, and a doc comment on WireUpdate.HTML is a sentence nobody will read.

The distinction is in the flag rather than in a path list because a root is not under the tree root the way bench/ and tools/ are; notLibraryTrees cannot name something that is not below it.

There is a second reason, specific to those modules and worth stating because it is the one that would make the wider rule actively bad. Most of their exported identifiers are exported by the COMPILER's demand rather than by an author's decision: a field is capitalised because encoding/json will not marshal it otherwise, a type because templ generates a call to it. When this gate was first run, against the tree at 452e1e74, 359 of the 410 undocumented symbols it found were struct fields, and the bulk of them were JSON payload fields in a benchmark fixture — that measurement is the argument's evidence and is dated on purpose, because the live figure is the one the run prints below, not the one a comment remembers. A doc comment on ChatEvent.Body is a sentence nobody will read, in a file nobody imports, and writing 180 of them is how a doc gate teaches a team that doc comments are noise.

So the out-of-scope packages are printed with their counts on every run, under the scope label "reported", rather than being dropped from the walk. The number is in the CI log, the decision is auditable, and widening the rule is a one-line change to enforcedScope rather than an archaeology exercise. Hiding them would be the defect ci.sh's ci_modules_unrun comment refuses in those words; printing them unenforced is the smallest thing that is not that.

Rule 3 narrows once more, to the packages of the published module with no internal element in their path — today live and live/livetest, derived rather than listed, so a third one is covered the day it appears. FR-66's overview clause says "exported package", and a package under internal/ is precisely the package that is not exported: no consumer can import it and godoc publishes no page for it, so the overview a package example sits under is a page nobody outside this module can reach.

Two exclusions are forced rather than chosen:

  • Generated files. A doc comment written into a file that carries "Code generated … DO NOT EDIT." is deleted by the next gen.sh run, and FR-7's byte-reproducibility gate fails on the attempt. The generator is where such a comment would have to come from, so a violation here is a finding against the generator, which is not this tool's subject.
  • Methods on unexported types. go doc drops them entirely, so a doc comment there is documentation with no reader. tools/apisurface makes the same call for the same reason — "a method on an unexported type is not reachable by a consumer, so it is not surface" — and the two gates agreeing on what the surface is matters more than either answer.

Output

The report lists every package with its scope, its symbol count and its examples, and every enforced violation with file:line, because a gate that reports the first failure makes the fix a sequence of CI rounds. Paths are relative to the module root.

Usage:

go run ./doccheck                             # report the coverage and check it
go run ./doccheck -report                     # report only, exit 0
go run ./doccheck -reported-root ../../x      # also walk a measured-only tree

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL