From 20bb6895bf60ae23d71f638994211fba2e00b0b4 Mon Sep 17 00:00:00 2001 From: Arpad Ryszka Date: Wed, 21 Jan 2026 22:00:31 +0100 Subject: [PATCH] utilize wand --- .gitignore | 2 +- Makefile | 51 +- cmd/docreflect/docs.gen.go | 55 ++ cmd/docreflect/main.go | 20 +- cmd/docreflect/readme.md | 90 +++ generate/generate.go | 651 ++++++++++++++++++++ generate/{lib_test.go => generate_test.go} | 0 generate/lib.go | 675 +-------------------- go.mod | 15 +- go.sum | 22 +- lib.go | 11 +- readme.md | 24 +- scripts/man.go | 18 + scripts/markdown.go | 14 + 14 files changed, 942 insertions(+), 706 deletions(-) create mode 100644 cmd/docreflect/docs.gen.go create mode 100644 cmd/docreflect/readme.md create mode 100644 generate/generate.go rename generate/{lib_test.go => generate_test.go} (100%) create mode 100644 scripts/man.go create mode 100644 scripts/markdown.go diff --git a/.gitignore b/.gitignore index 904be13..8fe15ea 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ -.bin +.build testdocs_test.go .cover diff --git a/Makefile b/Makefile index 0260f50..454d993 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,8 @@ -sources = $(shell find . -name "*.go" | grep -v testdocs_test.go) -prefix ?= ~/bin +sources = $(shell find . -name "*.go" | grep -v testdocs_test.go | grep -v cmd/docreflect/docs.gen.go) +PREFIX ?= /usr/local +prefix ?= $(PREFIX) +release_date = $(shell git show -s --format=%cs HEAD) +version = $(release_date)-$(shell git rev-parse --short HEAD) default: build @@ -9,16 +12,33 @@ libdocreflect: $(sources) libgenerate: $(sources) go build ./generate -.bin: - mkdir -p .bin +.build: + mkdir -p .build -.bin/docreflect: $(sources) .bin - go build -o .bin/docreflect ./cmd/docreflect +.build/docreflect: $(sources) .build cmd/docreflect/docs.gen.go + go build -o .build/docreflect -ldflags "-X main.version=$(version)" ./cmd/docreflect -build: libdocreflect libgenerate .bin/docreflect +.build/docreflect.1: $(sources) .build cmd/docreflect/docs.gen.go + go run scripts/man.go $(version) $(release_date) ./cmd/docreflect > .build/docreflect.1 -testdocs_test.go: $(sources) .bin/docreflect - .bin/docreflect generate docreflect_test code.squareroundforest.org/arpio/docreflect/internal/tests/src/testpackage > testdocs_test.go +cmd/docreflect/readme.md: $(sources) cmd/docreflect/docs.gen.go + go run scripts/markdown.go ./cmd/docreflect > cmd/docreflect/readme.md + +build: libdocreflect libgenerate .build/docreflect cmd/docreflect/readme.md + +testdocs_test.go: $(sources) .build + go run ./cmd/docreflect \ + docreflect_test \ + code.squareroundforest.org/arpio/docreflect/internal/tests/src/testpackage \ + > .build/testdocs_test.go \ + && mv .build/testdocs_test.go testdocs_test.go + +cmd/docreflect/docs.gen.go: $(sources) + go run ./cmd/docreflect \ + main \ + code.squareroundforest.org/arpio/docreflect/generate \ + > .build/docs.gen.go \ + && mv .build/docs.gen.go cmd/docreflect/docs.gen.go .cover: $(sources) testdocs_test.go go test -count 1 -coverprofile .cover . ./generate @@ -32,12 +52,17 @@ showcover: .cover go tool cover -html .cover fmt: $(sources) - go fmt . ./generate ./cmd/docreflect + go fmt . ./generate ./cmd/docreflect ./scripts -install: .bin/docreflect - cp .bin/docreflect $(prefix) +$(prefix)/bin/docreflect: .build/docreflect + cp .build/docreflect $(prefix)/bin/docreflect + +$(prefix)/share/man/man1/docreflect.1: .build/docreflect.1 + cp .build/docreflect.1 $(prefix)/share/man/man1/docreflect.1 + +install: $(prefix)/bin/docreflect $(prefix)/share/man/man1/docreflect.1 clean: go clean ./... - rm -rf .bin + rm -rf .build rm -f testdocs_test.go diff --git a/cmd/docreflect/docs.gen.go b/cmd/docreflect/docs.gen.go new file mode 100644 index 0000000..a0db7e8 --- /dev/null +++ b/cmd/docreflect/docs.gen.go @@ -0,0 +1,55 @@ +/* +Generated with https://code.squareroundforest.org/arpio/docreflect +*/ + +package main + +import "code.squareroundforest.org/arpio/docreflect" + +func init() { + docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate", "Package generate produces Go source code that registers documentation for use with the docreflect package.\n") + docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.GenerateRegistry", "GenerateRegistry generates a Go source file containing an init function that registers the documentation for\nthe declarations specified by their fully qualified Go path.\n\nThe paths argument accepts arbitrary packages, package-level symbols, or struct fields. Usage of package\npaths is recommended over specific symbols in most cases.\n\nLimitations:\n\n- Type references (such as type aliases or definitions based on named types) are not resolved.\n\n- Import paths are not followed.\n\nfunc(o, w, outputPackageName, path)") + docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.Options", "Options contains options for the generator.\n") + docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.Options.Main", "Main indicates that the docs for the symbols will be lookded up as part of the main package of an\nexecutable. This is necessary, because the fully qualified Go path of symbols in a main package differs\nfrom the path pointing to that package in a module.\n") + docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.cleanPaths", "\nfunc(gopath)") + docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.collectGoDirs", "\nfunc(o)") + docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.findFieldDocs", "\nfunc(str, fieldPath)") + docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.findGoMod", "\nfunc(dir)") + docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.fixDocPackage", "\nfunc(p)") + docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.format", "\nfunc(w, pname, docs)") + docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.funcDocs", "\nfunc(f)") + docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.funcParams", "\nfunc(f)") + docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.generate", "\nfunc(o, gopaths)") + docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.getGoroot", "\nfunc()") + docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.header", "") + docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.importPackages", "\nfunc(o, godirs, paths)") + docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.initOptions", "\nfunc()") + docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.merge", "\nfunc(m)") + docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.methodDocs", "\nfunc(importPath, t)") + docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.modCache", "\nfunc()") + docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.options", "") + docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.options.goroot", "") + docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.options.isMain", "") + docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.options.modules", "") + docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.options.wd", "") + docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.packageDocs", "\nfunc(pkg)") + docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.packageFuncDocs", "\nfunc(importPath, funcs)") + docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.packagePaths", "\nfunc(p)") + docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.parsePackages", "\nfunc(pkgs)") + docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.parserInclude", "\nfunc(pkg)") + docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.readGomod", "\nfunc(wd)") + docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.replacePath", "\nfunc(p, prefix, replace)") + docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.replacePaths", "\nfunc(m, prefix, replace)") + docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.set", "\nfunc(m, key, value)") + docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.splitGopath", "\nfunc(p)") + docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.structFieldDocs", "\nfunc(t, fieldPath)") + docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.structFieldsDocs", "\nfunc(importPath, t)") + docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.symbolDocs", "\nfunc(pkg, gopath)") + docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.symbolPath", "\nfunc(packagePath, name)") + docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.takeDocs", "\nfunc(o, pkgs, gopaths)") + docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.takeFieldDocs", "\nfunc(packagePath, prefix, f)") + docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.typeDocs", "\nfunc(importPath, types)") + docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.typeMethodDocs", "\nfunc(t, name)") + docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.unpack", "\nfunc(e)") + docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.valueDocs", "\nfunc(packagePath, v)") +} diff --git a/cmd/docreflect/main.go b/cmd/docreflect/main.go index 5fe4e61..621d661 100644 --- a/cmd/docreflect/main.go +++ b/cmd/docreflect/main.go @@ -2,18 +2,16 @@ package main import ( "code.squareroundforest.org/arpio/docreflect/generate" - "log" - "os" + "code.squareroundforest.org/arpio/wand" ) -func main() { - args := os.Args[1:] - if args[0] == "generate" { - args = args[1:] - } +var version = "dev" - packageName, args := args[0], args[1:] - if err := generate.GenerateRegistry(generate.Options{}, os.Stdout, packageName, args...); err != nil { - log.Fatalln(err) - } +func main() { + cmd := wand.Command("generate-registry", generate.GenerateRegistry) + cmd = wand.Default(cmd) + cmd = wand.ShortForm(cmd, "m", "main") + cmd = wand.Group("docreflect", cmd) + cmd = wand.Version(cmd, version) + wand.Exec(cmd) } diff --git a/cmd/docreflect/readme.md b/cmd/docreflect/readme.md new file mode 100644 index 0000000..a5f93dd --- /dev/null +++ b/cmd/docreflect/readme.md @@ -0,0 +1,90 @@ +# docreflect + +## Synopsis: + +``` +docreflect [options]... [--] [path string]... +docreflect +``` + +## Description: + +generates a Go source file containing an init function that registers the documentation for the declarations +specified by their fully qualified Go path. + +The paths argument accepts arbitrary packages, package-level symbols, or struct fields. Usage of package paths +is recommended over specific symbols in most cases. + +Limitations: + +\- Type references (such as type aliases or definitions based on named types) are not resolved. + +\- Import paths are not followed. + +## Options: + +- --main, -m bool: indicates that the docs for the symbols will be lookded up as part of the main package of an + executable. This is necessary, because the fully qualified Go path of symbols in a main package differs from + the path pointing to that package in a module. +- --help: Show help. + +Hints: + +- Bool options can be used with implicit true values. +- Option values can be set both via = or just separated by space. + +## Subcommands: + +Show help for each subcommand by calling \ help or \ --help. + +### docreflect generate-registry (default) + +#### Synopsis: + +``` +docreflect generate-registry [options]... [--] [path string]... +docreflect generate-registry +``` + +#### Description: + +generates a Go source file containing an init function that registers the documentation for the declarations +specified by their fully qualified Go path. + +The paths argument accepts arbitrary packages, package-level symbols, or struct fields. Usage of package paths +is recommended over specific symbols in most cases. + +Limitations: + +\- Type references (such as type aliases or definitions based on named types) are not resolved. + +\- Import paths are not followed. + +#### Options: + +- --main, -m bool: indicates that the docs for the symbols will be lookded up as part of the main package of an + executable. This is necessary, because the fully qualified Go path of symbols in a main package differs from + the path pointing to that package in a module. +- --help: Show help. + +### docreflect version + +Show version. + +## Environment variables: + +Every command line option's value can also be provided as an environment variable. Environment variable names +need to use snake casing like myapp\_foo\_bar\_baz or MYAPP\_FOO\_BAR\_BAZ, or other casing that doesn't include the +'-' dash character, and they need to be prefixed with the name of the application, as in the base name of the +command. + +When both the environment variable and the command line option is defined, the command line option overrides the +environment variable. Multiple values for the same environment variable can be defined by concatenating the +values with the ':' separator character. When overriding multiple values with command line options, all the +environment values of the same field are dropped. + +### Example environment variable: + +``` +DOCREFLECT_MAIN=true +``` diff --git a/generate/generate.go b/generate/generate.go new file mode 100644 index 0000000..cd6a89b --- /dev/null +++ b/generate/generate.go @@ -0,0 +1,651 @@ +package generate + +import ( + "fmt" + "go/ast" + "go/build" + "go/doc" + "go/parser" + "go/token" + "golang.org/x/mod/modfile" + "io" + "io/fs" + "os" + "path" + "runtime" + "slices" + "sort" + "strconv" + "strings" +) + +const header = `/* +Generated with https://code.squareroundforest.org/arpio/docreflect +*/ + +` + +type options struct { + wd string + goroot string + modules map[string]string + isMain bool +} + +func getGoroot() string { + gr := os.Getenv("GOROOT") + if gr != "" { + return gr + } + + return runtime.GOROOT() +} + +func modCache() string { + mc := os.Getenv("GOMODCACHE") + if mc != "" { + return mc + } + + gp := os.Getenv("GOPATH") + if gp == "" { + gp = path.Join(os.Getenv("HOME"), "go") + } + + mc = path.Join(gp, "pkg/mod") + return mc +} + +func findGoMod(dir string) (string, bool) { + p := path.Join(dir, "go.mod") + f, err := os.Stat(p) + if err == nil && !f.IsDir() { + return p, true + } + + if dir == "/" { + return "", false + } + + return findGoMod(path.Dir(dir)) +} + +func readGomod(wd string) map[string]string { + p, ok := findGoMod(wd) + if !ok { + return nil + } + + d, err := os.ReadFile(p) + if err != nil { + return nil + } + + m, err := modfile.Parse(p, d, nil) + if err != nil { + return nil + } + + var dirs map[string]string + mc := modCache() + for _, dep := range m.Require { + p := dep.Mod.String() + dirs = set(dirs, p, path.Join(mc, p)) + } + + name := m.Module.Mod.String() + dirs[name] = path.Dir(p) + return dirs +} + +func initOptions() options { + wd, _ := os.Getwd() + gr := getGoroot() + modules := readGomod(wd) + return options{ + wd: wd, + goroot: gr, + modules: modules, + } +} + +func cleanPaths(gopath []string) []string { + var c []string + for _, pi := range gopath { + pi = path.Clean(pi) + c = append(c, pi) + } + + return c +} + +func splitGopath(p string) (string, string) { + parts := strings.Split(p, "/") + last := len(parts) - 1 + parts, lastPart := parts[:last], parts[last] + symbolParts := strings.Split(lastPart, ".") + pkg := symbolParts[0] + return strings.Join(append(parts, pkg), "/"), strings.Join(symbolParts[1:], ".") +} + +func packagePaths(p []string) []string { + var ( + pp []string + m map[string]bool + ) + + for _, pi := range p { + ppi, _ := splitGopath(pi) + if m[ppi] { + continue + } + + pp = append(pp, ppi) + m = set(m, ppi, true) + } + + return pp +} + +func collectGoDirs(o options) []string { + var dirs []string + if o.goroot != "" { + dirs = append(dirs, path.Join(o.goroot, "src")) + dirs = append(dirs, path.Join(o.goroot, "src", "cmd")) + } + + return dirs +} + +func importPackages(o options, godirs, paths []string) ([]*build.Package, error) { + var pkgs []*build.Package + for _, p := range paths { + var found bool + for mod, modDir := range o.modules { + if !strings.HasPrefix(p, strings.Split(mod, "@")[0]) { + continue + } + + pkg, err := build.Import(p, modDir, build.ImportComment) + if err != nil || pkg == nil { + continue + } + + pkgs = append(pkgs, pkg) + found = true + break + } + + if found { + continue + } + + for _, d := range godirs { + pkg, err := build.Import(p, d, build.ImportComment) + if err != nil || pkg == nil { + continue + } + + pkgs = append(pkgs, pkg) + found = true + break + } + + if !found { + return nil, fmt.Errorf("failed to import package for %s", p) + } + } + + return pkgs, nil +} + +func parserInclude(pkg *build.Package) func(fs.FileInfo) bool { + return func(file fs.FileInfo) bool { + for _, fn := range pkg.GoFiles { + if fn == file.Name() { + return true + } + } + + return false + } +} + +func parsePackages(pkgs []*build.Package) (map[string][]*ast.Package, error) { + var ppkgs map[string][]*ast.Package + for _, p := range pkgs { + fset := token.NewFileSet() + pm, err := parser.ParseDir(fset, p.Dir, parserInclude(p), parser.ParseComments) + if err != nil { + return nil, fmt.Errorf("failed to parse package %s: %w", p.Name, err) + } + + for _, pp := range pm { + if pp == nil { + continue + } + + ppkgs = set(ppkgs, p.ImportPath, append(ppkgs[p.ImportPath], pp)) + } + } + + return ppkgs, nil +} + +func fixDocPackage(p doc.Package) doc.Package { + for _, t := range p.Types { + p.Consts = append(p.Consts, t.Consts...) + p.Vars = append(p.Vars, t.Vars...) + p.Funcs = append(p.Funcs, t.Funcs...) + } + + return p +} + +func set[K comparable, V any](m map[K]V, key K, value V) map[K]V { + if m == nil { + m = make(map[K]V) + } + + m[key] = value + return m +} + +func merge[K comparable, V any](m ...map[K]V) map[K]V { + if len(m) == 1 { + return m[0] + } + + var mm map[K]V + for key, value := range m[0] { + mm = set(mm, key, value) + } + + mr := merge(m[1:]...) + for key, value := range mr { + mm = set(mm, key, value) + } + + return mm +} + +func unpack(e ast.Expr) ast.Expr { + p, ok := e.(*ast.StarExpr) + if !ok { + return e + } + + return unpack(p.X) +} + +func funcParams(f *doc.Func) string { + if f.Decl == nil || f.Decl.Type == nil || f.Decl.Type.Params == nil { + return "func()" + } + + var paramNames []string + for _, p := range f.Decl.Type.Params.List { + for _, n := range p.Names { + if n == nil { + continue + } + + paramNames = append(paramNames, n.Name) + } + } + + return fmt.Sprintf("func(%s)", strings.Join(paramNames, ", ")) +} + +func funcDocs(f *doc.Func) string { + return fmt.Sprintf("%s\n%s", f.Doc, funcParams(f)) +} + +func findFieldDocs(str *ast.StructType, fieldPath []string) (string, bool) { + if str.Fields == nil { + return "", false + } + + for _, f := range str.Fields.List { + var found bool + for _, fn := range f.Names { + if fn != nil && fn.Name == fieldPath[0] { + found = true + break + } + } + + if !found { + continue + } + + if len(fieldPath) == 1 { + if f.Doc == nil { + return "", true + } + + return f.Doc.Text(), true + } + + te := unpack(f.Type) + fstr, ok := te.(*ast.StructType) + if !ok { + return "", false + } + + return findFieldDocs(fstr, fieldPath[1:]) + } + + return "", false +} + +func structFieldDocs(t *doc.Type, fieldPath []string) (string, bool) { + if t.Decl == nil || len(t.Decl.Specs) != 1 { + return "", false + } + + ts, ok := t.Decl.Specs[0].(*ast.TypeSpec) + if !ok { + return "", false + } + + te := unpack(ts.Type) + str, ok := te.(*ast.StructType) + if !ok { + return "", false + } + + return findFieldDocs(str, fieldPath) +} + +func typeMethodDocs(t *doc.Type, name string) (string, bool) { + for _, m := range t.Methods { + if m.Name != name { + continue + } + + return funcDocs(m), true + } + + return "", false +} + +func symbolDocs(pkg *doc.Package, gopath string) (string, bool) { + _, s := splitGopath(gopath) + symbol := strings.Split(s, ".") + if len(symbol) == 1 { + for _, c := range pkg.Consts { + if c == nil || !slices.Contains(c.Names, symbol[0]) { + continue + } + + return c.Doc, true + } + + for _, v := range pkg.Vars { + if v == nil || !slices.Contains(v.Names, symbol[0]) { + continue + } + + return v.Doc, true + } + + for _, f := range pkg.Funcs { + if f == nil || f.Name != symbol[0] { + continue + } + + return funcDocs(f), true + } + } + + for _, t := range pkg.Types { + if t == nil || t.Name != symbol[0] { + continue + } + + if len(symbol) == 1 { + return t.Doc, true + } + + if d, ok := structFieldDocs(t, symbol[1:]); ok { + return d, true + } + + if len(symbol) != 2 { + return "", false + } + + if d, ok := typeMethodDocs(t, symbol[1]); ok { + return d, ok + } + } + + return "", false +} + +func symbolPath(packagePath string, name ...string) string { + return fmt.Sprintf("%s.%s", packagePath, strings.Join(name, ".")) +} + +func valueDocs(packagePath string, v []*doc.Value) map[string]string { + var d map[string]string + for _, vi := range v { + if vi != nil { + for _, n := range vi.Names { + d = set(d, symbolPath(packagePath, n), vi.Doc) + } + } + } + + return d +} + +func takeFieldDocs(packagePath string, prefix []string, f *ast.Field) map[string]string { + var docs map[string]string + for _, fn := range f.Names { + if fn == nil { + continue + } + + docs = set(docs, symbolPath(packagePath, append(prefix, fn.Name)...), f.Doc.Text()) + te := unpack(f.Type) + fst, ok := te.(*ast.StructType) + if !ok || fst.Fields == nil { + continue + } + + for _, fi := range fst.Fields.List { + if fi == nil { + continue + } + + docs = merge(docs, takeFieldDocs(packagePath, append(prefix, fn.Name), fi)) + } + } + + return docs +} + +func structFieldsDocs(importPath string, t *doc.Type) map[string]string { + if t.Decl == nil || len(t.Decl.Specs) != 1 { + return nil + } + + ts, ok := t.Decl.Specs[0].(*ast.TypeSpec) + if !ok { + return nil + } + + te := unpack(ts.Type) + str, ok := te.(*ast.StructType) + if !ok || str.Fields == nil { + return nil + } + + var d map[string]string + for _, f := range str.Fields.List { + if f == nil { + continue + } + + d = merge(d, takeFieldDocs(importPath, []string{t.Name}, f)) + } + + return d +} + +func methodDocs(importPath string, t *doc.Type) map[string]string { + var d map[string]string + for _, m := range t.Methods { + if m != nil { + d = set(d, symbolPath(importPath, t.Name, m.Name), funcDocs(m)) + } + } + + return d +} + +func typeDocs(importPath string, types []*doc.Type) map[string]string { + var d map[string]string + for _, t := range types { + if t == nil { + continue + } + + d = set(d, symbolPath(importPath, t.Name), t.Doc) + d = merge(d, structFieldsDocs(importPath, t)) + d = merge(d, methodDocs(importPath, t)) + + } + + return d +} + +func packageFuncDocs(importPath string, funcs []*doc.Func) map[string]string { + var d map[string]string + for _, f := range funcs { + if f != nil { + d = set(d, symbolPath(importPath, f.Name), funcDocs(f)) + } + } + + return d +} + +func packageDocs(pkg *doc.Package) map[string]string { + return merge( + map[string]string{pkg.ImportPath: pkg.Doc}, + valueDocs(pkg.ImportPath, pkg.Consts), + valueDocs(pkg.ImportPath, pkg.Vars), + typeDocs(pkg.ImportPath, pkg.Types), + packageFuncDocs(pkg.ImportPath, pkg.Funcs), + ) +} + +func replacePath(p, prefix, replace string) string { + if !strings.HasPrefix(p, prefix) { + return p + } + + return fmt.Sprintf("%s%s", replace, p[len(prefix):]) +} + +func replacePaths(m map[string]string, prefix, replace string) map[string]string { + var mm map[string]string + for p, d := range m { + mm = set(mm, replacePath(p, prefix, replace), d) + } + + return mm +} + +func takeDocs(o options, pkgs map[string][]*ast.Package, gopaths []string) (map[string]string, error) { + var dm map[string]string + for _, gp := range gopaths { + pp, _ := splitGopath(gp) + isPackage := pp == gp + for _, pkg := range pkgs[pp] { + dpkg := doc.New(pkg, pp, doc.AllDecls|doc.PreserveAST) + *dpkg = fixDocPackage(*dpkg) + if isPackage { + pd := packageDocs(dpkg) + if o.isMain { + pd = replacePaths(pd, pp, "main") + } + + dm = merge(dm, pd) + continue + } + + sd, ok := symbolDocs(dpkg, gp) + if !ok { + return nil, fmt.Errorf("symbol not found: %s", gp) + } + + if o.isMain { + gp = replacePath(gp, pp, "main") + } + + dm = set(dm, gp, sd) + } + } + + return dm, nil +} + +func generate(o options, gopaths ...string) (map[string]string, error) { + gopaths = cleanPaths(gopaths) + ppaths := packagePaths(gopaths) + dirs := collectGoDirs(o) + pkgs, err := importPackages(o, dirs, ppaths) + if err != nil { + return nil, err + } + + ppkgs, err := parsePackages(pkgs) + if err != nil { + return nil, err + } + + return takeDocs(o, ppkgs, gopaths) +} + +func format(w io.Writer, pname string, docs map[string]string) error { + var err error + printf := func(f string, a ...any) { + if err != nil { + return + } + + _, err = fmt.Fprintf(w, f, a...) + } + + println := func(a ...any) { + if err != nil { + return + } + + _, err = fmt.Fprintln(w, a...) + } + + println(header) + printf("package %s\n", pname) + println("import \"code.squareroundforest.org/arpio/docreflect\"") + println("func init() {") + + var paths []string + for path := range docs { + paths = append(paths, path) + } + + sort.Strings(paths) + for _, path := range paths { + doc := docs[path] + path, doc = strconv.Quote(path), strconv.Quote(doc) + printf("docreflect.Register(%s, %s)\n", path, doc) + } + + printf("}") + return err +} diff --git a/generate/lib_test.go b/generate/generate_test.go similarity index 100% rename from generate/lib_test.go rename to generate/generate_test.go diff --git a/generate/lib.go b/generate/lib.go index 7f5919f..61a3d63 100644 --- a/generate/lib.go +++ b/generate/lib.go @@ -1,677 +1,32 @@ -// Package generate provides a generator to generate go code from go docs that registers doc entries -// for use with the docreflect package. +// Package generate produces Go source code that registers documentation for use with the docreflect package. package generate -import ( - "fmt" - "go/ast" - "go/build" - "go/doc" - "go/parser" - "go/token" - "golang.org/x/mod/modfile" - "io" - "io/fs" - "os" - "path" - "runtime" - "slices" - "sort" - "strconv" - "strings" -) - -const header = `/* -Generated with https://code.squareroundforest.org/arpio/docreflect -*/ - -` +import "io" // Options contains options for the generator. type Options struct { // Main indicates that the docs for the symbols will be lookded up as part of the main package of an - // executable. + // executable. This is necessary, because the fully qualified Go path of symbols in a main package differs + // from the path pointing to that package in a module. Main bool } -type options struct { - wd string - goroot string - modules map[string]string - isMain bool -} - -func getGoroot() string { - gr := os.Getenv("GOROOT") - if gr != "" { - return gr - } - - return runtime.GOROOT() -} - -func modCache() string { - mc := os.Getenv("GOMODCACHE") - if mc != "" { - return mc - } - - gp := os.Getenv("GOPATH") - if gp == "" { - gp = path.Join(os.Getenv("HOME"), "go") - } - - mc = path.Join(gp, "pkg/mod") - return mc -} - -func findGoMod(dir string) (string, bool) { - p := path.Join(dir, "go.mod") - f, err := os.Stat(p) - if err == nil && !f.IsDir() { - return p, true - } - - if dir == "/" { - return "", false - } - - return findGoMod(path.Dir(dir)) -} - -func readGomod(wd string) map[string]string { - p, ok := findGoMod(wd) - if !ok { - return nil - } - - d, err := os.ReadFile(p) - if err != nil { - return nil - } - - m, err := modfile.Parse(p, d, nil) - if err != nil { - return nil - } - - var dirs map[string]string - mc := modCache() - for _, dep := range m.Require { - p := dep.Mod.String() - dirs = set(dirs, p, path.Join(mc, p)) - } - - name := m.Module.Mod.String() - dirs[name] = path.Dir(p) - return dirs -} - -func initOptions() options { - wd, _ := os.Getwd() - gr := getGoroot() - modules := readGomod(wd) - return options{ - wd: wd, - goroot: gr, - modules: modules, - } -} - -func cleanPaths(gopath []string) []string { - var c []string - for _, pi := range gopath { - pi = path.Clean(pi) - c = append(c, pi) - } - - return c -} - -func splitGopath(p string) (string, string) { - parts := strings.Split(p, "/") - last := len(parts) - 1 - parts, lastPart := parts[:last], parts[last] - symbolParts := strings.Split(lastPart, ".") - pkg := symbolParts[0] - return strings.Join(append(parts, pkg), "/"), strings.Join(symbolParts[1:], ".") -} - -func packagePaths(p []string) []string { - var ( - pp []string - m map[string]bool - ) - - for _, pi := range p { - ppi, _ := splitGopath(pi) - if m[ppi] { - continue - } - - pp = append(pp, ppi) - m = set(m, ppi, true) - } - - return pp -} - -func collectGoDirs(o options) []string { - var dirs []string - if o.goroot != "" { - dirs = append(dirs, path.Join(o.goroot, "src")) - dirs = append(dirs, path.Join(o.goroot, "src", "cmd")) - } - - return dirs -} - -func importPackages(o options, godirs, paths []string) ([]*build.Package, error) { - var pkgs []*build.Package - for _, p := range paths { - var found bool - for mod, modDir := range o.modules { - if !strings.HasPrefix(p, strings.Split(mod, "@")[0]) { - continue - } - - pkg, err := build.Import(p, modDir, build.ImportComment) - if err != nil || pkg == nil { - continue - } - - pkgs = append(pkgs, pkg) - found = true - break - } - - if found { - continue - } - - for _, d := range godirs { - pkg, err := build.Import(p, d, build.ImportComment) - if err != nil || pkg == nil { - continue - } - - pkgs = append(pkgs, pkg) - found = true - break - } - - if !found { - return nil, fmt.Errorf("failed to import package for %s", p) - } - } - - return pkgs, nil -} - -func parserInclude(pkg *build.Package) func(fs.FileInfo) bool { - return func(file fs.FileInfo) bool { - for _, fn := range pkg.GoFiles { - if fn == file.Name() { - return true - } - } - - return false - } -} - -func parsePackages(pkgs []*build.Package) (map[string][]*ast.Package, error) { - var ppkgs map[string][]*ast.Package - for _, p := range pkgs { - fset := token.NewFileSet() - pm, err := parser.ParseDir(fset, p.Dir, parserInclude(p), parser.ParseComments) - if err != nil { - return nil, fmt.Errorf("failed to parse package %s: %w", p.Name, err) - } - - for _, pp := range pm { - if pp == nil { - continue - } - - ppkgs = set(ppkgs, p.ImportPath, append(ppkgs[p.ImportPath], pp)) - } - } - - return ppkgs, nil -} - -func fixDocPackage(p doc.Package) doc.Package { - for _, t := range p.Types { - p.Consts = append(p.Consts, t.Consts...) - p.Vars = append(p.Vars, t.Vars...) - p.Funcs = append(p.Funcs, t.Funcs...) - } - - return p -} - -func set[K comparable, V any](m map[K]V, key K, value V) map[K]V { - if m == nil { - m = make(map[K]V) - } - - m[key] = value - return m -} - -func merge[K comparable, V any](m ...map[K]V) map[K]V { - if len(m) == 1 { - return m[0] - } - - var mm map[K]V - for key, value := range m[0] { - mm = set(mm, key, value) - } - - mr := merge(m[1:]...) - for key, value := range mr { - mm = set(mm, key, value) - } - - return mm -} - -func unpack(e ast.Expr) ast.Expr { - p, ok := e.(*ast.StarExpr) - if !ok { - return e - } - - return unpack(p.X) -} - -func funcParams(f *doc.Func) string { - if f.Decl == nil || f.Decl.Type == nil || f.Decl.Type.Params == nil { - return "func()" - } - - var paramNames []string - for _, p := range f.Decl.Type.Params.List { - for _, n := range p.Names { - if n == nil { - continue - } - - paramNames = append(paramNames, n.Name) - } - } - - return fmt.Sprintf("func(%s)", strings.Join(paramNames, ", ")) -} - -func funcDocs(f *doc.Func) string { - return fmt.Sprintf("%s\n%s", f.Doc, funcParams(f)) -} - -func findFieldDocs(str *ast.StructType, fieldPath []string) (string, bool) { - if str.Fields == nil { - return "", false - } - - for _, f := range str.Fields.List { - var found bool - for _, fn := range f.Names { - if fn != nil && fn.Name == fieldPath[0] { - found = true - break - } - } - - if !found { - continue - } - - if len(fieldPath) == 1 { - if f.Doc == nil { - return "", true - } - - return f.Doc.Text(), true - } - - te := unpack(f.Type) - fstr, ok := te.(*ast.StructType) - if !ok { - return "", false - } - - return findFieldDocs(fstr, fieldPath[1:]) - } - - return "", false -} - -func structFieldDocs(t *doc.Type, fieldPath []string) (string, bool) { - if t.Decl == nil || len(t.Decl.Specs) != 1 { - return "", false - } - - ts, ok := t.Decl.Specs[0].(*ast.TypeSpec) - if !ok { - return "", false - } - - te := unpack(ts.Type) - str, ok := te.(*ast.StructType) - if !ok { - return "", false - } - - return findFieldDocs(str, fieldPath) -} - -func typeMethodDocs(t *doc.Type, name string) (string, bool) { - for _, m := range t.Methods { - if m.Name != name { - continue - } - - return funcDocs(m), true - } - - return "", false -} - -func symbolDocs(pkg *doc.Package, gopath string) (string, bool) { - _, s := splitGopath(gopath) - symbol := strings.Split(s, ".") - if len(symbol) == 1 { - for _, c := range pkg.Consts { - if c == nil || !slices.Contains(c.Names, symbol[0]) { - continue - } - - return c.Doc, true - } - - for _, v := range pkg.Vars { - if v == nil || !slices.Contains(v.Names, symbol[0]) { - continue - } - - return v.Doc, true - } - - for _, f := range pkg.Funcs { - if f == nil || f.Name != symbol[0] { - continue - } - - return funcDocs(f), true - } - } - - for _, t := range pkg.Types { - if t == nil || t.Name != symbol[0] { - continue - } - - if len(symbol) == 1 { - return t.Doc, true - } - - if d, ok := structFieldDocs(t, symbol[1:]); ok { - return d, true - } - - if len(symbol) != 2 { - return "", false - } - - if d, ok := typeMethodDocs(t, symbol[1]); ok { - return d, ok - } - } - - return "", false -} - -func symbolPath(packagePath string, name ...string) string { - return fmt.Sprintf("%s.%s", packagePath, strings.Join(name, ".")) -} - -func valueDocs(packagePath string, v []*doc.Value) map[string]string { - var d map[string]string - for _, vi := range v { - if vi != nil { - for _, n := range vi.Names { - d = set(d, symbolPath(packagePath, n), vi.Doc) - } - } - } - - return d -} - -func takeFieldDocs(packagePath string, prefix []string, f *ast.Field) map[string]string { - var docs map[string]string - for _, fn := range f.Names { - if fn == nil { - continue - } - - docs = set(docs, symbolPath(packagePath, append(prefix, fn.Name)...), f.Doc.Text()) - te := unpack(f.Type) - fst, ok := te.(*ast.StructType) - if !ok || fst.Fields == nil { - continue - } - - for _, fi := range fst.Fields.List { - if fi == nil { - continue - } - - docs = merge(docs, takeFieldDocs(packagePath, append(prefix, fn.Name), fi)) - } - } - - return docs -} - -func structFieldsDocs(importPath string, t *doc.Type) map[string]string { - if t.Decl == nil || len(t.Decl.Specs) != 1 { - return nil - } - - ts, ok := t.Decl.Specs[0].(*ast.TypeSpec) - if !ok { - return nil - } - - te := unpack(ts.Type) - str, ok := te.(*ast.StructType) - if !ok || str.Fields == nil { - return nil - } - - var d map[string]string - for _, f := range str.Fields.List { - if f == nil { - continue - } - - d = merge(d, takeFieldDocs(importPath, []string{t.Name}, f)) - } - - return d -} - -func methodDocs(importPath string, t *doc.Type) map[string]string { - var d map[string]string - for _, m := range t.Methods { - if m != nil { - d = set(d, symbolPath(importPath, t.Name, m.Name), funcDocs(m)) - } - } - - return d -} - -func typeDocs(importPath string, types []*doc.Type) map[string]string { - var d map[string]string - for _, t := range types { - if t == nil { - continue - } - - d = set(d, symbolPath(importPath, t.Name), t.Doc) - d = merge(d, structFieldsDocs(importPath, t)) - d = merge(d, methodDocs(importPath, t)) - - } - - return d -} - -func packageFuncDocs(importPath string, funcs []*doc.Func) map[string]string { - var d map[string]string - for _, f := range funcs { - if f != nil { - d = set(d, symbolPath(importPath, f.Name), funcDocs(f)) - } - } - - return d -} - -func packageDocs(pkg *doc.Package) map[string]string { - return merge( - map[string]string{pkg.ImportPath: pkg.Doc}, - valueDocs(pkg.ImportPath, pkg.Consts), - valueDocs(pkg.ImportPath, pkg.Vars), - typeDocs(pkg.ImportPath, pkg.Types), - packageFuncDocs(pkg.ImportPath, pkg.Funcs), - ) -} - -func replacePath(p, prefix, replace string) string { - if !strings.HasPrefix(p, prefix) { - return p - } - - return fmt.Sprintf("%s%s", replace, p[len(prefix):]) -} - -func replacePaths(m map[string]string, prefix, replace string) map[string]string { - var mm map[string]string - for p, d := range m { - mm = set(mm, replacePath(p, prefix, replace), d) - } - - return mm -} - -func takeDocs(o options, pkgs map[string][]*ast.Package, gopaths []string) (map[string]string, error) { - var dm map[string]string - for _, gp := range gopaths { - pp, _ := splitGopath(gp) - isPackage := pp == gp - for _, pkg := range pkgs[pp] { - dpkg := doc.New(pkg, pp, doc.AllDecls|doc.PreserveAST) - *dpkg = fixDocPackage(*dpkg) - if isPackage { - pd := packageDocs(dpkg) - if o.isMain { - pd = replacePaths(pd, pp, "main") - } - - dm = merge(dm, pd) - continue - } - - sd, ok := symbolDocs(dpkg, gp) - if !ok { - return nil, fmt.Errorf("symbol not found: %s", gp) - } - - if o.isMain { - gp = replacePath(gp, pp, "main") - } - - dm = set(dm, gp, sd) - } - } - - return dm, nil -} - -func generate(o options, gopaths ...string) (map[string]string, error) { - gopaths = cleanPaths(gopaths) - ppaths := packagePaths(gopaths) - dirs := collectGoDirs(o) - pkgs, err := importPackages(o, dirs, ppaths) - if err != nil { - return nil, err - } - - ppkgs, err := parsePackages(pkgs) - if err != nil { - return nil, err - } - - return takeDocs(o, ppkgs, gopaths) -} - -func format(w io.Writer, pname string, docs map[string]string) error { - var err error - printf := func(f string, a ...any) { - if err != nil { - return - } - - _, err = fmt.Fprintf(w, f, a...) - } - - println := func(a ...any) { - if err != nil { - return - } - - _, err = fmt.Fprintln(w, a...) - } - - println(header) - printf("package %s\n", pname) - println("import \"code.squareroundforest.org/arpio/docreflect\"") - println("func init() {") - - var paths []string - for path := range docs { - paths = append(paths, path) - } - - sort.Strings(paths) - for _, path := range paths { - doc := docs[path] - path, doc = strconv.Quote(path), strconv.Quote(doc) - printf("docreflect.Register(%s, %s)\n", path, doc) - } - - printf("}") - return err -} - -// GenerateRegistry generates a Go code file to the output, including a package init function that -// will register the documentation of the declarations specified by their gopath. +// GenerateRegistry generates a Go source file containing an init function that registers the documentation for +// the declarations specified by their fully qualified Go path. // -// The gopath argument accepts any number of package, package level symbol, or struct field paths. -// It is recommended to use package paths unless special circumstances. +// The paths argument accepts arbitrary packages, package-level symbols, or struct fields. Usage of package +// paths is recommended over specific symbols in most cases. // -// Some important gotchas to keep in mind, GenerateRegistry does not resolve type references like -// type aliases, or type definitions based on named types, and it doesn't follow import paths. -func GenerateRegistry(o Options, w io.Writer, outputPackageName string, gopath ...string) error { +// Limitations: +// +// - Type references (such as type aliases or definitions based on named types) are not resolved. +// +// - Import paths are not followed. +func GenerateRegistry(o Options, w io.Writer, outputPackageName string, path ...string) error { oo := initOptions() oo.isMain = o.Main - d, err := generate(oo, gopath...) + d, err := generate(oo, path...) if err != nil { return err } diff --git a/go.mod b/go.mod index f7b9c42..7ef79f6 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,19 @@ module code.squareroundforest.org/arpio/docreflect -go 1.24.3 +go 1.25.3 require ( - code.squareroundforest.org/arpio/notation v0.0.0-20241225183158-af3bd591a174 + code.squareroundforest.org/arpio/notation v0.0.0-20251101123932-5f5c05ee0239 + code.squareroundforest.org/arpio/wand v0.0.0-20260115221425-3b0fa9ff1ec4 golang.org/x/mod v0.27.0 ) -require github.com/aryszka/notation v0.0.0-20230129164653-172017dde5e4 // indirect +require ( + code.squareroundforest.org/arpio/bind v0.0.0-20251105181644-3443251be2d5 // indirect + code.squareroundforest.org/arpio/html v0.0.0-20251103020946-e262eca50ac9 // indirect + code.squareroundforest.org/arpio/textedit v0.0.0-20251207224821-c75c3965789f // indirect + code.squareroundforest.org/arpio/textfmt v0.0.0-20251207234108-fed32c8bbe18 // indirect + github.com/iancoleman/strcase v0.3.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/term v0.37.0 // indirect +) diff --git a/go.sum b/go.sum index 5aa4180..95f0a8e 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,20 @@ -code.squareroundforest.org/arpio/notation v0.0.0-20241225183158-af3bd591a174 h1:DKMSagVY3uyRhJ4ohiwQzNnR6CWdVKLkg97A8eQGxQU= -code.squareroundforest.org/arpio/notation v0.0.0-20241225183158-af3bd591a174/go.mod h1:ait4Fvg9o0+bq5hlxi9dAcPL5a+/sr33qsZPNpToMLY= -github.com/aryszka/notation v0.0.0-20230129164653-172017dde5e4 h1:JzqT9RArcw2sD4QPAyTss/sHaCZvCv+91DDJPZOrShw= -github.com/aryszka/notation v0.0.0-20230129164653-172017dde5e4/go.mod h1:myJFmFAZ/75y5xdA1jjpc4ItNJwdRqaL+TQhIvDU8Vk= +code.squareroundforest.org/arpio/bind v0.0.0-20251105181644-3443251be2d5 h1:SIgLIawD6Vv7rAvUobpVshLshdwFEJ0NOUrWpheS088= +code.squareroundforest.org/arpio/bind v0.0.0-20251105181644-3443251be2d5/go.mod h1:tTCmCwFABKNm3PO0Dclsp4zWhNQFTfg9+uSrgoarZFI= +code.squareroundforest.org/arpio/html v0.0.0-20251103020946-e262eca50ac9 h1:b7voJlwe0jKH568X+O7b/JTAUrHLTSKNSSL+hhV2Q/Q= +code.squareroundforest.org/arpio/html v0.0.0-20251103020946-e262eca50ac9/go.mod h1:hq+2CENEd4bVSZnOdq38FUFOJJnF3OTQRv78qMGkNlE= +code.squareroundforest.org/arpio/notation v0.0.0-20251101123932-5f5c05ee0239 h1:JvLVMuvF2laxXkIZbHC1/0xtKyKndAwIHbIIWkHqTzc= +code.squareroundforest.org/arpio/notation v0.0.0-20251101123932-5f5c05ee0239/go.mod h1:ait4Fvg9o0+bq5hlxi9dAcPL5a+/sr33qsZPNpToMLY= +code.squareroundforest.org/arpio/textedit v0.0.0-20251207224821-c75c3965789f h1:gomu8xTD953IkL3M528qVEuZ2z93C2I6Hr4vyIwE7kI= +code.squareroundforest.org/arpio/textedit v0.0.0-20251207224821-c75c3965789f/go.mod h1:nXdFdxdI69JrkIT97f+AEE4OgplmxbgNFZC5j7gsdqs= +code.squareroundforest.org/arpio/textfmt v0.0.0-20251207234108-fed32c8bbe18 h1:2aa62CYm9ld5SNoFxWzE2wUN0xjVWQ+xieoeFantdg4= +code.squareroundforest.org/arpio/textfmt v0.0.0-20251207234108-fed32c8bbe18/go.mod h1:+0G3gufMAP8SCEIrDT1D/DaVOSfjS8EwPTBs5vfxqQg= +code.squareroundforest.org/arpio/wand v0.0.0-20260115221425-3b0fa9ff1ec4 h1:+CjwwD1mWGHFpcIgP9F0uQ8JEepDBht7yojLum6n6QA= +code.squareroundforest.org/arpio/wand v0.0.0-20260115221425-3b0fa9ff1ec4/go.mod h1:fPxs3LeGPxRMWUIXgBcdszk3a8d1TRqSHSVs5VL28Rc= +github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= diff --git a/lib.go b/lib.go index 817bac1..ca327ae 100644 --- a/lib.go +++ b/lib.go @@ -1,8 +1,8 @@ // Package docreflect returns the Go documentation for packages, types and functions. // -// The Go documentation of packages is not available during runtime by default, so in order to use docreflect, the documentation -// for the selected packages and symbols needs to be generated during build time. To generate the documentation, see the -// docreflect/generate package or the docreflect generate command. +// The Go documentation of packages is not available during runtime by default, so in order to use docreflect, +// the documentation for the selected packages and symbols needs to be generated during build time. To generate +// the documentation, see the docreflect/generate package or the docreflect generate command. package docreflect import ( @@ -95,8 +95,9 @@ func functionParams(d string) []string { return pp } -// Register is used by the code generated by the docreflect/generate package or the docreflect generate command to register -// the selected documentation during startup. Register is not meant to be used directly by importing packages. +// Register is used by the code generated by the docreflect/generate package or the docreflect generate command +// to register the selected documentation during startup. Register is not meant to be used directly by importing +// packages. func Register(gopath, docs string) { registry[gopath] = docs } diff --git a/readme.md b/readme.md index facedf2..e9d3e46 100644 --- a/readme.md +++ b/readme.md @@ -2,20 +2,23 @@ Library and command to help accessing go doc comments during runtime. -Go doc comments are not accessible during runtime via reflection. To make them avaiable during runtime, we need to capture them -during build time. The docreflect command, or the docreflect/generate library package, fetches the go doc comments of the specified -declarations, and generates Go code that registers the docs for every declaration. Code that includes the generated initialization -code, will have the docs accessible via the top level docreflect package methods. +Go doc comments are not accessible during runtime via reflection. To make them avaiable during runtime, we need +to capture them during build time. The docreflect command, or the docreflect/generate library package, fetches +the go doc comments of the specified declarations, and generates Go code that registers the docs for every +declaration. Code that includes the generated initialization code, will have the docs accessible via the top +level docreflect package methods. **Declarations:** - the declarations must be absolute Go paths -- when passing in the import path of a package, all the top level symbols of the package, plus the struct fields and methods of the - top level types will be included +- when passing in the import path of a package, all the top level symbols of the package, plus the struct fields + and methods of the top level types will be included - the package documentation can be fetched using `docreflect.Docs("absolute/import/path/of/package")` -- when passing in the import path of only the selected symbols, the rest of the package level symbols will be ignroed +- when passing in the import path of only the selected symbols, the rest of the package level symbols will be + ignroed **Gotchas:** + - type aliases and type definitions based on named types are not resolved - package imports are not resolved, necessary packages need to be included in the generate arguments @@ -30,7 +33,10 @@ make install Usage of the docreflect command: ``` -docreflect generate --package-name mypackage coderepos.org/jdoe/mypackage coderepos.org/jdoe/otherpackage > docreflect.go +docreflect generate \ + --package-name mypackage \ + coderepos.org/jdoe/mypackage coderepos.org/jdoe/otherpackage \ + > docreflect.go ``` -*Made in Berlin, DE* \ No newline at end of file +*Made in Berlin, DE* diff --git a/scripts/man.go b/scripts/man.go new file mode 100644 index 0000000..b5ae784 --- /dev/null +++ b/scripts/man.go @@ -0,0 +1,18 @@ +package main + +import ( + "code.squareroundforest.org/arpio/wand/tools" + "log" + "os" +) + +func main() { + o := tools.ManOptions{ + Version: os.Args[1], + DateString: os.Args[2], + } + + if err := tools.Man(os.Stdout, o, os.Args[3]); err != nil { + log.Fatalln(err) + } +} diff --git a/scripts/markdown.go b/scripts/markdown.go new file mode 100644 index 0000000..8dbb27c --- /dev/null +++ b/scripts/markdown.go @@ -0,0 +1,14 @@ +package main + +import ( + "code.squareroundforest.org/arpio/wand/tools" + "log" + "os" +) + +func main() { + var o tools.MarkdownOptions + if err := tools.Markdown(os.Stdout, o, os.Args[1]); err != nil { + log.Fatalln(err) + } +}