diff --git a/.gitignore b/.gitignore index d51f406..f19078a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *~ -*exe \ No newline at end of file +*exe +.idea \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..83e58ec --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module github.com/GreenFuze/go-parser + +go 1.16 + +require ( + github.com/OrlovEvgeny/go-mcache v0.0.0-20200121124330-1a8195b34f3a // indirect + golang.org/x/tools v0.20.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..25679b9 --- /dev/null +++ b/go.sum @@ -0,0 +1,71 @@ +github.com/OrlovEvgeny/go-mcache v0.0.0-20200121124330-1a8195b34f3a h1:Cf4CrDeyrIcuIiJZEZJAH5dapqQ6J3OmP/vHPbDjaFA= +github.com/OrlovEvgeny/go-mcache v0.0.0-20200121124330-1a8195b34f3a/go.mod h1:ig6eVXkYn/9dz0Vm8UdLf+E0u1bE6kBSn3n2hqk6jas= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= +golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= +golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= +golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/importer.go b/importer.go new file mode 100644 index 0000000..fa1c66c --- /dev/null +++ b/importer.go @@ -0,0 +1,46 @@ +package parser + +import ( + "fmt" + "go/importer" + "go/token" + "go/types" + "golang.org/x/tools/go/packages" +) + +type PackImporter struct { + Fset *token.FileSet +} + +func (this *PackImporter) Import(path string) (*types.Package, error) { + println("searching for " + path) + + pack, err := importer.Default().Import(path) + + if err != nil { + cfg := &packages.Config{ + Fset: this.Fset, + Mode: packages.NeedTypes, + Tests: true, + } + + // Load the package by its import path + pkgs, err := packages.Load(cfg, path) + if err != nil { + return nil, err + } + + // Check for errors + if packages.PrintErrors(pkgs) > 0 { + return nil, fmt.Errorf("package %s has errors", path) + } + + // Return the first package object + return pkgs[0].Types, nil + + } + + return pack, nil +} + +//-------------------------------------------------------------------- diff --git a/parser.go b/parser.go index b9a4ef2..4a6bc4e 100644 --- a/parser.go +++ b/parser.go @@ -1,68 +1,196 @@ package parser import ( + "errors" "fmt" "go/ast" - "go/importer" "go/parser" "go/token" "go/types" - "io/ioutil" + "io/fs" + "os" + "os/exec" + "reflect" + "strings" + "unicode" ) // ParseFiles parses files at the same time -func ParseFiles(paths []string) ([]*GoFile, error) { - files := make([]*ast.File, len(paths)) - fsets := make([]*token.FileSet, len(paths)) - for i, p := range paths { - // File: A File node represents a Go source file: https://golang.org/pkg/go/ast/#File - fset := token.NewFileSet() - file, err := parser.ParseFile(fset, p, nil, 0) - if err != nil { - return nil, err - } - files[i] = file - fsets[i] = fset +func ParseDir(path string, withComments bool, filterFiles func(fs.FileInfo) bool) ([]*GoFile, error) { + + // File: A File node represents a Go source file: https://golang.org/pkg/go/ast/#File + fset := token.NewFileSet() + + var mode parser.Mode + if withComments { + mode = parser.ParseComments + } else { + mode = 0 } - goFiles := make([]*GoFile, len(paths)) - for i, p := range paths { - goFile, err := parseFile(p, files[i], fsets[i], files) - if err != nil { - return nil, err + pkgs, err := parser.ParseDir(fset, path, filterFiles, mode) + if err != nil { + return nil, err + } + + goFiles := make([]*GoFile, 0) + for _, astPackage := range pkgs { + + files := make([]*ast.File, 0) + for _, file := range astPackage.Files { + files = append(files, file) + } + + for name, file := range astPackage.Files { + goFile, err := parseFile(name, file, fset, files) + if err != nil { + return nil, err + } + goFiles = append(goFiles, goFile) } - goFiles[i] = goFile } return goFiles, nil } // ParseSingleFile parses a single file at the same time -func ParseSingleFile(path string) (*GoFile, error) { +func ParseSingleFile(path string, withComments bool) (*GoFile, error) { fset := token.NewFileSet() - file, err := parser.ParseFile(fset, path, nil, 0) + + var mode parser.Mode + if withComments { + mode = parser.ParseComments + } else { + mode = 0 + } + file, err := parser.ParseFile(fset, path, nil, mode) if err != nil { return nil, err } return parseFile(path, file, fset, []*ast.File{file}) } -func parseFile(path string, file *ast.File, fset *token.FileSet, files []*ast.File) (*GoFile, error) { - source, err := ioutil.ReadFile(path) +func ParseSource(source string, filepath string, withComments bool) (*GoFile, error) { + fset := token.NewFileSet() + path := filepath + var mode parser.Mode + if withComments { + mode = parser.ParseComments + } else { + mode = 0 + } + file, err := parser.ParseFile(fset, path, source, mode) + if err != nil { return nil, err } + return parseFile(path, file, fset, []*ast.File{file}) +} + +func execCommand(name string, args ...string) (out string, exitCode int, err error) { + stream := &strings.Builder{} + + cmd := exec.Command(name, args...) + cmd.Stderr = stream + cmd.Stdout = stream + fmt.Printf("%v\n", strings.Join(cmd.Args, " ")) + + err = cmd.Run() + if err != nil { + var terr *exec.ExitError + if errors.As(err, &terr) { + exitCode = terr.ExitCode() + out = stream.String() + } + } + + fmt.Printf("Execution: %v\n", stream.String()) + + return +} + +func getLibrary(importUrl string) (err error, cleanup func()) { + + fmt.Printf("Importing %v\n", importUrl) + + cleanup = func() {} + + var out string + var exitCode int + + _, staterr := os.Stat("go.mod") + if os.IsNotExist(staterr) { + out, exitCode, err = execCommand("go", "mod", "init", "tempmod") + if err != nil { + err = fmt.Errorf("failed to execute go mod init command to import Go library: %v.\nError: %v. Exit Code: %v\nOutput: %v\n", importUrl, err, exitCode, out) + return + } + + cleanup = func() { + _ = os.Remove("go.mod") + _ = os.Remove("go.sum") + } + } + + out, exitCode, err = execCommand("go", "get", "-v", importUrl) + if err != nil { + err = fmt.Errorf("failed to execute go get command to import Go library: %v.\nError: %v. Exit Code: %v\nOutput: %v\n", importUrl, err, exitCode, out) + return + } + + return +} + +func isNamePublic(name string) bool { + if name == "" { + return false + } + + r := []rune(name)[0] + return unicode.IsLetter(r) && unicode.IsUpper(r) +} + +func parseFile(path string, file *ast.File, fset *token.FileSet, files []*ast.File) (*GoFile, error) { + + var err error // To import sources from vendor, we use "source" compile // https://github.com/golang/go/issues/11415#issuecomment-283445198 - conf := types.Config{Importer: importer.For("source", nil)} + conf := types.Config{Importer: &PackImporter{fset} /*importer.ForCompiler(fset, "source", nil)*/} info := &types.Info{ Types: make(map[ast.Expr]types.TypeAndValue), Defs: make(map[*ast.Ident]types.Object), Uses: make(map[*ast.Ident]types.Object), } - if _, err = conf.Check(file.Name.Name, fset, files, info); err != nil { - return nil, err + conf.IgnoreFuncBodies = true + + tries := 2 + for tries > 0 { + tries-- + if _, err = conf.Check(file.Name.Name, fset, files, info); err != nil { + + // Get package to import + startingPointString := "could not import " + + start := strings.Index(err.Error(), startingPointString) + if start > -1 && tries > 0 { + start += len(startingPointString) + end := strings.Index(err.Error()[start:], " ") + + if end > -1 { + toimport := err.Error()[start : start+end] + err, cleanup := getLibrary(toimport) + defer cleanup() + if err != nil { + return nil, err + } + + continue + } + } + + return nil, fmt.Errorf("errors type checking source file. error: %v", err) + } } goFile := &GoFile{ @@ -81,10 +209,34 @@ func parseFile(path string, file *ast.File, fset *token.FileSet, files []*ast.Fi // Specs: the Spec type stands for any of *ImportSpec, *ValueSpec, and *TypeSpec: https://golang.org/pkg/go/ast/#Spec for _, genSpec := range genDecl.Specs { + switch genSpecType := genSpec.(type) { + // A ValueSpec node represents a constant or variable declaration: https://pkg.go.dev/go/ast#ValueSpec + case *ast.ValueSpec: + + if !isNamePublic(genSpecType.Names[0].Name) { + continue + } + + valueSpec := genSpecType + + switch genDecl.Tok { + case token.CONST: + goConst := buildGoConstant(goFile, info, valueSpec) + goFile.GlobalConstants = append(goFile.GlobalConstants, goConst) + + case token.VAR: + goVar := buildGoVariable(goFile, info, valueSpec) + goFile.GlobalVariables = append(goFile.GlobalVariables, goVar) + } // TypeSpec: A TypeSpec node represents a type declaration: https://golang.org/pkg/go/ast/#TypeSpec case *ast.TypeSpec: + + if !isNamePublic(genSpecType.Name.Name) { + continue + } + typeSpec := genSpecType // typeSpec.Type: an Expr (expression) node: https://golang.org/pkg/go/ast/#Expr @@ -93,12 +245,12 @@ func parseFile(path string, file *ast.File, fset *token.FileSet, files []*ast.Fi // StructType: A StructType node represents a struct type: https://golang.org/pkg/go/ast/#StructType case (*ast.StructType): structType := typeSpecType - goStruct := buildGoStruct(source, goFile, info, typeSpec, structType) + goStruct := buildGoStruct(goFile, info, typeSpec, structType, genDecl.Doc) goFile.Structs = append(goFile.Structs, goStruct) // InterfaceType: An InterfaceType node represents an interface type. https://golang.org/pkg/go/ast/#InterfaceType case (*ast.InterfaceType): interfaceType := typeSpecType - goInterface := buildGoInterface(source, goFile, info, typeSpec, interfaceType) + goInterface := buildGoInterface(goFile, info, typeSpec, interfaceType, genDecl.Doc) goFile.Interfaces = append(goFile.Interfaces, goInterface) default: // a not-implemented typeSpec.Type.(type), ignore @@ -113,8 +265,13 @@ func parseFile(path string, file *ast.File, fset *token.FileSet, files []*ast.Fi } } case *ast.FuncDecl: + + if !isNamePublic(declType.Name.Name) { + continue + } + funcDecl := declType - goStructMethod := buildStructMethod(info, funcDecl, source) + goStructMethod := buildStructMethod(info, funcDecl, declType.Doc) goFile.StructMethods = append(goFile.StructMethods, goStructMethod) default: @@ -125,6 +282,39 @@ func parseFile(path string, file *ast.File, fset *token.FileSet, files []*ast.Fi return goFile, nil } +func buildGoVariable(_ *GoFile, info *types.Info, spec *ast.ValueSpec) *GoType { + var t *GoType + if spec.Type == nil { // untyped const + t = buildType(info, spec.Values[0]) + } else { + t = buildType(info, spec.Type) + } + + t.Name = spec.Names[0].Name + + return t +} + +func goTypeStringFromInterface(data interface{}) string { + return reflect.TypeOf(data).Name() +} + +func buildGoConstant(_ *GoFile, info *types.Info, spec *ast.ValueSpec) *GoType { + + var t *GoType + if spec.Type != nil { // untyped const + t = buildType(info, spec.Type) + } else if spec.Values != nil && len(spec.Values) > 0 { + t = buildType(info, spec.Values[0]) + } else { + t = &GoType{Type: goTypeStringFromInterface(spec.Names[0].Obj.Data)} + } + + t.Name = spec.Names[0].Name + + return t +} + func buildGoImport(spec *ast.ImportSpec, file *GoFile) *GoImport { name := "" if spec.Name != nil { @@ -143,17 +333,18 @@ func buildGoImport(spec *ast.ImportSpec, file *GoFile) *GoImport { } } -func buildGoInterface(source []byte, file *GoFile, info *types.Info, typeSpec *ast.TypeSpec, interfaceType *ast.InterfaceType) *GoInterface { +func buildGoInterface(file *GoFile, info *types.Info, typeSpec *ast.TypeSpec, interfaceType *ast.InterfaceType, cg *ast.CommentGroup) *GoInterface { goInterface := &GoInterface{ - File: file, - Name: typeSpec.Name.Name, - Methods: buildMethodList(info, interfaceType.Methods.List, source), + File: file, + Name: typeSpec.Name.Name, + Methods: buildMethodList(info, interfaceType.Methods.List), + Comments: extractComment(cg), } return goInterface } -func buildMethodList(info *types.Info, fieldList []*ast.Field, source []byte) []*GoMethod { +func buildMethodList(info *types.Info, fieldList []*ast.Field) []*GoMethod { methods := []*GoMethod{} for _, field := range fieldList { @@ -167,8 +358,8 @@ func buildMethodList(info *types.Info, fieldList []*ast.Field, source []byte) [] goMethod := &GoMethod{ Name: name, - Params: buildTypeList(info, fType.Params, source), - Results: buildTypeList(info, fType.Results, source), + Params: buildTypeList(info, fType.Params), + Results: buildTypeList(info, fType.Results), } methods = append(methods, goMethod) @@ -177,35 +368,36 @@ func buildMethodList(info *types.Info, fieldList []*ast.Field, source []byte) [] return methods } -func buildStructMethod(info *types.Info, funcDecl *ast.FuncDecl, source []byte) *GoStructMethod { +func buildStructMethod(info *types.Info, funcDecl *ast.FuncDecl, cg *ast.CommentGroup) *GoStructMethod { return &GoStructMethod{ - Receivers: buildReceiverList(info, funcDecl.Recv, source), + Receivers: buildReceiverList(info, funcDecl.Recv), GoMethod: GoMethod{ - Name: funcDecl.Name.Name, - Params: buildTypeList(info, funcDecl.Type.Params, source), - Results: buildTypeList(info, funcDecl.Type.Results, source), + Name: funcDecl.Name.Name, + Params: buildTypeList(info, funcDecl.Type.Params), + Results: buildTypeList(info, funcDecl.Type.Results), + Comments: extractComment(cg), }, } } -func buildReceiverList(info *types.Info, fieldList *ast.FieldList, source []byte) []string { +func buildReceiverList(info *types.Info, fieldList *ast.FieldList) []string { receivers := []string{} if fieldList != nil { for _, t := range fieldList.List { - receivers = append(receivers, getTypeString(t.Type, source)) + receivers = append(receivers, getTypeString(info, t.Type)) } } return receivers } -func buildTypeList(info *types.Info, fieldList *ast.FieldList, source []byte) []*GoType { +func buildTypeList(info *types.Info, fieldList *ast.FieldList) []*GoType { types := []*GoType{} if fieldList != nil { for _, t := range fieldList.List { - goType := buildType(info, t.Type, source) + goType := buildType(info, t.Type) for _, n := range getNames(t) { copyType := copyType(goType) @@ -231,13 +423,103 @@ func getNames(field *ast.Field) []string { return result } -func getTypeString(expr ast.Expr, source []byte) string { - return string(source[expr.Pos()-1 : expr.End()-1]) +func getTypeString(info *types.Info, expr ast.Expr) string { + + if expr == nil { + return "" + } + + if typeInfo := info.TypeOf(expr); typeInfo != nil { + return typeInfo.String() + } + + panic("info.TypeOf failed to extract type from expression") + +} + +func typesBasicToString(basic *types.Basic) string { + switch basic.Kind() { + case types.Bool: + return "bool" + case types.Int: + return "int" + case types.Int8: + return "int8" + case types.Int16: + return "int16" + case types.Int32: + return "int32" + case types.Int64: + return "int64" + case types.Uint: + return "uint" + case types.Uint8: + return "uint8" + case types.Uint16: + return "uint16" + case types.Uint32: + return "uint32" + case types.Uint64: + return "uint64" + case types.Uintptr: + return "uint64" + case types.Float32: + return "float32" + case types.Float64: + return "float64" + case types.Complex64: + return "complex64" + case types.Complex128: + return "complex128" + case types.String: + return "string" + case types.UnsafePointer: + return "uint64" + + // types for untyped values + case types.UntypedBool: + return "bool" + case types.UntypedInt: + return "int" + case types.UntypedRune: + return "int32" + case types.UntypedFloat: + return "float64" + case types.UntypedComplex: + return "complex128" + case types.UntypedString: + return "string" + case types.UntypedNil: + return "" + + default: + return "" + } } func getUnderlyingTypeString(info *types.Info, expr ast.Expr) string { if typeInfo := info.TypeOf(expr); typeInfo != nil { if underlying := typeInfo.Underlying(); underlying != nil { + if _, ok := underlying.(*types.Interface); ok { + return typeInfo.String() + } + + if _, ok := underlying.(*types.Slice); ok { + if e, ok := underlying.(*types.Slice).Elem().(*types.Basic); ok { + str := typesBasicToString(e) + if str != "" { + return "[]" + str + } + } + } + + if e, ok := underlying.(*types.Basic); ok { + str := typesBasicToString(e) + if str != "" { + return str + } + } + return underlying.String() } } @@ -254,30 +536,32 @@ func copyType(goType *GoType) *GoType { } } -func buildType(info *types.Info, expr ast.Expr, source []byte) *GoType { +func buildType(info *types.Info, expr ast.Expr) *GoType { innerTypes := []*GoType{} - typeString := getTypeString(expr, source) + typeString := getTypeString(info, expr) underlyingString := getUnderlyingTypeString(info, expr) switch specType := expr.(type) { case *ast.FuncType: - innerTypes = append(innerTypes, buildTypeList(info, specType.Params, source)...) - innerTypes = append(innerTypes, buildTypeList(info, specType.Results, source)...) + innerTypes = append(innerTypes, buildTypeList(info, specType.Params)...) + innerTypes = append(innerTypes, buildTypeList(info, specType.Results)...) case *ast.ArrayType: - innerTypes = append(innerTypes, buildType(info, specType.Elt, source)) + innerTypes = append(innerTypes, buildType(info, specType.Elt)) case *ast.StructType: - innerTypes = append(innerTypes, buildTypeList(info, specType.Fields, source)...) + innerTypes = append(innerTypes, buildTypeList(info, specType.Fields)...) case *ast.MapType: - innerTypes = append(innerTypes, buildType(info, specType.Key, source)) - innerTypes = append(innerTypes, buildType(info, specType.Value, source)) + innerTypes = append(innerTypes, buildType(info, specType.Key)) + innerTypes = append(innerTypes, buildType(info, specType.Value)) case *ast.ChanType: - innerTypes = append(innerTypes, buildType(info, specType.Value, source)) + innerTypes = append(innerTypes, buildType(info, specType.Value)) case *ast.StarExpr: - innerTypes = append(innerTypes, buildType(info, specType.X, source)) + innerTypes = append(innerTypes, buildType(info, specType.X)) case *ast.Ellipsis: - innerTypes = append(innerTypes, buildType(info, specType.Elt, source)) + typeString = strings.ReplaceAll(typeString, "[]", "...") + underlyingString = strings.ReplaceAll(underlyingString, "[]", "...") + innerTypes = append(innerTypes, buildType(info, specType.Elt)) case *ast.InterfaceType: - methods := buildMethodList(info, specType.Methods.List, source) + methods := buildMethodList(info, specType.Methods.List) for _, m := range methods { innerTypes = append(innerTypes, m.Params...) innerTypes = append(innerTypes, m.Results...) @@ -285,6 +569,8 @@ func buildType(info *types.Info, expr ast.Expr, source []byte) *GoType { case *ast.Ident: case *ast.SelectorExpr: + case *ast.BasicLit: + case *ast.BinaryExpr: default: fmt.Printf("Unexpected field type: `%s`,\n %#v\n", typeString, specType) } @@ -296,11 +582,12 @@ func buildType(info *types.Info, expr ast.Expr, source []byte) *GoType { } } -func buildGoStruct(source []byte, file *GoFile, info *types.Info, typeSpec *ast.TypeSpec, structType *ast.StructType) *GoStruct { +func buildGoStruct(file *GoFile, info *types.Info, typeSpec *ast.TypeSpec, structType *ast.StructType, cg *ast.CommentGroup) *GoStruct { goStruct := &GoStruct{ - File: file, - Name: typeSpec.Name.Name, - Fields: []*GoField{}, + File: file, + Name: typeSpec.Name.Name, + Fields: []*GoField{}, + Comments: extractComment(cg), } // Field: A Field declaration list in a struct type, a method list in an interface type, @@ -310,7 +597,7 @@ func buildGoStruct(source []byte, file *GoFile, info *types.Info, typeSpec *ast. goField := &GoField{ Struct: goStruct, Name: name.String(), - Type: string(source[field.Type.Pos()-1 : field.Type.End()-1]), + Type: getTypeString(info, field.Type), } if field.Tag != nil { @@ -328,3 +615,20 @@ func buildGoStruct(source []byte, file *GoFile, info *types.Info, typeSpec *ast. return goStruct } + +func extractComment(cg *ast.CommentGroup) string { + if cg == nil || cg.List == nil { + return "" + } + + var comment string + for _, c := range cg.List { + comment += c.Text + comment = strings.ReplaceAll(comment, "//", "") + comment = strings.ReplaceAll(comment, "/*", "") + comment = strings.ReplaceAll(comment, "*/", "") + comment = strings.TrimSpace(comment) + } + + return comment +} diff --git a/types.go b/types.go index 63fba43..6ee2e4a 100644 --- a/types.go +++ b/types.go @@ -1,6 +1,7 @@ package parser import ( + "go/build" "os" "path/filepath" "reflect" @@ -8,30 +9,100 @@ import ( ) type GoFile struct { - Package string - Path string - Structs []*GoStruct - Interfaces []*GoInterface - Imports []*GoImport - StructMethods []*GoStructMethod + Package string + Path string + GlobalConstants []*GoType + GlobalVariables []*GoType + Structs []*GoStruct + Interfaces []*GoInterface + Imports []*GoImport + StructMethods []*GoStructMethod } -func (g *GoFile) ImportPath() (string, error) { - importPath, err := filepath.Abs(g.Path) +func isInGoPackages(path string) bool { + goPath := strings.Replace(build.Default.GOPATH, "\\", "/", -1) + return strings.Contains(path, goPath) +} + +func (g *GoFile) ImportPath() (importPath string, isExternalPackage bool, err error) { + isExternalPackage = false + + importPath, err = filepath.Abs(g.Path) + if err != nil { + return "", false, err + } + + if _, err = os.Stat(importPath); err != nil { + return g.Path, false, err + } + + if !isInGoPackages(importPath) { + importPath = strings.TrimSuffix(importPath, filepath.Base(importPath)) + importPath = strings.TrimSuffix(importPath, "/") + return importPath, false, nil + } + + importPath, err = filepath.Abs(g.Path) if err != nil { - return "", err + return } importPath = strings.Replace(importPath, "\\", "/", -1) - goPath := strings.Replace(os.Getenv("GOPATH"), "\\", "/", -1) + goPath := strings.Replace(build.Default.GOPATH, "\\", "/", -1) + + isExternalPackage = true + importPath = strings.TrimPrefix(importPath, goPath) importPath = strings.TrimPrefix(importPath, "/src/") + importPath = strings.TrimPrefix(importPath, "/pkg/mod/") + importPath = strings.TrimPrefix(importPath, "pkg/mod/") + + i := strings.Index(importPath, "@") + if i > 0 { + importPath = importPath[:i] + } + + if strings.HasSuffix(strings.ToLower(importPath), ".go") { + i := strings.LastIndex(importPath, "/") + if i > 0 { + importPath = importPath[:i] + } + } + + if strings.Contains(importPath, "!") { // replace "!c" to "C" + temp := "" + nextUppercase := false + for i := 0; i < len(importPath); i++ { + if importPath[i] == '!' { + nextUppercase = true + } else { + if nextUppercase { + temp += strings.ToUpper(string(importPath[i])) + nextUppercase = false + } else { + temp += string(importPath[i]) + } + } + } + importPath = temp + } - importPath = strings.TrimSuffix(importPath, filepath.Base(importPath)) importPath = strings.TrimSuffix(importPath, "/") - return importPath, nil + return + + // + //importPath = strings.Replace(importPath, "\\", "/", -1) + // + //goPath := strings.Replace(os.Getenv("GOPATH"), "\\", "/", -1) + //importPath = strings.TrimPrefix(importPath, goPath) + //importPath = strings.TrimPrefix(importPath, "/src/") + // + //importPath = strings.TrimSuffix(importPath, filepath.Base(importPath)) + //importPath = strings.TrimSuffix(importPath, "/") + // + //return importPath, false, nil } type GoImport struct { @@ -41,15 +112,17 @@ type GoImport struct { } type GoInterface struct { - File *GoFile - Name string - Methods []*GoMethod + File *GoFile + Name string + Comments string + Methods []*GoMethod } type GoMethod struct { - Name string - Params []*GoType - Results []*GoType + Name string + Params []*GoType + Comments string + Results []*GoType } type GoStructMethod struct { @@ -65,9 +138,10 @@ type GoType struct { } type GoStruct struct { - File *GoFile - Name string - Fields []*GoField + File *GoFile + Name string + Comments string + Fields []*GoField } type GoField struct { @@ -89,8 +163,10 @@ func (g *GoTag) Get(key string) string { // For an import - guess what prefix will be used // in type declarations. For examples: -// "strings" -> "strings" -// "net/http/httptest" -> "httptest" +// +// "strings" -> "strings" +// "net/http/httptest" -> "httptest" +// // Libraries where the package name does not match // will be mis-identified. func (g *GoImport) Prefix() string {