init repo
This commit is contained in:
commit
b130b3dcd8
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.bin
|
||||||
|
testdocs.go
|
||||||
|
.cover
|
41
Makefile
Normal file
41
Makefile
Normal 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
19
cmd/docreflect/main.go
Normal 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
151
docs.go
Normal 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
116
docs_test.go
Normal 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
536
generate/generate.go
Normal 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
358
generate/generate_test.go
Normal 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
8
go.mod
Normal 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
4
go.sum
Normal 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
30
readme.md
Normal 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
|
||||||
|
```
|
5
tests/src/command/main.go
Normal file
5
tests/src/command/main.go
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
// main func
|
||||||
|
func main() {
|
||||||
|
}
|
3
tests/src/syntaxerror/syntaxerror.go
Normal file
3
tests/src/syntaxerror/syntaxerror.go
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
package syntaxerror
|
||||||
|
|
||||||
|
type struct foo
|
64
tests/src/testpackage/test.go
Normal file
64
tests/src/testpackage/test.go
Normal 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
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user