init repo

This commit is contained in:
Arpad Ryszka 2025-08-08 21:44:31 +02:00
commit b130b3dcd8
13 changed files with 1338 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.bin
testdocs.go
.cover

41
Makefile Normal file
View File

@ -0,0 +1,41 @@
SOURCES = $(shell find . -name "*.go")
PREFIX ?= ~/bin
default: build
build: $(SOURCES) .bin
go build .
go build ./generate
go build -o .bin/docreflect ./cmd/docreflect
.bin/docreflect: build
.bin:
mkdir -p .bin
check: $(SOURCES) .bin/docreflect
.bin/docreflect generate docreflect_test code.squareroundforest.org/arpio/docreflect/tests/src/testpackage > testdocs_test.go
go test -count 1 . ./generate
rm -f testdocs_test.go
.cover: $(SOURCES) .bin/docreflect
.bin/docreflect generate docreflect_test code.squareroundforest.org/arpio/docreflect/tests/src/testpackage > testdocs_test.go
go test -count 1 -coverprofile .cover . ./generate
rm -f testdocs_test.go
cover: .cover
go tool cover -func .cover
showcover: .cover
go tool cover -html .cover
fmt: $(SOURCES)
go fmt . ./generate ./cmd/docreflect
install: .bin/docreflect
cp .bin/docreflect $(PREFIX)
clean:
go clean ./...
rm -rf .bin
rm -f testdocs_test.go

19
cmd/docreflect/main.go Normal file
View File

@ -0,0 +1,19 @@
package main
import (
"code.squareroundforest.org/arpio/docreflect/generate"
"log"
"os"
)
func main() {
args := os.Args[1:]
if args[0] == "generate" {
args = args[1:]
}
packageName, args := args[0], args[1:]
if err := generate.GenerateRegistry(os.Stdout, packageName, args...); err != nil {
log.Fatalln(err)
}
}

151
docs.go Normal file
View File

@ -0,0 +1,151 @@
// 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.
package docreflect
import (
"fmt"
"reflect"
"regexp"
"runtime"
"strings"
)
var (
registry = make(map[string]string)
funcParamsExp = regexp.MustCompile("^func[(]([a-zA-Z_][a-zA-Z0-9_]+(, [a-zA-Z_][a-zA-Z0-9_]+)*)?[)]$")
)
func docs(p string) string {
return registry[p]
}
func functionPath(v reflect.Value) string {
t := v.Type()
pp := t.PkgPath()
p := v.Pointer()
rf := runtime.FuncForPC(p)
n := rf.Name()
// method names appear to have an -fm suffix:
np := strings.Split(n, ".")
npl := np[len(np)-1]
nplp := strings.Split(npl, "-")
if len(nplp) > 1 {
npl = nplp[0]
np[len(np)-1] = npl
n = strings.Join(np, ".")
}
if strings.HasPrefix(n, pp) {
return n
}
// sometimes we get packagename.FunctionName instead of the full import path:
np = strings.Split(n, ".")
if len(np) > 1 {
np = np[1:]
}
return strings.Join(append([]string{pp}, np...), ".")
}
func splitDocs(d string) (string, string) {
parts := strings.Split(d, "\n")
last := len(parts) - 1
lastPart := parts[last]
var params string
if funcParamsExp.MatchString(lastPart) {
parts = parts[:last]
params = lastPart
}
return strings.Join(parts, "\n"), params
}
func functionParams(d string) []string {
_, p := splitDocs(d)
if p == "" {
return nil
}
p = p[len("func(") : len(p)-1]
return strings.Split(p, ", ")
}
// 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
}
// Docs returns the documentation for a package or a symbol in a package identified by its full go path.
func Docs(gopath string) string {
d := docs(gopath)
d, _ = splitDocs(d)
return d
}
// Function returns the documentation for a package level function.
func Function(v reflect.Value) string {
if v.Kind() != reflect.Func {
return ""
}
return Docs(functionPath(v))
}
// FunctionParams returns the list of the parameter names of a package level function.
func FunctionParams(v reflect.Value) []string {
if v.Kind() != reflect.Func {
return nil
}
d := docs(functionPath(v))
return functionParams(d)
}
// Type returns the docuemntation for a package level type.
func Type(t reflect.Type) string {
p := fmt.Sprintf("%s.%s", t.PkgPath(), t.Name())
return docs(p)
}
// Field returns the docuemntation for a struct field.
func Field(t reflect.Type, fieldPath ...string) string {
if len(fieldPath) == 0 {
return ""
}
if t.Kind() != reflect.Struct {
return ""
}
p := strings.Join(append([]string{t.PkgPath(), t.Name()}, fieldPath...), ".")
println(p)
return docs(p)
}
// Method returns the documentation for a type method.
func Method(t reflect.Type, name string) string {
if t.Kind() != reflect.Struct {
return ""
}
p := fmt.Sprintf("%s.%s.%s", t.PkgPath(), t.Name(), name)
return docs(p)
}
// MethodParams returns the list of the parameter names of a type method.
func MethodParams(t reflect.Type, name string) []string {
if t.Kind() != reflect.Struct {
return nil
}
p := fmt.Sprintf("%s.%s.%s", t.PkgPath(), t.Name(), name)
d := docs(p)
return functionParams(d)
}

116
docs_test.go Normal file
View File

@ -0,0 +1,116 @@
package docreflect_test
import (
"code.squareroundforest.org/arpio/docreflect"
"code.squareroundforest.org/arpio/docreflect/tests/src/testpackage"
"reflect"
"strings"
"testing"
)
func Test(t *testing.T) {
t.Run("unregistered", func(t *testing.T) {
d := docreflect.Docs("foo/bar/baz.qux")
if d != "" {
t.Fatal()
}
})
t.Run("package", func(t *testing.T) {
d := docreflect.Docs("code.squareroundforest.org/arpio/docreflect/tests/src/testpackage")
if !strings.Contains(d, "Package testpackage") {
t.Fatal()
}
})
t.Run("function by path", func(t *testing.T) {
d := docreflect.Docs("code.squareroundforest.org/arpio/docreflect/tests/src/testpackage.ExportedFunc")
if !strings.Contains(d, "ExportedFunc has documentation") {
t.Fatal()
}
if strings.Contains(d, "func(p1, p2)") {
t.Fatal()
}
})
t.Run("function by value", func(t *testing.T) {
d := docreflect.Function(reflect.ValueOf(testpackage.ExportedFunc))
if !strings.Contains(d, "ExportedFunc has documentation") {
t.Fatal(d)
}
if strings.Contains(d, "func(p1, p2)") {
t.Fatal()
}
})
t.Run("function params", func(t *testing.T) {
d := docreflect.FunctionParams(reflect.ValueOf(testpackage.ExportedFunc))
if len(d) != 2 {
t.Fatal(d)
}
if d[0] != "p1" {
t.Fatal(d)
}
if d[1] != "p2" {
t.Fatal(d)
}
})
t.Run("method as function value", func(t *testing.T) {
s := testpackage.ExportedType{}
m := s.Method
d := docreflect.Function(reflect.ValueOf(m))
if !strings.Contains(d, "Method is a method of ExportedType") {
t.Fatal(d)
}
p := docreflect.FunctionParams(reflect.ValueOf(m))
if len(p) != 3 {
t.Fatal()
}
if p[0] != "p1" || p[1] != "p2" || p[2] != "p3" {
t.Fatal()
}
})
t.Run("type", func(t *testing.T) {
s := testpackage.ExportedType{}
typ := reflect.TypeOf(s)
d := docreflect.Type(typ)
if !strings.Contains(d, "ExportedType has docs") {
t.Fatal()
}
})
t.Run("field", func(t *testing.T) {
s := testpackage.ExportedType{}
typ := reflect.TypeOf(s)
d := docreflect.Field(typ, "Foo")
if !strings.Contains(d, "Foo is a field") {
t.Fatal()
}
})
t.Run("method", func(t *testing.T) {
s := testpackage.ExportedType{}
typ := reflect.TypeOf(s)
d := docreflect.Method(typ, "Method")
if !strings.Contains(d, "Method is a method of ExportedType") {
t.Fatal()
}
p := docreflect.MethodParams(typ, "Method")
if len(p) != 3 {
t.Fatal()
}
if p[0] != "p1" || p[1] != "p2" || p[2] != "p3" {
t.Fatal()
}
})
}

536
generate/generate.go Normal file
View File

@ -0,0 +1,536 @@
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"
)
type options struct {
wd string
goroot string
gomod string
modules map[string]string
}
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) (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
}
mc := modCache()
dirs := make(map[string]string)
for _, dep := range m.Require {
pth := dep.Mod.String()
moduleDir := path.Join(mc, pth)
dirs[pth] = moduleDir
}
name := m.Module.Mod.String()
dirs[name] = path.Dir(p)
return name, dirs
}
func initOptions() options {
wd, _ := os.Getwd()
gr := getGoroot()
gomod, modules := readGomod(wd)
return options{
wd: wd,
goroot: gr,
gomod: gomod,
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 := make(map[string]bool)
for _, pi := range p {
ppi, _ := splitGopath(pi)
if m[ppi] {
continue
}
pp = append(pp, ppi)
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) {
ppkgs := make(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 {
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 merge(m ...map[string]string) map[string]string {
if len(m) == 1 {
return m[0]
}
mm := make(map[string]string)
for key, value := range m[0] {
mm[key] = value
}
mr := merge(m[1:]...)
for key, value := range mr {
mm[key] = value
}
return mm
}
func funcParams(f *doc.Func) string {
var paramNames []string
if f.Decl != nil && f.Decl.Type != nil && f.Decl.Type.Params != nil {
for _, p := range f.Decl.Type.Params.List {
for _, n := range p.Names {
if n != nil {
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 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]) {
return c.Doc, true
}
}
for _, v := range pkg.Vars {
if v != nil && slices.Contains(v.Names, symbol[0]) {
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 t.Decl == nil || len(t.Decl.Specs) != 1 {
continue
}
ts, ok := t.Decl.Specs[0].(*ast.TypeSpec)
if !ok {
continue
}
str, ok := ts.Type.(*ast.StructType)
if !ok {
continue
}
fieldPath := symbol[1:]
for len(fieldPath) > 0 {
var found bool
if str.Fields != nil {
for _, f := range str.Fields.List {
for _, fn := range f.Names {
if fn != nil {
if fn.Name != fieldPath[0] {
continue
}
if len(fieldPath) == 1 {
if f.Doc == nil {
return "", true
}
return f.Doc.Text(), true
}
fstr, ok := f.Type.(*ast.StructType)
if !ok {
break
}
found = true
str = fstr
fieldPath = fieldPath[1:]
break
}
}
}
}
if !found {
break
}
}
if len(symbol) != 2 {
return "", false
}
methodName := symbol[1]
for _, m := range t.Methods {
if m.Name != methodName {
continue
}
return funcDocs(m), true
}
}
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 {
d := make(map[string]string)
for _, vi := range v {
if vi != nil {
for _, n := range vi.Names {
d[symbolPath(packagePath, n)] = vi.Doc
}
}
}
return d
}
func packageDocs(pkg *doc.Package) map[string]string {
d := make(map[string]string)
d[pkg.ImportPath] = pkg.Doc
d = merge(d, valueDocs(pkg.ImportPath, pkg.Consts))
d = merge(d, valueDocs(pkg.ImportPath, pkg.Vars))
for _, t := range pkg.Types {
if t != nil {
d[symbolPath(pkg.ImportPath, t.Name)] = t.Doc
}
if t.Decl != nil && len(t.Decl.Specs) == 1 {
if ts, ok := t.Decl.Specs[0].(*ast.TypeSpec); ok {
if str, ok := ts.Type.(*ast.StructType); ok && str.Fields != nil {
for _, f := range str.Fields.List {
if f == nil || f.Doc == nil {
continue
}
for _, fn := range f.Names {
if fn == nil {
continue
}
d[symbolPath(pkg.ImportPath, t.Name, fn.Name)] = f.Doc.Text()
}
}
}
}
}
for _, m := range t.Methods {
if m != nil {
doc := funcDocs(m)
d[symbolPath(pkg.ImportPath, t.Name, m.Name)] = doc
}
}
}
for _, f := range pkg.Funcs {
if f != nil {
doc := funcDocs(f)
d[symbolPath(pkg.ImportPath, f.Name)] = doc
}
}
return d
}
func takeDocs(pkgs map[string][]*ast.Package, gopaths []string) (map[string]string, error) {
dm := make(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)
dm = merge(dm, pd)
continue
}
sd, ok := symbolDocs(dpkg, gp)
if !ok {
return nil, fmt.Errorf("symbol not found: %s", gp)
}
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(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...)
}
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)
}
println("}")
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.
func GenerateRegistry(w io.Writer, outputPackageName string, gopath ...string) error {
o := initOptions()
d, err := generate(o, gopath...)
if err != nil {
return err
}
if err := format(w, outputPackageName, d); err != nil {
return err
}
return nil
}

358
generate/generate_test.go Normal file
View File

@ -0,0 +1,358 @@
package generate
import (
"bytes"
"code.squareroundforest.org/arpio/notation"
"fmt"
"os"
"path"
"runtime"
"strings"
"testing"
)
func check(c ...string) map[string]string {
m := make(map[string]string)
for i := 0; i < len(c); i += 2 {
name := c[i]
var value string
if i < len(c)-1 {
value = c[i+1]
}
m[name] = value
}
return m
}
func testGenerate(check map[string]string, errstr string, o options, gopath ...string) func(t *testing.T) {
return func(t *testing.T) {
d, err := generate(o, gopath...)
if errstr != "" {
if err == nil {
t.Fatal("failed to fail")
}
if !strings.Contains(err.Error(), errstr) {
t.Fatal("unexpected error", err)
}
return
}
if err != nil {
t.Fatal(err)
}
var failed bool
for name, expect := range check {
dn, ok := d[name]
if !ok {
failed = true
break
}
if !strings.Contains(dn, expect) {
failed = true
break
}
}
if failed {
t.Log("failed to get the right documentation")
t.Log("expected matches:")
t.Log(notation.Sprint(check))
t.Log("documetation got:")
t.Log(notation.Sprint(d))
t.Fatal()
}
}
}
func TestGenerate(t *testing.T) {
wd := path.Clean(path.Join(os.Getenv("PWD"), ".."))
h := os.Getenv("HOME")
o := options{
wd: wd,
goroot: runtime.GOROOT(),
gomod: "code.squareroundforest.org/arpio/docreflect",
modules: map[string]string{
"golang.org/x/mod@v0.27.0": path.Join(h, "go", "pkg/mod", "golang.org/x/mod@v0.27.0"),
"code.squareroundforest.org/arpio/notation@v0.0.0-20241225183158-af3bd591a174": path.Join(
h, "go", "pkg/mod", "code.squareroundforest.org/arpio/notation@v0.0.0-20241225183158-af3bd591a174",
),
"code.squareroundforest.org/arpio/docreflect": wd,
},
}
t.Run("with modules", func(t *testing.T) {
t.Run(
"stdlib",
testGenerate(check("strings.Join", "Join concatenates", "strings.Join", "func(elems, sep)"), "", o, "strings.Join"),
)
notationPrintln := "code.squareroundforest.org/arpio/notation.Println"
t.Run(
"imported module",
testGenerate(
check(
notationPrintln,
"Println prints",
notationPrintln,
"func(v)",
),
"",
o,
notationPrintln,
),
)
localDocs := "code.squareroundforest.org/arpio/docreflect.Docs"
t.Run(
"local",
testGenerate(check(localDocs, "Docs returns the documentation for"), "", o, localDocs),
)
packagePath := "code.squareroundforest.org/arpio/docreflect/tests/src/testpackage"
t.Run(
"package",
testGenerate(
check(
packagePath, "Package testpackage is a test package",
fmt.Sprintf("%s.%s", packagePath, "ExportedType"), "ExportedType has docs",
),
"",
o,
packagePath,
),
)
t.Run("symbol", func(t *testing.T) {
t.Run("type", testGenerate(
check(
"code.squareroundforest.org/arpio/docreflect/tests/src/testpackage.ExportedType",
"ExportedType has docs",
"code.squareroundforest.org/arpio/docreflect/tests/src/testpackage.ExportedType2",
"",
),
"",
o,
"code.squareroundforest.org/arpio/docreflect/tests/src/testpackage.ExportedType",
"code.squareroundforest.org/arpio/docreflect/tests/src/testpackage.ExportedType2",
))
t.Run("const", testGenerate(
check(
"code.squareroundforest.org/arpio/docreflect/tests/src/testpackage.C1",
"C1 is a const",
),
"",
o,
"code.squareroundforest.org/arpio/docreflect/tests/src/testpackage.C1",
))
t.Run("const grouped", testGenerate(
check(
"code.squareroundforest.org/arpio/docreflect/tests/src/testpackage.C3",
"Cx is a const",
),
"",
o,
"code.squareroundforest.org/arpio/docreflect/tests/src/testpackage.C3",
))
t.Run("var", testGenerate(
check(
"code.squareroundforest.org/arpio/docreflect/tests/src/testpackage.V1",
"V1 is a var",
),
"",
o,
"code.squareroundforest.org/arpio/docreflect/tests/src/testpackage.V1",
))
t.Run("var grouped", testGenerate(
check(
"code.squareroundforest.org/arpio/docreflect/tests/src/testpackage.V3",
"Vx is a var",
),
"",
o,
"code.squareroundforest.org/arpio/docreflect/tests/src/testpackage.V3",
))
t.Run("func", testGenerate(
check(
"code.squareroundforest.org/arpio/docreflect/tests/src/testpackage.ExportedFunc",
"ExportedFunc has documentation",
"code.squareroundforest.org/arpio/docreflect/tests/src/testpackage.New",
"New create a new instance of ExportedType",
),
"",
o,
"code.squareroundforest.org/arpio/docreflect/tests/src/testpackage.New",
"code.squareroundforest.org/arpio/docreflect/tests/src/testpackage.ExportedFunc",
))
})
t.Run("field", testGenerate(
check(
"code.squareroundforest.org/arpio/docreflect/tests/src/testpackage.ExportedType.Foo",
"Foo is a field",
),
"",
o,
"code.squareroundforest.org/arpio/docreflect/tests/src/testpackage.ExportedType.Foo",
))
t.Run("method", testGenerate(
check(
"code.squareroundforest.org/arpio/docreflect/tests/src/testpackage.ExportedType.Method",
"Method is a method of ExportedType",
),
"",
o,
"code.squareroundforest.org/arpio/docreflect/tests/src/testpackage.ExportedType.Method",
))
t.Run("inline struct type expression", testGenerate(
check(
"code.squareroundforest.org/arpio/docreflect/tests/src/testpackage.ExportedType.Bar",
"Bar is an inline struct type expression",
"code.squareroundforest.org/arpio/docreflect/tests/src/testpackage.ExportedType.Bar.Baz",
"Baz is another field",
),
"",
o,
"code.squareroundforest.org/arpio/docreflect/tests/src/testpackage.ExportedType.Bar",
"code.squareroundforest.org/arpio/docreflect/tests/src/testpackage.ExportedType.Bar.Baz",
))
t.Run("unexported symbol", testGenerate(
check(
"code.squareroundforest.org/arpio/docreflect/tests/src/testpackage.unexportedFunc",
"unexportedFunc can have documentation",
),
"",
o,
"code.squareroundforest.org/arpio/docreflect/tests/src/testpackage.unexportedFunc",
))
mainFunc := "code.squareroundforest.org/arpio/docreflect/tests/src/command.main"
t.Run("main package", testGenerate(check(mainFunc, "main func"), "", o, mainFunc))
})
t.Run("errors", func(t *testing.T) {
t.Run("package not found", testGenerate(nil, "package", o, "foo.bar.baz/qux"))
t.Run(
"symbol not found",
testGenerate(nil, "symbol", o, "code.squareroundforest.org/arpio/docreflect/tests/src/testpackage.Qux"),
)
t.Run(
"field not found",
testGenerate(
nil,
"symbol",
o,
"code.squareroundforest.org/arpio/docreflect/tests/src/testpackage.ExportedType.Qux",
),
)
t.Run(
"field not found, second level",
testGenerate(
nil,
"symbol",
o,
"code.squareroundforest.org/arpio/docreflect/tests/src/testpackage.ExportedType.Foo.Qux",
),
)
t.Run(
"method not found",
testGenerate(nil, "symbol", o, "code.squareroundforest.org/arpio/docreflect/tests/src/testpackage.ExportedType2.Qux"),
)
t.Run(
"invalid path",
testGenerate(nil, "foo", o, "./foo/bar"),
)
t.Run(
"non-existent module path",
testGenerate(nil, "package", o, "code.squareroundforest.org/arpio/notation/foo.Foo"),
)
t.Run(
"parse failed, syntax error",
testGenerate(nil, "package", o, "code.squareroundforest.org/arpio/docreflect/tests/src/syntaxerror"),
)
t.Run(
"no type spec",
testGenerate(nil, "symbol", o, "code.squareroundforest.org/arpio/docreflect/tests/src/testpackage.Foo.Qux"),
)
})
t.Run("init options", func(t *testing.T) {
o := initOptions()
if o.wd != os.Getenv("PWD") {
t.Fatal("wd")
}
if o.goroot != runtime.GOROOT() {
t.Fatal("goroot")
}
if o.gomod != "code.squareroundforest.org/arpio/docreflect" {
t.Fatal("gomod")
}
for _, module := range []string{
"code.squareroundforest.org/arpio/notation",
"golang.org/x/mod",
} {
var found bool
for key := range o.modules {
if strings.Contains(key, module) {
found = true
}
}
if !found {
t.Fatal("gomod")
}
}
})
}
func TestFormat(t *testing.T) {
// header
// import
// few items
// escaping
b := bytes.NewBuffer(nil)
d := map[string]string{
"foo": "bar",
"baz": "qux",
}
if err := format(b, "testpackage", d); err != nil {
t.Fatal(err)
}
o := b.String()
if o != `package testpackage
import "code.squareroundforest.org/arpio/docreflect"
func init() {
docreflect.Register("baz", "qux")
docreflect.Register("foo", "bar")
}
` {
t.Fatal()
}
}

8
go.mod Normal file
View File

@ -0,0 +1,8 @@
module code.squareroundforest.org/arpio/docreflect
go 1.24.3
require (
code.squareroundforest.org/arpio/notation v0.0.0-20241225183158-af3bd591a174 // indirect
golang.org/x/mod v0.27.0 // indirect
)

4
go.sum Normal file
View File

@ -0,0 +1,4 @@
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=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=

30
readme.md Normal file
View File

@ -0,0 +1,30 @@
# Docreflect
Library and command to help accessing go doc comments during runtime.
Go doc comments are not accessible during runtime vi 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
- 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
Library documentation: https://godocs.io/code.squareroundforest.org/arpio/docreflect
To insall the docreflect command, run:
```
make install
```
Usage of the docreflect command:
```
docreflect generate --package-name mypackage coderepos.org/jdoe/mypackage > docreflect.go
```

View File

@ -0,0 +1,5 @@
package main
// main func
func main() {
}

View File

@ -0,0 +1,3 @@
package syntaxerror
type struct foo

View File

@ -0,0 +1,64 @@
// Package testpackage is a test package
//
package testpackage
import "fmt"
// unexportedType can have docs, too
type unexportedType int
// ExportedType has docs
type ExportedType struct {
// Foo is a field
Foo int
// Bar is an inline struct type expression
Bar struct {
// Baz is another field
Baz int
}
}
type Foo = ExportedType
type ExportedType2 struct{}
// C1 is a const
const C1 = 42
// Cx is a const
const (
C2 = 1 << iota
C3
)
// V1 is a var
var V1 = C1
// Vx is a var
var (
V2 = C2
V3 = C3
)
// New create a new instance of ExportedType
func New(p1, p2 int, p3 string) ExportedType {
return ExportedType{}
}
// Method is a method of ExportedType
func (t ExportedType) Method(p1, p2 int, p3 string) (o string, err error) {
return fmt.Sprint(t.Bar.Baz), nil
}
// ExportedFunc has documentation
func ExportedFunc(p1, p2 int) int {
return p1 + p2
}
// unexportedFunc can have documentation
func unexportedFunc(p1, p2 int) int {
return p1 + p2
}