Document every exported symbol and how callers use the library.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+278
@@ -0,0 +1,278 @@
|
||||
package gonexapp_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/doc"
|
||||
"go/doc/comment"
|
||||
"go/parser"
|
||||
"go/printer"
|
||||
"go/token"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const apiModulePath = "gitea.neitzel.de/konrad/go-nc-exapp"
|
||||
|
||||
func TestAPIDoc(t *testing.T) {
|
||||
got, err := renderAPIDoc(".", apiModulePath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
const path = "docs/api.md"
|
||||
if os.Getenv("UPDATE_API_DOCS") == "1" {
|
||||
if err := os.MkdirAll("docs", 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(got), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
want, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read %s: %v", path, err)
|
||||
}
|
||||
if string(want) != got {
|
||||
t.Fatalf("docs/api.md is stale; regenerate with UPDATE_API_DOCS=1 go test -run TestAPIDoc -count=1\n%s", firstDiff(string(want), got))
|
||||
}
|
||||
}
|
||||
|
||||
func renderAPIDoc(dir, modulePath string) (string, error) {
|
||||
fset := token.NewFileSet()
|
||||
pkgs, err := parser.ParseDir(fset, dir, func(fi os.FileInfo) bool {
|
||||
return !strings.HasSuffix(fi.Name(), "_test.go")
|
||||
}, parser.ParseComments)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(pkgs) != 1 {
|
||||
return "", fmt.Errorf("expected 1 package in %s, found %d", dir, len(pkgs))
|
||||
}
|
||||
var astPkg *ast.Package
|
||||
for _, p := range pkgs {
|
||||
astPkg = p
|
||||
}
|
||||
pkg := doc.New(astPkg, modulePath, 0)
|
||||
lookup := symLookup(pkg)
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString("# API\n\n")
|
||||
b.WriteString("Generated from the package comment and every exported declaration. Do not edit.\n")
|
||||
b.WriteString("Regenerate with `UPDATE_API_DOCS=1 go test -run TestAPIDoc -count=1`.\n\n")
|
||||
fmt.Fprintf(&b, "## Package %s\n\n", pkg.Name)
|
||||
writeDoc(&b, pkg.Doc, lookup)
|
||||
writeValues(&b, fset, "Constants", pkg.Consts, "### ", lookup)
|
||||
writeValues(&b, fset, "Variables", pkg.Vars, "### ", lookup)
|
||||
writeFuncs(&b, fset, "Functions", pkg.Funcs, "### ", lookup)
|
||||
if len(pkg.Types) > 0 {
|
||||
b.WriteString("## Types\n\n")
|
||||
for _, typ := range pkg.Types {
|
||||
writeType(&b, fset, typ, lookup)
|
||||
}
|
||||
}
|
||||
return b.String(), nil
|
||||
}
|
||||
|
||||
func writeType(b *strings.Builder, fset *token.FileSet, typ *doc.Type, lookup func(string, string) bool) {
|
||||
fmt.Fprintf(b, "### %s\n\n", typ.Name)
|
||||
sig, err := formatGenDecl(fset, typ.Decl)
|
||||
if err != nil {
|
||||
fmt.Fprintf(b, "_(signature unavailable: %v)_\n\n", err)
|
||||
} else {
|
||||
writeCode(b, sig)
|
||||
}
|
||||
writeDoc(b, typ.Doc, lookup)
|
||||
writeValues(b, fset, "", typ.Consts, "#### ", lookup)
|
||||
writeValues(b, fset, "", typ.Vars, "#### ", lookup)
|
||||
writeFuncs(b, fset, "", typ.Funcs, "#### ", lookup)
|
||||
if !isInterface(typ) {
|
||||
writeFuncs(b, fset, "", typ.Methods, "#### ", lookup)
|
||||
}
|
||||
}
|
||||
|
||||
func writeValues(b *strings.Builder, fset *token.FileSet, title string, vals []*doc.Value, heading string, lookup func(string, string) bool) {
|
||||
if len(vals) == 0 {
|
||||
return
|
||||
}
|
||||
if title != "" {
|
||||
fmt.Fprintf(b, "## %s\n\n", title)
|
||||
}
|
||||
for _, v := range vals {
|
||||
for _, name := range v.Names {
|
||||
fmt.Fprintf(b, "%s%s\n\n", heading, name)
|
||||
}
|
||||
sig, err := formatGenDecl(fset, v.Decl)
|
||||
if err != nil {
|
||||
fmt.Fprintf(b, "_(signature unavailable: %v)_\n\n", err)
|
||||
} else {
|
||||
writeCode(b, sig)
|
||||
}
|
||||
writeDoc(b, v.Doc, lookup)
|
||||
}
|
||||
}
|
||||
|
||||
func writeFuncs(b *strings.Builder, fset *token.FileSet, title string, fns []*doc.Func, heading string, lookup func(string, string) bool) {
|
||||
if len(fns) == 0 {
|
||||
return
|
||||
}
|
||||
if title != "" {
|
||||
fmt.Fprintf(b, "## %s\n\n", title)
|
||||
}
|
||||
for _, fn := range fns {
|
||||
name := fn.Name
|
||||
if fn.Recv != "" {
|
||||
name = strings.TrimPrefix(fn.Recv, "*") + "." + fn.Name
|
||||
}
|
||||
fmt.Fprintf(b, "%s%s\n\n", heading, name)
|
||||
sig, err := formatFunc(fset, fn.Decl)
|
||||
if err != nil {
|
||||
fmt.Fprintf(b, "_(signature unavailable: %v)_\n\n", err)
|
||||
} else {
|
||||
writeCode(b, sig)
|
||||
}
|
||||
writeDoc(b, fn.Doc, lookup)
|
||||
}
|
||||
}
|
||||
|
||||
func writeCode(b *strings.Builder, sig string) {
|
||||
b.WriteString("```go\n")
|
||||
b.WriteString(sig)
|
||||
b.WriteString("\n```\n\n")
|
||||
}
|
||||
|
||||
func writeDoc(b *strings.Builder, raw string, lookup func(string, string) bool) {
|
||||
text := renderComment(raw, lookup)
|
||||
if text == "" {
|
||||
return
|
||||
}
|
||||
b.WriteString(text)
|
||||
if !strings.HasSuffix(text, "\n") {
|
||||
b.WriteByte('\n')
|
||||
}
|
||||
b.WriteByte('\n')
|
||||
}
|
||||
|
||||
func symLookup(pkg *doc.Package) func(string, string) bool {
|
||||
syms := map[string]bool{}
|
||||
methods := map[string]bool{}
|
||||
addNames := func(vals []*doc.Value) {
|
||||
for _, v := range vals {
|
||||
for _, name := range v.Names {
|
||||
syms[name] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
addFuncs := func(fns []*doc.Func) {
|
||||
for _, fn := range fns {
|
||||
syms[fn.Name] = true
|
||||
if fn.Recv != "" {
|
||||
recv := strings.TrimPrefix(fn.Recv, "*")
|
||||
methods[recv+"."+fn.Name] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
addNames(pkg.Consts)
|
||||
addNames(pkg.Vars)
|
||||
addFuncs(pkg.Funcs)
|
||||
for _, typ := range pkg.Types {
|
||||
syms[typ.Name] = true
|
||||
addNames(typ.Consts)
|
||||
addNames(typ.Vars)
|
||||
addFuncs(typ.Funcs)
|
||||
addFuncs(typ.Methods)
|
||||
}
|
||||
return func(recv, name string) bool {
|
||||
if recv == "" {
|
||||
return syms[name]
|
||||
}
|
||||
return methods[recv+"."+name]
|
||||
}
|
||||
}
|
||||
|
||||
func renderComment(raw string, lookup func(string, string) bool) string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return ""
|
||||
}
|
||||
var parser comment.Parser
|
||||
parser.LookupSym = lookup
|
||||
var printer comment.Printer
|
||||
printer.HeadingLevel = 4
|
||||
printer.DocLinkURL = func(link *comment.DocLink) string {
|
||||
if link.ImportPath != "" {
|
||||
return link.DefaultURL("https://pkg.go.dev")
|
||||
}
|
||||
name := link.Name
|
||||
if link.Recv != "" {
|
||||
name = link.Recv + "." + link.Name
|
||||
}
|
||||
return "#" + githubAnchor(name)
|
||||
}
|
||||
return strings.TrimSpace(string(printer.Markdown(parser.Parse(raw))))
|
||||
}
|
||||
|
||||
// githubAnchor matches GitHub heading slugs: lowercase, drop punctuation, keep
|
||||
// letters, digits, hyphens, and underscores. "Client.Chat" becomes "clientchat".
|
||||
func githubAnchor(name string) string {
|
||||
var b strings.Builder
|
||||
for _, r := range strings.ToLower(name) {
|
||||
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' || r == '_' {
|
||||
b.WriteRune(r)
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func formatGenDecl(fset *token.FileSet, decl *ast.GenDecl) (string, error) {
|
||||
if decl == nil {
|
||||
return "", fmt.Errorf("nil decl")
|
||||
}
|
||||
copyDecl := *decl
|
||||
copyDecl.Doc = nil
|
||||
return formatNode(fset, ©Decl)
|
||||
}
|
||||
|
||||
func formatFunc(fset *token.FileSet, decl *ast.FuncDecl) (string, error) {
|
||||
if decl == nil {
|
||||
return "", fmt.Errorf("nil decl")
|
||||
}
|
||||
copyDecl := *decl
|
||||
copyDecl.Doc = nil
|
||||
copyDecl.Body = nil
|
||||
return formatNode(fset, ©Decl)
|
||||
}
|
||||
|
||||
func formatNode(fset *token.FileSet, node ast.Node) (string, error) {
|
||||
var buf bytes.Buffer
|
||||
if err := printer.Fprint(&buf, fset, node); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(buf.String()), nil
|
||||
}
|
||||
|
||||
func isInterface(typ *doc.Type) bool {
|
||||
if typ.Decl == nil || len(typ.Decl.Specs) != 1 {
|
||||
return false
|
||||
}
|
||||
spec, ok := typ.Decl.Specs[0].(*ast.TypeSpec)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
_, ok = spec.Type.(*ast.InterfaceType)
|
||||
return ok
|
||||
}
|
||||
|
||||
func firstDiff(want, got string) string {
|
||||
w := strings.Split(want, "\n")
|
||||
g := strings.Split(got, "\n")
|
||||
n := min(len(g), len(w))
|
||||
for i := 0; i < n; i++ {
|
||||
if w[i] != g[i] {
|
||||
return fmt.Sprintf("line %d:\n got: %s\nwant: %s", i+1, g[i], w[i])
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("length got %d lines, want %d lines", len(g), len(w))
|
||||
}
|
||||
Reference in New Issue
Block a user