fix struct field docs

This commit is contained in:
Arpad Ryszka 2025-08-09 20:39:45 +02:00
parent 0fcdf50ce5
commit af83b13d7c
5 changed files with 142 additions and 19 deletions

78
docs.go
View File

@ -13,11 +13,24 @@ import (
"strings" "strings"
) )
type pointer[T any] interface {
Kind() reflect.Kind
Elem() T
}
var ( var (
registry = make(map[string]string) registry = make(map[string]string)
funcParamsExp = regexp.MustCompile("^func[(]([a-zA-Z_][a-zA-Z0-9_]+(, [a-zA-Z_][a-zA-Z0-9_]+)*)?[)]$") 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 { func docs(p string) string {
return registry[p] return registry[p]
} }
@ -91,6 +104,7 @@ func Docs(gopath string) string {
// Function returns the documentation for a package level function. // Function returns the documentation for a package level function.
func Function(v reflect.Value) string { func Function(v reflect.Value) string {
v = unpack(v)
if v.Kind() != reflect.Func { if v.Kind() != reflect.Func {
return "" return ""
} }
@ -100,6 +114,7 @@ func Function(v reflect.Value) string {
// FunctionParams returns the list of the parameter names of a package level function. // FunctionParams returns the list of the parameter names of a package level function.
func FunctionParams(v reflect.Value) []string { func FunctionParams(v reflect.Value) []string {
v = unpack(v)
if v.Kind() != reflect.Func { if v.Kind() != reflect.Func {
return nil return nil
} }
@ -110,27 +125,83 @@ func FunctionParams(v reflect.Value) []string {
// Type returns the docuemntation for a package level type. // Type returns the docuemntation for a package level type.
func Type(t reflect.Type) string { func Type(t reflect.Type) string {
t = unpack(t)
p := fmt.Sprintf("%s.%s", t.PkgPath(), t.Name()) p := fmt.Sprintf("%s.%s", t.PkgPath(), t.Name())
return docs(p) 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. // Field returns the docuemntation for a struct field.
func Field(t reflect.Type, fieldPath ...string) string { func Field(t reflect.Type, fieldPath ...string) string {
if len(fieldPath) == 0 { if len(fieldPath) == 0 {
return "" return ""
} }
t = unpack(t)
if t.Kind() != reflect.Struct { if t.Kind() != reflect.Struct {
return "" return ""
} }
p := strings.Join(append([]string{t.PkgPath(), t.Name()}, fieldPath...), ".") f, found := structField(t, fieldPath[0])
println(p) if !found {
return docs(p) 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. // Method returns the documentation for a type method.
func Method(t reflect.Type, name string) string { func Method(t reflect.Type, name string) string {
t = unpack(t)
if t.Kind() != reflect.Struct { if t.Kind() != reflect.Struct {
return "" return ""
} }
@ -141,6 +212,7 @@ func Method(t reflect.Type, name string) string {
// MethodParams returns the list of the parameter names of a type method. // MethodParams returns the list of the parameter names of a type method.
func MethodParams(t reflect.Type, name string) []string { func MethodParams(t reflect.Type, name string) []string {
t = unpack(t)
if t.Kind() != reflect.Struct { if t.Kind() != reflect.Struct {
return nil return nil
} }

View File

@ -96,6 +96,24 @@ func Test(t *testing.T) {
} }
}) })
t.Run("field in inline struct type", func(t *testing.T) {
s := &testpackage.ExportedType{}
typ := reflect.TypeOf(s)
d := docreflect.Field(typ, "Bar", "Baz")
if !strings.Contains(d, "Baz is another field") {
t.Fatal(d)
}
})
t.Run("field in another type", func(t *testing.T) {
s := &testpackage.ExportedType{}
typ := reflect.TypeOf(s)
d := docreflect.Field(typ, "Baz", "Foo")
if !strings.Contains(d, "Foo is a field in ExportedType2") {
t.Fatal(d)
}
})
t.Run("method", func(t *testing.T) { t.Run("method", func(t *testing.T) {
s := testpackage.ExportedType{} s := testpackage.ExportedType{}
typ := reflect.TypeOf(s) typ := reflect.TypeOf(s)

View File

@ -320,7 +320,7 @@ func symbolDocs(pkg *doc.Package, gopath string) (string, bool) {
fieldPath := symbol[1:] fieldPath := symbol[1:]
for len(fieldPath) > 0 { for len(fieldPath) > 0 {
var found bool var foundField bool
if str.Fields != nil { if str.Fields != nil {
for _, f := range str.Fields.List { for _, f := range str.Fields.List {
for _, fn := range f.Names { for _, fn := range f.Names {
@ -329,6 +329,7 @@ func symbolDocs(pkg *doc.Package, gopath string) (string, bool) {
continue continue
} }
foundField = true
if len(fieldPath) == 1 { if len(fieldPath) == 1 {
if f.Doc == nil { if f.Doc == nil {
return "", true return "", true
@ -339,19 +340,22 @@ func symbolDocs(pkg *doc.Package, gopath string) (string, bool) {
fstr, ok := f.Type.(*ast.StructType) fstr, ok := f.Type.(*ast.StructType)
if !ok { if !ok {
break return "", false
} }
found = true
str = fstr str = fstr
fieldPath = fieldPath[1:] fieldPath = fieldPath[1:]
break break
} }
} }
if foundField {
break
}
} }
} }
if !found { if !foundField {
break break
} }
} }
@ -390,6 +394,27 @@ func valueDocs(packagePath string, v []*doc.Value) map[string]string {
return d return d
} }
func takeFieldDocs(docs map[string]string, packagePath string, prefix []string, f *ast.Field) {
for _, fn := range f.Names {
if fn == nil {
continue
}
docs[symbolPath(packagePath, append(prefix, fn.Name)...)] = f.Doc.Text()
if fst, ok := f.Type.(*ast.StructType); ok {
if fst.Fields != nil {
for _, fi := range fst.Fields.List {
if fi == nil {
continue
}
takeFieldDocs(docs, packagePath, append(prefix, fn.Name), fi)
}
}
}
}
}
func packageDocs(pkg *doc.Package) map[string]string { func packageDocs(pkg *doc.Package) map[string]string {
d := make(map[string]string) d := make(map[string]string)
d[pkg.ImportPath] = pkg.Doc d[pkg.ImportPath] = pkg.Doc
@ -404,17 +429,11 @@ func packageDocs(pkg *doc.Package) map[string]string {
if ts, ok := t.Decl.Specs[0].(*ast.TypeSpec); ok { if ts, ok := t.Decl.Specs[0].(*ast.TypeSpec); ok {
if str, ok := ts.Type.(*ast.StructType); ok && str.Fields != nil { if str, ok := ts.Type.(*ast.StructType); ok && str.Fields != nil {
for _, f := range str.Fields.List { for _, f := range str.Fields.List {
if f == nil || f.Doc == nil { if f == nil {
continue continue
} }
for _, fn := range f.Names { takeFieldDocs(d, pkg.ImportPath, []string{t.Name}, f)
if fn == nil {
continue
}
d[symbolPath(pkg.ImportPath, t.Name, fn.Name)] = f.Doc.Text()
}
} }
} }
} }
@ -534,3 +553,9 @@ func GenerateRegistry(w io.Writer, outputPackageName string, gopath ...string) e
return nil return nil
} }
// TODO:
// - type foo = bar
// - type foo bar
// - type (...)
// - pointers in struct fields

View File

@ -63,9 +63,9 @@ func testGenerate(check map[string]string, errstr string, o options, gopath ...s
if failed { if failed {
t.Log("failed to get the right documentation") t.Log("failed to get the right documentation")
t.Log("expected matches:") t.Log("expected matches:")
t.Log(notation.Sprint(check)) t.Log(notation.Sprintw(check))
t.Log("documetation got:") t.Log("documetation got:")
t.Log(notation.Sprint(d)) t.Log(notation.Sprintw(d))
t.Fatal() t.Fatal()
} }
} }
@ -121,7 +121,8 @@ func TestGenerate(t *testing.T) {
testGenerate( testGenerate(
check( check(
packagePath, "Package testpackage is a test package", packagePath, "Package testpackage is a test package",
fmt.Sprintf("%s.%s", packagePath, "ExportedType"), "ExportedType has docs", fmt.Sprintf("%s.ExportedType", packagePath), "ExportedType has docs",
fmt.Sprintf("%s.ExportedType.Bar.Baz", packagePath), "Baz is another field",
), ),
"", "",
o, o,

View File

@ -19,11 +19,18 @@ type ExportedType struct {
// Baz is another field // Baz is another field
Baz int Baz int
} }
// Baz is a field of type *ExportedType2
Baz *ExportedType2
} }
type Foo = ExportedType type Foo = ExportedType
type ExportedType2 struct{} type ExportedType2 struct{
// Foo is a field in ExportedType2
Foo int
}
// C1 is a const // C1 is a const
const C1 = 42 const C1 = 42