reflect_comments.go

  1package jsonschema
  2
  3import (
  4	"fmt"
  5	"io/fs"
  6	gopath "path"
  7	"path/filepath"
  8	"reflect"
  9	"strings"
 10
 11	"go/ast"
 12	"go/doc"
 13	"go/parser"
 14	"go/token"
 15)
 16
 17type commentOptions struct {
 18	fullObjectText bool // use the first sentence only?
 19}
 20
 21// CommentOption allows for special configuration options when preparing Go
 22// source files for comment extraction.
 23type CommentOption func(*commentOptions)
 24
 25// WithFullComment will configure the comment extraction to process to use an
 26// object type's full comment text instead of just the synopsis.
 27func WithFullComment() CommentOption {
 28	return func(o *commentOptions) {
 29		o.fullObjectText = true
 30	}
 31}
 32
 33// AddGoComments will update the reflectors comment map with all the comments
 34// found in the provided source directories including sub-directories, in order to
 35// generate a dictionary of comments associated with Types and Fields. The results
 36// will be added to the `Reflect.CommentMap` ready to use with Schema "description"
 37// fields.
 38//
 39// The `go/parser` library is used to extract all the comments and unfortunately doesn't
 40// have a built-in way to determine the fully qualified name of a package. The `base`
 41// parameter, the URL used to import that package, is thus required to be able to match
 42// reflected types.
 43//
 44// When parsing type comments, by default we use the `go/doc`'s Synopsis method to extract
 45// the first phrase only. Field comments, which tend to be much shorter, will include everything.
 46// This behavior can be changed by using the `WithFullComment` option.
 47func (r *Reflector) AddGoComments(base, path string, opts ...CommentOption) error {
 48	if r.CommentMap == nil {
 49		r.CommentMap = make(map[string]string)
 50	}
 51	co := new(commentOptions)
 52	for _, opt := range opts {
 53		opt(co)
 54	}
 55
 56	return r.extractGoComments(base, path, r.CommentMap, co)
 57}
 58
 59func (r *Reflector) extractGoComments(base, path string, commentMap map[string]string, opts *commentOptions) error {
 60	fset := token.NewFileSet()
 61	dict := make(map[string][]*ast.Package)
 62	err := filepath.Walk(path, func(path string, info fs.FileInfo, err error) error {
 63		if err != nil {
 64			return err
 65		}
 66		if info.IsDir() {
 67			d, err := parser.ParseDir(fset, path, nil, parser.ParseComments)
 68			if err != nil {
 69				return err
 70			}
 71			for _, v := range d {
 72				// paths may have multiple packages, like for tests
 73				k := gopath.Join(base, path)
 74				dict[k] = append(dict[k], v)
 75			}
 76		}
 77		return nil
 78	})
 79	if err != nil {
 80		return err
 81	}
 82
 83	for pkg, p := range dict {
 84		for _, f := range p {
 85			gtxt := ""
 86			typ := ""
 87			ast.Inspect(f, func(n ast.Node) bool {
 88				switch x := n.(type) {
 89				case *ast.TypeSpec:
 90					typ = x.Name.String()
 91					if !ast.IsExported(typ) {
 92						typ = ""
 93					} else {
 94						txt := x.Doc.Text()
 95						if txt == "" && gtxt != "" {
 96							txt = gtxt
 97							gtxt = ""
 98						}
 99						if !opts.fullObjectText {
100							txt = doc.Synopsis(txt)
101						}
102						commentMap[fmt.Sprintf("%s.%s", pkg, typ)] = strings.TrimSpace(txt)
103					}
104				case *ast.Field:
105					txt := x.Doc.Text()
106					if txt == "" {
107						txt = x.Comment.Text()
108					}
109					if typ != "" && txt != "" {
110						for _, n := range x.Names {
111							if ast.IsExported(n.String()) {
112								k := fmt.Sprintf("%s.%s.%s", pkg, typ, n)
113								commentMap[k] = strings.TrimSpace(txt)
114							}
115						}
116					}
117				case *ast.GenDecl:
118					// remember for the next type
119					gtxt = x.Doc.Text()
120				}
121				return true
122			})
123		}
124	}
125
126	return nil
127}
128
129func (r *Reflector) lookupComment(t reflect.Type, name string) string {
130	if r.LookupComment != nil {
131		if comment := r.LookupComment(t, name); comment != "" {
132			return comment
133		}
134	}
135
136	if r.CommentMap == nil {
137		return ""
138	}
139
140	n := fullyQualifiedTypeName(t)
141	if name != "" {
142		n = n + "." + name
143	}
144
145	return r.CommentMap[n]
146}