docreflect/docs.go

224 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 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 {
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(st reflect.Type, name string) (reflect.StructField, bool) {
for i := 0; i < st.NumField(); i++ {
f := st.Field(i)
if f.Name == name {
return f, true
}
}
return reflect.StructField{}, false
}
// Field returns the docuemntation for a struct field.
func Field(t reflect.Type, fieldPath ...string) string {
if len(fieldPath) == 0 {
return ""
}
t = unpack(t)
if t.Kind() != reflect.Struct {
return ""
}
f, found := structField(t, fieldPath[0])
if !found {
return ""
}
if len(fieldPath) == 1 {
println("returning docs", strings.Join([]string{t.PkgPath(), t.Name(), fieldPath[0]}, "."))
return docs(strings.Join([]string{t.PkgPath(), t.Name(), fieldPath[0]}, "."))
}
ft := unpack(f.Type)
if ft.Kind() != reflect.Struct {
return ""
}
if ft.Name() != "" {
return Field(ft, fieldPath[1:]...)
}
st := ft
path := fieldPath[1:]
for {
f, found = structField(st, path[0])
if !found {
return ""
}
if len(path) == 1 {
return docs(strings.Join(append([]string{t.PkgPath(), t.Name()}, fieldPath...), "."))
}
path = path[1:]
if len(path) == 0 {
return ""
}
st = unpack(f.Type)
if st.Kind() != reflect.Struct {
return ""
}
if st.Name() != "" {
return Field(st, path...)
}
}
}
// 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)
}