diff options
| author | Brad Fitzpatrick <bradfitz@tailscale.com> | 2026-04-22 21:08:16 +0000 |
|---|---|---|
| committer | Brad Fitzpatrick <brad@danga.com> | 2026-04-22 15:13:09 -0700 |
| commit | a7d8aeb8aebc4bb01066eb6ffa69b9d8fe178b81 (patch) | |
| tree | ee56dded693e6db6115a816b136ed5ee47842ed1 /tempfork/pkgdoc/pkgdoc.go | |
| parent | 311dd3839da3b41c515eec87d3dc040861e6dea6 (diff) | |
| download | tailscale-a7d8aeb8aebc4bb01066eb6ffa69b9d8fe178b81.tar.xz tailscale-a7d8aeb8aebc4bb01066eb6ffa69b9d8fe178b81.zip | |
misc/genreadme,tempfork/pkgdoc,tsnet: generate README.md files from godocfserb/main
Adds a CI check to keep opted-in directories' README.md files in sync
with their package godoc. For now tsnet (and its sub-packages under
tsnet/example) is the only opted-in tree. The list of directories
lives in misc/genreadme/genreadme.go as defaultRoots, so CI and humans
both just run `./tool/go run ./misc/genreadme` with no arguments.
The check piggybacks on the existing go_generate job in test.yml and
fails if any README.md is out of date, pointing the user at the same
command.
Along the way:
- tempfork/pkgdoc now emits Markdown instead of plain text: headings
become level-2 with no {#hdr-...} anchors, and [Symbol] doc links
resolve to pkg.go.dev URLs, including for symbols in the current
package (which the default Printer would otherwise emit as bare
#Name fragments with no backing anchor in a README). Parsing no
longer uses parser.ImportsOnly, so doc.Package knows the package's
symbols and can resolve [Symbol] links at all.
- genreadme also emits a pkg.go.dev Go Reference badge at the top of
a library package's README; suppressed for package main.
- tsnet/tsnet.go's package godoc is expanded in idiomatic godoc
syntax — [Type], [Type.Method], reference-style [link]: URL
definitions — rather than Markdown-flavored [text](url) or
backtick-quoted identifiers, so that both pkg.go.dev and the
generated README.md render cleanly from a single source.
Fixes #19431
Fixes #19483
Fixes #19470
Change-Id: I8ca37e9e7b3bd446b8bfa7a91ac548f142688cb1
Co-authored-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Signed-off-by: Walter Poupore <walterp@tailscale.com>
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Diffstat (limited to 'tempfork/pkgdoc/pkgdoc.go')
| -rw-r--r-- | tempfork/pkgdoc/pkgdoc.go | 46 |
1 files changed, 41 insertions, 5 deletions
diff --git a/tempfork/pkgdoc/pkgdoc.go b/tempfork/pkgdoc/pkgdoc.go index 1868b028e..cab38dd48 100644 --- a/tempfork/pkgdoc/pkgdoc.go +++ b/tempfork/pkgdoc/pkgdoc.go @@ -13,6 +13,7 @@ import ( "go/ast" "go/build" "go/doc" + "go/doc/comment" "go/parser" "go/token" "io" @@ -46,6 +47,31 @@ func (pkg *Package) ToText(w io.Writer, text, prefix, codePrefix string) { w.Write(pr.Text(d)) } +// ToMarkdown parses the godoc comment text and writes a Markdown rendering to w +// suitable for a repository README.md: top-level sections become ## headings +// without per-heading anchor IDs, and [Symbol] doc links resolve to pkg.go.dev, +// including for symbols in the current package (which the default printer would +// otherwise emit as bare #Name fragments with no backing anchor). +func (pkg *Package) ToMarkdown(w io.Writer, text string) { + d := pkg.doc.Parser().Parse(text) + pr := pkg.doc.Printer() + pr.HeadingLevel = 2 + pr.HeadingID = func(*comment.Heading) string { return "" } + pr.DocLinkBaseURL = "https://pkg.go.dev" + pr.DocLinkURL = func(link *comment.DocLink) string { + importPath := link.ImportPath + if importPath == "" { + importPath = pkg.doc.ImportPath + } + name := link.Name + if link.Recv != "" { + name = link.Recv + "." + name + } + return "https://pkg.go.dev/" + importPath + "#" + name + } + w.Write(pr.Markdown(d)) +} + // pkgBuffer is a wrapper for bytes.Buffer that prints a package clause the // first time Write is called. type pkgBuffer struct { @@ -85,7 +111,10 @@ func parsePackage(writer io.Writer, pkg *build.Package, userPath string) *Packag return slices.Contains(pkg.GoFiles, info.Name()) || slices.Contains(pkg.CgoFiles, info.Name()) } fset := token.NewFileSet() - pkgs, err := parser.ParseDir(fset, pkg.Dir, include, parser.ParseComments|parser.ImportsOnly) + // Parse declarations (not just imports) so that doc.Package knows the + // package's symbols; the Markdown printer needs this to resolve + // [Symbol] doc links in package comments. + pkgs, err := parser.ParseDir(fset, pkg.Dir, include, parser.ParseComments) if err != nil { log.Fatal(err) } @@ -144,10 +173,10 @@ func (pkg *Package) newlines(n int) { } } -// packageDoc prints the docs for the package. +// packageDoc prints the docs for the package as Markdown. func (pkg *Package) packageDoc() { pkg.Printf("") // Trigger the package clause; we know the package exists. - pkg.ToText(&pkg.buf, pkg.doc.Doc, "", indent) + pkg.ToMarkdown(&pkg.buf, pkg.doc.Doc) pkg.newlines(1) pkg.bugs() @@ -175,8 +204,12 @@ func (pkg *Package) bugs() { } } -// PackageDoc generates documentation for a package in the given directory. -func PackageDoc(dir string) ([]byte, error) { +// PackageDoc generates Markdown documentation for the package in the given +// directory. importPath is the full Go import path of that package (e.g. +// "tailscale.com/tsnet"); it's used to render [Symbol] doc links to the +// right pkg.go.dev URL. If importPath is empty, build.ImportDir's guess +// is used (typically "." for module-based repos). +func PackageDoc(dir, importPath string) ([]byte, error) { var buf bytes.Buffer var writer io.Writer = &buf @@ -188,6 +221,9 @@ func PackageDoc(dir string) ([]byte, error) { } return nil, err } + if importPath != "" { + buildPackage.ImportPath = importPath + } userPath := dir pkg := parsePackage(writer, buildPackage, userPath) |
