// # Build from project root: // go build -o tools/parse_go_ast tools/parse_go_ast.go package main import ( "encoding/json" "fmt" "go/ast" "go/parser" "go/token" "io/ioutil" "os" "strings" ) type GoDecl struct { Name string `json:"name"` Type string `json:"type"` // "func", "method", "type", "struct", "interface", "var", "package" Receiver string `json:"receiver,omitempty"` // e.g., "*User" FullName string `json:"full_name,omitempty"` Fields []string `json:"fields,omitempty"` // for structs Methods []string `json:"methods,omitempty"` // for interfaces DocComment string `json:"doc_comment,omitempty"` StartLine int `json:"start_line"` EndLine int `json:"end_line"` } func astTypeToString(expr ast.Expr) string { switch t := expr.(type) { case *ast.Ident: return t.Name case *ast.StarExpr: return "*" + astTypeToString(t.X) case *ast.SelectorExpr: return astTypeToString(t.X) + "." + t.Sel.Name case *ast.ArrayType: return "[]" + astTypeToString(t.Elt) case *ast.MapType: return "map[" + astTypeToString(t.Key) + "]" + astTypeToString(t.Value) default: return "unknown" } } func extractDocComment(comments []*ast.CommentGroup, pos token.Pos, fset *token.FileSet) string { if len(comments) == 0 { return "" } line := fset.Position(pos).Line for i := len(comments) - 1; i >= 0; i-- { cg := comments[i] cgLine := fset.Position(cg.End()).Line if cgLine < line && line-cgLine <= 5 { return strings.TrimSpace(cg.Text()) } } return "" } func main() { if len(os.Args) < 2 { fmt.Println("Usage: parse_go_ast ") os.Exit(1) } filename := os.Args[1] src, err := ioutil.ReadFile(filename) if err != nil { fmt.Printf("ERR: %v\n", err) os.Exit(2) } fset := token.NewFileSet() fileNode, err := parser.ParseFile(fset, filename, src, parser.ParseComments) if err != nil { fmt.Printf("ERR: %v\n", err) os.Exit(3) } decls := []GoDecl{} // Package comment (first comment group before package decl) pkgComment := "" for _, cg := range fileNode.Comments { if cg.Pos() < fileNode.Package { pkgComment = strings.TrimSpace(cg.Text()) } else { break } } if pkgComment != "" { decls = append(decls, GoDecl{ Name: "package", Type: "package", DocComment: pkgComment, StartLine: 1, EndLine: 1, }) } // Process declarations for _, d := range fileNode.Decls { switch d := d.(type) { case *ast.FuncDecl: name := d.Name.Name recvType := "" if d.Recv != nil && len(d.Recv.List) > 0 { recvType = astTypeToString(d.Recv.List[0].Type) } typ := "func" fullName := name if recvType != "" { typ = "method" fullName = fmt.Sprintf("(%s).%s", recvType, name) } doc := extractDocComment(fileNode.Comments, d.Pos(), fset) decls = append(decls, GoDecl{ Name: name, Type: typ, Receiver: recvType, FullName: fullName, DocComment: doc, StartLine: fset.Position(d.Pos()).Line, EndLine: fset.Position(d.End()).Line, }) case *ast.GenDecl: for _, spec := range d.Specs { switch s := spec.(type) { case *ast.TypeSpec: typName := s.Name.Name declType := "type" var fields []string var methods []string switch t := s.Type.(type) { case *ast.StructType: declType = "struct" for _, f := range t.Fields.List { for _, n := range f.Names { field := n.Name if f.Tag != nil { field += " " + f.Tag.Value } fields = append(fields, field) } if f.Names == nil { // Embedded field fields = append(fields, astTypeToString(f.Type)) } } case *ast.InterfaceType: declType = "interface" for _, m := range t.Methods.List { if len(m.Names) > 0 { methods = append(methods, m.Names[0].Name) } } } doc := extractDocComment(fileNode.Comments, s.Pos(), fset) decls = append(decls, GoDecl{ Name: typName, Type: declType, Fields: fields, Methods: methods, DocComment: doc, StartLine: fset.Position(d.Pos()).Line, EndLine: fset.Position(d.End()).Line, }) case *ast.ValueSpec: for _, name := range s.Names { doc := extractDocComment(fileNode.Comments, name.Pos(), fset) // Use the individual identifier's position, not the declaration's position startLine := fset.Position(name.Pos()).Line endLine := fset.Position(name.End()).Line decls = append(decls, GoDecl{ Name: name.Name, Type: "var", DocComment: doc, StartLine: startLine, EndLine: endLine, }) } } } } } out, _ := json.Marshal(decls) fmt.Println(string(out)) }