├── .gitignore ├── LICENSE ├── types.go ├── README.md └── parser.go /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *exe -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2015, Zack Patrick 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "reflect" 7 | "strings" 8 | ) 9 | 10 | type GoFile struct { 11 | Package string 12 | Path string 13 | Structs []*GoStruct 14 | Interfaces []*GoInterface 15 | Imports []*GoImport 16 | StructMethods []*GoStructMethod 17 | } 18 | 19 | func (g *GoFile) ImportPath() (string, error) { 20 | importPath, err := filepath.Abs(g.Path) 21 | if err != nil { 22 | return "", err 23 | } 24 | 25 | importPath = strings.Replace(importPath, "\\", "/", -1) 26 | 27 | goPath := strings.Replace(os.Getenv("GOPATH"), "\\", "/", -1) 28 | importPath = strings.TrimPrefix(importPath, goPath) 29 | importPath = strings.TrimPrefix(importPath, "/src/") 30 | 31 | importPath = strings.TrimSuffix(importPath, filepath.Base(importPath)) 32 | importPath = strings.TrimSuffix(importPath, "/") 33 | 34 | return importPath, nil 35 | } 36 | 37 | type GoImport struct { 38 | File *GoFile 39 | Name string 40 | Path string 41 | } 42 | 43 | type GoInterface struct { 44 | File *GoFile 45 | Name string 46 | Methods []*GoMethod 47 | } 48 | 49 | type GoMethod struct { 50 | Name string 51 | Params []*GoType 52 | Results []*GoType 53 | } 54 | 55 | type GoStructMethod struct { 56 | GoMethod 57 | Receivers []string 58 | } 59 | 60 | type GoType struct { 61 | Name string 62 | Type string 63 | Underlying string 64 | Inner []*GoType 65 | } 66 | 67 | type GoStruct struct { 68 | File *GoFile 69 | Name string 70 | Fields []*GoField 71 | } 72 | 73 | type GoField struct { 74 | Struct *GoStruct 75 | Name string 76 | Type string 77 | Tag *GoTag 78 | } 79 | 80 | type GoTag struct { 81 | Field *GoField 82 | Value string 83 | } 84 | 85 | func (g *GoTag) Get(key string) string { 86 | tag := strings.Replace(g.Value, "`", "", -1) 87 | return reflect.StructTag(tag).Get(key) 88 | } 89 | 90 | // For an import - guess what prefix will be used 91 | // in type declarations. For examples: 92 | // "strings" -> "strings" 93 | // "net/http/httptest" -> "httptest" 94 | // Libraries where the package name does not match 95 | // will be mis-identified. 96 | func (g *GoImport) Prefix() string { 97 | if g.Name != "" { 98 | return g.Name 99 | } 100 | 101 | path := strings.Trim(g.Path, "\"") 102 | lastSlash := strings.LastIndex(path, "/") 103 | if lastSlash == -1 { 104 | return path 105 | } 106 | 107 | return path[lastSlash+1:] 108 | } 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go Parser 2 | 3 | The `go-parser` package is a small wrapper around the [parser](https://golang.org/pkg/go/parser/) and [ast](https://golang.org/pkg/go/ast/) packages. 4 | 5 | # Installation 6 | To install this package, run: 7 | 8 | `go get github.com/zpatrick/go-parser` 9 | 10 | # Motivation 11 | If you've ever tried to implement code generation in Go, you're probably familiar with the `parser` and `ast` packages. 12 | These packages analyze and parse Go source files. 13 | The main problems I have with these packages are: 14 | * They aren't very intuitive 15 | * The amount of type assertions required to use them causes code to look cluttered and confusing 16 | 17 | This package is meant to fix both of those issues. 18 | For example, the following two snippets of code perform the same function: 19 | 20 | **Using go-parser** 21 | ``` 22 | goFile, err := parser.ParseFile("user.go") 23 | if err != nil { 24 | log.Fatal(err) 25 | } 26 | 27 | for _, goStruct := range goFile.Structs { 28 | for _, goField := range goStruct.Fields { 29 | log.Println(goField.Name, goField.Type, goField.Tag) 30 | } 31 | } 32 | ``` 33 | 34 | **Using `ast` and `parser`** 35 | ``` 36 | src, err := ioutil.ReadFile("user.go") 37 | if err != nil { 38 | log.Fatal(err) 39 | } 40 | 41 | file, err := parser.ParseFile(token.NewFileSet(), path, nil, 0) 42 | if err != nil { 43 | log.Fatal(err) 44 | } 45 | 46 | for _, d := range file.Decls { 47 | if typeDecl, ok := d.(*ast.GenDecl); ok { 48 | for _, s := range typeDecl.Specs { 49 | if typeSpec, ok := s.(*ast.TypeSpec); ok { 50 | if structDecl, ok := typeSpec.Type.(*ast.StructType); ok { 51 | for _, field := range structDecl.Fields.List { 52 | for _, name := range field.Names { 53 | name := name.String() 54 | _type := string(src[field.Type.Pos()-1 : field.Type.End()-1]) 55 | tag := "" 56 | if field.Tag != nil{ 57 | tag = field.Tag.Value 58 | } 59 | 60 | fmt.Printf(name, _type, tag) 61 | } 62 | } 63 | } 64 | } 65 | } 66 | } 67 | } 68 | ``` 69 | 70 | # Disclaimer & Contributing 71 | I have been adding coverage on an as-needed basis. As such, this package is very much incomplete relative to the amount of coverage available in the `ast` and `parser` packages. However, merge requests are **strongly** encouraged! I've added a lot of comments to the parsing code to hopefully make it easier to read and contribute to. 72 | 73 | # License 74 | This work is published under the MIT license. 75 | 76 | Please see the `LICENSE` file for details. -------------------------------------------------------------------------------- /parser.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "go/importer" 7 | "go/parser" 8 | "go/token" 9 | "go/types" 10 | "io/ioutil" 11 | ) 12 | 13 | // ParseFiles parses files at the same time 14 | func ParseFiles(paths []string) ([]*GoFile, error) { 15 | files := make([]*ast.File, len(paths)) 16 | fsets := make([]*token.FileSet, len(paths)) 17 | for i, p := range paths { 18 | // File: A File node represents a Go source file: https://golang.org/pkg/go/ast/#File 19 | fset := token.NewFileSet() 20 | file, err := parser.ParseFile(fset, p, nil, 0) 21 | if err != nil { 22 | return nil, err 23 | } 24 | files[i] = file 25 | fsets[i] = fset 26 | } 27 | 28 | goFiles := make([]*GoFile, len(paths)) 29 | for i, p := range paths { 30 | goFile, err := parseFile(p, files[i], fsets[i], files) 31 | if err != nil { 32 | return nil, err 33 | } 34 | goFiles[i] = goFile 35 | } 36 | return goFiles, nil 37 | } 38 | 39 | // ParseSingleFile parses a single file at the same time 40 | func ParseSingleFile(path string) (*GoFile, error) { 41 | fset := token.NewFileSet() 42 | file, err := parser.ParseFile(fset, path, nil, 0) 43 | if err != nil { 44 | return nil, err 45 | } 46 | return parseFile(path, file, fset, []*ast.File{file}) 47 | } 48 | 49 | func parseFile(path string, file *ast.File, fset *token.FileSet, files []*ast.File) (*GoFile, error) { 50 | source, err := ioutil.ReadFile(path) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | // To import sources from vendor, we use "source" compile 56 | // https://github.com/golang/go/issues/11415#issuecomment-283445198 57 | conf := types.Config{Importer: importer.For("source", nil)} 58 | info := &types.Info{ 59 | Types: make(map[ast.Expr]types.TypeAndValue), 60 | Defs: make(map[*ast.Ident]types.Object), 61 | Uses: make(map[*ast.Ident]types.Object), 62 | } 63 | 64 | if _, err = conf.Check(file.Name.Name, fset, files, info); err != nil { 65 | return nil, err 66 | } 67 | 68 | goFile := &GoFile{ 69 | Path: path, 70 | Package: file.Name.Name, 71 | Structs: []*GoStruct{}, 72 | } 73 | 74 | // File.Decls: A list of the declarations in the file: https://golang.org/pkg/go/ast/#Decl 75 | for _, decl := range file.Decls { 76 | switch declType := decl.(type) { 77 | 78 | // GenDecl: represents an import, constant, type or variable declaration: https://golang.org/pkg/go/ast/#GenDecl 79 | case *ast.GenDecl: 80 | genDecl := declType 81 | 82 | // Specs: the Spec type stands for any of *ImportSpec, *ValueSpec, and *TypeSpec: https://golang.org/pkg/go/ast/#Spec 83 | for _, genSpec := range genDecl.Specs { 84 | switch genSpecType := genSpec.(type) { 85 | 86 | // TypeSpec: A TypeSpec node represents a type declaration: https://golang.org/pkg/go/ast/#TypeSpec 87 | case *ast.TypeSpec: 88 | typeSpec := genSpecType 89 | 90 | // typeSpec.Type: an Expr (expression) node: https://golang.org/pkg/go/ast/#Expr 91 | switch typeSpecType := typeSpec.Type.(type) { 92 | 93 | // StructType: A StructType node represents a struct type: https://golang.org/pkg/go/ast/#StructType 94 | case (*ast.StructType): 95 | structType := typeSpecType 96 | goStruct := buildGoStruct(source, goFile, info, typeSpec, structType) 97 | goFile.Structs = append(goFile.Structs, goStruct) 98 | // InterfaceType: An InterfaceType node represents an interface type. https://golang.org/pkg/go/ast/#InterfaceType 99 | case (*ast.InterfaceType): 100 | interfaceType := typeSpecType 101 | goInterface := buildGoInterface(source, goFile, info, typeSpec, interfaceType) 102 | goFile.Interfaces = append(goFile.Interfaces, goInterface) 103 | default: 104 | // a not-implemented typeSpec.Type.(type), ignore 105 | } 106 | // ImportSpec: An ImportSpec node represents a single package import. https://golang.org/pkg/go/ast/#ImportSpec 107 | case *ast.ImportSpec: 108 | importSpec := genSpec.(*ast.ImportSpec) 109 | goImport := buildGoImport(importSpec, goFile) 110 | goFile.Imports = append(goFile.Imports, goImport) 111 | default: 112 | // a not-implemented genSpec.(type), ignore 113 | } 114 | } 115 | case *ast.FuncDecl: 116 | funcDecl := declType 117 | goStructMethod := buildStructMethod(info, funcDecl, source) 118 | goFile.StructMethods = append(goFile.StructMethods, goStructMethod) 119 | 120 | default: 121 | // a not-implemented decl.(type), ignore 122 | } 123 | } 124 | 125 | return goFile, nil 126 | } 127 | 128 | func buildGoImport(spec *ast.ImportSpec, file *GoFile) *GoImport { 129 | name := "" 130 | if spec.Name != nil { 131 | name = spec.Name.Name 132 | } 133 | 134 | path := "" 135 | if spec.Path != nil { 136 | path = spec.Path.Value 137 | } 138 | 139 | return &GoImport{ 140 | Name: name, 141 | Path: path, 142 | File: file, 143 | } 144 | } 145 | 146 | func buildGoInterface(source []byte, file *GoFile, info *types.Info, typeSpec *ast.TypeSpec, interfaceType *ast.InterfaceType) *GoInterface { 147 | goInterface := &GoInterface{ 148 | File: file, 149 | Name: typeSpec.Name.Name, 150 | Methods: buildMethodList(info, interfaceType.Methods.List, source), 151 | } 152 | 153 | return goInterface 154 | } 155 | 156 | func buildMethodList(info *types.Info, fieldList []*ast.Field, source []byte) []*GoMethod { 157 | methods := []*GoMethod{} 158 | 159 | for _, field := range fieldList { 160 | name := getNames(field)[0] 161 | 162 | fType, ok := field.Type.(*ast.FuncType) 163 | if !ok { 164 | // method was not a function 165 | continue 166 | } 167 | 168 | goMethod := &GoMethod{ 169 | Name: name, 170 | Params: buildTypeList(info, fType.Params, source), 171 | Results: buildTypeList(info, fType.Results, source), 172 | } 173 | 174 | methods = append(methods, goMethod) 175 | } 176 | 177 | return methods 178 | } 179 | 180 | func buildStructMethod(info *types.Info, funcDecl *ast.FuncDecl, source []byte) *GoStructMethod { 181 | return &GoStructMethod{ 182 | Receivers: buildReceiverList(info, funcDecl.Recv, source), 183 | GoMethod: GoMethod{ 184 | Name: funcDecl.Name.Name, 185 | Params: buildTypeList(info, funcDecl.Type.Params, source), 186 | Results: buildTypeList(info, funcDecl.Type.Results, source), 187 | }, 188 | } 189 | } 190 | 191 | func buildReceiverList(info *types.Info, fieldList *ast.FieldList, source []byte) []string { 192 | receivers := []string{} 193 | 194 | if fieldList != nil { 195 | for _, t := range fieldList.List { 196 | receivers = append(receivers, getTypeString(t.Type, source)) 197 | } 198 | } 199 | 200 | return receivers 201 | } 202 | 203 | func buildTypeList(info *types.Info, fieldList *ast.FieldList, source []byte) []*GoType { 204 | types := []*GoType{} 205 | 206 | if fieldList != nil { 207 | for _, t := range fieldList.List { 208 | goType := buildType(info, t.Type, source) 209 | 210 | for _, n := range getNames(t) { 211 | copyType := copyType(goType) 212 | copyType.Name = n 213 | types = append(types, copyType) 214 | } 215 | } 216 | } 217 | 218 | return types 219 | } 220 | 221 | func getNames(field *ast.Field) []string { 222 | if field.Names == nil || len(field.Names) == 0 { 223 | return []string{""} 224 | } 225 | 226 | result := []string{} 227 | for _, name := range field.Names { 228 | result = append(result, name.String()) 229 | } 230 | 231 | return result 232 | } 233 | 234 | func getTypeString(expr ast.Expr, source []byte) string { 235 | return string(source[expr.Pos()-1 : expr.End()-1]) 236 | } 237 | 238 | func getUnderlyingTypeString(info *types.Info, expr ast.Expr) string { 239 | if typeInfo := info.TypeOf(expr); typeInfo != nil { 240 | if underlying := typeInfo.Underlying(); underlying != nil { 241 | return underlying.String() 242 | } 243 | } 244 | 245 | return "" 246 | } 247 | 248 | func copyType(goType *GoType) *GoType { 249 | return &GoType{ 250 | Type: goType.Type, 251 | Inner: goType.Inner, 252 | Name: goType.Name, 253 | Underlying: goType.Underlying, 254 | } 255 | } 256 | 257 | func buildType(info *types.Info, expr ast.Expr, source []byte) *GoType { 258 | innerTypes := []*GoType{} 259 | typeString := getTypeString(expr, source) 260 | underlyingString := getUnderlyingTypeString(info, expr) 261 | 262 | switch specType := expr.(type) { 263 | case *ast.FuncType: 264 | innerTypes = append(innerTypes, buildTypeList(info, specType.Params, source)...) 265 | innerTypes = append(innerTypes, buildTypeList(info, specType.Results, source)...) 266 | case *ast.ArrayType: 267 | innerTypes = append(innerTypes, buildType(info, specType.Elt, source)) 268 | case *ast.StructType: 269 | innerTypes = append(innerTypes, buildTypeList(info, specType.Fields, source)...) 270 | case *ast.MapType: 271 | innerTypes = append(innerTypes, buildType(info, specType.Key, source)) 272 | innerTypes = append(innerTypes, buildType(info, specType.Value, source)) 273 | case *ast.ChanType: 274 | innerTypes = append(innerTypes, buildType(info, specType.Value, source)) 275 | case *ast.StarExpr: 276 | innerTypes = append(innerTypes, buildType(info, specType.X, source)) 277 | case *ast.Ellipsis: 278 | innerTypes = append(innerTypes, buildType(info, specType.Elt, source)) 279 | case *ast.InterfaceType: 280 | methods := buildMethodList(info, specType.Methods.List, source) 281 | for _, m := range methods { 282 | innerTypes = append(innerTypes, m.Params...) 283 | innerTypes = append(innerTypes, m.Results...) 284 | } 285 | 286 | case *ast.Ident: 287 | case *ast.SelectorExpr: 288 | default: 289 | fmt.Printf("Unexpected field type: `%s`,\n %#v\n", typeString, specType) 290 | } 291 | 292 | return &GoType{ 293 | Type: typeString, 294 | Underlying: underlyingString, 295 | Inner: innerTypes, 296 | } 297 | } 298 | 299 | func buildGoStruct(source []byte, file *GoFile, info *types.Info, typeSpec *ast.TypeSpec, structType *ast.StructType) *GoStruct { 300 | goStruct := &GoStruct{ 301 | File: file, 302 | Name: typeSpec.Name.Name, 303 | Fields: []*GoField{}, 304 | } 305 | 306 | // Field: A Field declaration list in a struct type, a method list in an interface type, 307 | // or a parameter/result declaration in a signature: https://golang.org/pkg/go/ast/#Field 308 | for _, field := range structType.Fields.List { 309 | for _, name := range field.Names { 310 | goField := &GoField{ 311 | Struct: goStruct, 312 | Name: name.String(), 313 | Type: string(source[field.Type.Pos()-1 : field.Type.End()-1]), 314 | } 315 | 316 | if field.Tag != nil { 317 | goTag := &GoTag{ 318 | Field: goField, 319 | Value: field.Tag.Value, 320 | } 321 | 322 | goField.Tag = goTag 323 | } 324 | 325 | goStruct.Fields = append(goStruct.Fields, goField) 326 | } 327 | } 328 | 329 | return goStruct 330 | } 331 | --------------------------------------------------------------------------------