docreflect/docs.go
2025-08-17 18:33:25 +02:00

227 lines
4.8 KiB
Go

// 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"
)
type pointer[T any] interface {
Kind() reflect.Kind
Elem() T
}
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 unpack[T pointer[T]](p T) T {
if p.Kind() != reflect.Pointer {
return p
}
return unpack(p.Elem())
}
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 len(p) <= len("func()") {
return nil
}
p = p[len("func(") : len(p)-1]
pp := strings.Split(p, ",")
for i := range pp {
pp[i] = strings.TrimSpace(pp[i])
}
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.
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 {
v = unpack(v)
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 {
v = unpack(v)
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 {
t = unpack(t)
p := fmt.Sprintf("%s.%s", t.PkgPath(), t.Name())
return Docs(p)
}
func structField(visited map[reflect.Type]bool, t reflect.Type, fieldPath []string) (reflect.Type, string, bool) {
t = unpack(t)
if t.Kind() != reflect.Struct {
return nil, "", false
}
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
if !f.Anonymous {
continue
}
ft := unpack(f.Type)
if visited[ft] {
continue
}
if visited == nil {
visited = make(map[reflect.Type]bool)
}
visited[ft] = true
if t, s, ok := structField(visited, ft, fieldPath); ok {
return t, s, true
}
}
name := fieldPath[0]
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
if f.Name != name {
continue
}
if len(fieldPath) == 1 {
return t, name, true
}
tt, s, ok := structField(nil, unpack(f.Type), fieldPath[1:])
if !ok {
return nil, "", false
}
if tt.Name() == "" {
return t, fmt.Sprintf("%s.%s", name, s), true
}
return tt, s, true
}
return nil, "", false
}
// Field returns the docuemntation for a struct field.
func Field(t reflect.Type, fieldPath ...string) string {
if len(fieldPath) == 0 {
return ""
}
t, s, ok := structField(nil, t, fieldPath)
if !ok {
return ""
}
return Docs(strings.Join([]string{t.PkgPath(), t.Name(), s}, "."))
}
// Method returns the documentation for a type method.
func Method(t reflect.Type, name string) string {
t = unpack(t)
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 {
t = unpack(t)
if t.Kind() != reflect.Struct {
return nil
}
p := fmt.Sprintf("%s.%s.%s", t.PkgPath(), t.Name(), name)
d := docs(p)
return functionParams(d)
}