github.com/alecthomas/jsonschema@v0.0.0-20220216202328-9eeeec9d044b/comment_extractor.go (about)

     1  package jsonschema
     2  
     3  import (
     4  	"fmt"
     5  	"io/fs"
     6  	gopath "path"
     7  	"path/filepath"
     8  	"strings"
     9  
    10  	"go/ast"
    11  	"go/doc"
    12  	"go/parser"
    13  	"go/token"
    14  )
    15  
    16  // ExtractGoComments will read all the go files contained in the provided path,
    17  // including sub-directories, in order to generate a dictionary of comments
    18  // associated with Types and Fields. The results will be added to the `commentsMap`
    19  // provided in the parameters and expected to be used for Schema "description" fields.
    20  //
    21  // The `go/parser` library is used to extract all the comments and unfortunately doesn't
    22  // have a built-in way to determine the fully qualified name of a package. The `base` paremeter,
    23  // the URL used to import that package, is thus required to be able to match reflected types.
    24  //
    25  // When parsing type comments, we use the `go/doc`'s Synopsis method to extract the first phrase
    26  // only. Field comments, which tend to be much shorter, will include everything.
    27  func ExtractGoComments(base, path string, commentMap map[string]string) error {
    28  	fset := token.NewFileSet()
    29  	dict := make(map[string][]*ast.Package)
    30  	err := filepath.Walk(path, func(path string, info fs.FileInfo, err error) error {
    31  		if err != nil {
    32  			return err
    33  		}
    34  		if info.IsDir() {
    35  			d, err := parser.ParseDir(fset, path, nil, parser.ParseComments)
    36  			if err != nil {
    37  				return err
    38  			}
    39  			for _, v := range d {
    40  				// paths may have multiple packages, like for tests
    41  				k := gopath.Join(base, path)
    42  				dict[k] = append(dict[k], v)
    43  			}
    44  		}
    45  		return nil
    46  	})
    47  	if err != nil {
    48  		return err
    49  	}
    50  
    51  	for pkg, p := range dict {
    52  		for _, f := range p {
    53  			gtxt := ""
    54  			typ := ""
    55  			ast.Inspect(f, func(n ast.Node) bool {
    56  				switch x := n.(type) {
    57  				case *ast.TypeSpec:
    58  					typ = x.Name.String()
    59  					if !ast.IsExported(typ) {
    60  						typ = ""
    61  					} else {
    62  						txt := x.Doc.Text()
    63  						if txt == "" && gtxt != "" {
    64  							txt = gtxt
    65  							gtxt = ""
    66  						}
    67  						txt = doc.Synopsis(txt)
    68  						commentMap[fmt.Sprintf("%s.%s", pkg, typ)] = strings.TrimSpace(txt)
    69  					}
    70  				case *ast.Field:
    71  					txt := x.Doc.Text()
    72  					if typ != "" && txt != "" {
    73  						for _, n := range x.Names {
    74  							if ast.IsExported(n.String()) {
    75  								k := fmt.Sprintf("%s.%s.%s", pkg, typ, n)
    76  								commentMap[k] = strings.TrimSpace(txt)
    77  							}
    78  						}
    79  					}
    80  				case *ast.GenDecl:
    81  					// remember for the next type
    82  					gtxt = x.Doc.Text()
    83  				}
    84  				return true
    85  			})
    86  		}
    87  	}
    88  
    89  	return nil
    90  }