internal/parser/parser.go
package parser import ( "context" "fmt" "go/ast" "go/parser" "go/token" "go/types" "path/filepath" "reflect" "strings" "github.com/pkg/errors" "golang.org/x/tools/go/packages") // StructField represents one field in structtype StructField struct { name string // typ types.Type // field/method/parameter type tag reflect.StructTag // field tag; or nil} func (sf StructField) Name() string { return sf.name} func (sf StructField) Type() types.Type { return sf.typ} func (sf StructField) Tag() reflect.StructTag { return sf.tag} // ParsedStruct represents struct infotype ParsedStruct struct { TypeName string Fields []StructField Doc *ast.CommentGroup // line comments; or nil} type Result struct { Structs map[string]ParsedStruct PackageName string Types *types.Package} type Structs struct{} Method `Structs.ParseFile` has 5 return statements (exceeds 4 allowed).func (p Structs) ParseFile(ctx context.Context, filePath string) (*Result, error) { absFilePath, err := filepath.Abs(filePath) if err != nil { return nil, errors.Wrapf(err, "can't get abs path for %s", filePath) } neededStructs, err := p.getStructNamesInFile(absFilePath) if err != nil { return nil, errors.Wrap(err, "can't get struct names") } // need load the full package type info because // some deps can be in other files inPkgName := filepath.Dir(filePath) if !filepath.IsAbs(inPkgName) && !strings.HasPrefix(inPkgName, ".") { // to make this dir name a local package name // can't use filepath.Join because it calls Clean and removes "."+sep inPkgName = fmt.Sprintf(".%c%s", filepath.Separator, inPkgName) } pkgs, err := packages.Load(&packages.Config{ Mode: packages.LoadAllSyntax, Context: ctx, Tests: false, }, inPkgName) if err != nil { return nil, errors.Wrapf(err, "failed to load package for file %s", filePath) } if len(pkgs) != 1 { return nil, fmt.Errorf("got too many (%d) packages: %#v", len(pkgs), pkgs) } structs := p.buildParsedStructs(pkgs[0], neededStructs) return &Result{ Structs: structs, PackageName: pkgs[0].Name, Types: pkgs[0].Types, }, nil} func (p Structs) buildParsedStructs(pkg *packages.Package, neededStructs structNamesInfo) map[string]ParsedStruct { ret := map[string]ParsedStruct{} scope := pkg.Types.Scope() for _, name := range scope.Names() { obj := scope.Lookup(name) if neededStructs[name] == nil { continue } t := obj.Type().(*types.Named) s := t.Underlying().(*types.Struct) parsedStruct := parseStruct(s, neededStructs[name]) if parsedStruct != nil { parsedStruct.TypeName = name ret[name] = *parsedStruct } else { // TODO } } return ret} type structNamesInfo map[string]*ast.GenDecl type structNamesVisitor struct { names structNamesInfo curGenDecl *ast.GenDecl} func (v *structNamesVisitor) Visit(n ast.Node) (w ast.Visitor) { switch n := n.(type) { case *ast.GenDecl: v.curGenDecl = n case *ast.TypeSpec: if _, ok := n.Type.(*ast.StructType); ok { v.names[n.Name.Name] = v.curGenDecl } } return v} func (p Structs) getStructNamesInFile(fname string) (structNamesInfo, error) { fset := token.NewFileSet() f, err := parser.ParseFile(fset, fname, nil, parser.ParseComments) if err != nil { return nil, fmt.Errorf("can't parse file %q: %s", fname, err) } v := structNamesVisitor{ names: structNamesInfo{}, } ast.Walk(&v, f) return v.names, nil} func newStructField(f *types.Var, tag string) *StructField { return &StructField{ name: f.Name(), typ: f.Type(), tag: reflect.StructTag(tag), }} func parseStructFields(s *types.Struct) []StructField { var fields []StructField for i := 0; i < s.NumFields(); i++ { f := s.Field(i) if _, ok := f.Type().Underlying().(*types.Interface); ok { // skip interfaces continue } if f.Anonymous() { e, ok := f.Type().Underlying().(*types.Struct) if !ok { continue } pf := parseStructFields(e) if len(pf) == 0 { continue } fields = append(fields, pf...) continue } if !f.Exported() { continue } sf := newStructField(f, s.Tag(i)) fields = append(fields, *sf) } return fields} func parseStruct(s *types.Struct, decl *ast.GenDecl) *ParsedStruct { fields := parseStructFields(s) if len(fields) == 0 { // e.g. no exported fields in struct return nil } var doc *ast.CommentGroup if decl != nil { // decl can be nil for embedded structs doc = decl.Doc // can obtain doc only from AST } return &ParsedStruct{ Fields: fields, Doc: doc, }}