diff --git a/lang/golang/parser/parser.go b/lang/golang/parser/parser.go index 6973bbb..016300a 100644 --- a/lang/golang/parser/parser.go +++ b/lang/golang/parser/parser.go @@ -17,6 +17,7 @@ package parser import ( "bufio" "bytes" + "encoding/json" "fmt" "go/ast" "go/parser" @@ -94,12 +95,11 @@ func newGoParser(name string, homePageDir string, opts Options) *GoParser { } func (p *GoParser) collectGoMods(startDir string) error { - err := filepath.Walk(startDir, func(path string, info fs.FileInfo, err error) error { if err != nil || !strings.HasSuffix(path, "go.mod") { return nil } - name, content, err := getModuleName(path) + name, _, err := getModuleName(path) if err != nil { return err } @@ -109,7 +109,8 @@ func (p *GoParser) collectGoMods(startDir string) error { } p.repo.Modules[name] = newModule(name, rel) p.modules = append(p.modules, newModuleInfo(name, rel, name)) - deps, err := parseModuleFile(content) + + deps, err := getDeps(filepath.Dir(path)) if err != nil { return err } @@ -126,6 +127,75 @@ func (p *GoParser) collectGoMods(startDir string) error { return nil } +type replace struct { + Path string `json:"Path"` + Version string `json:"Version"` + Dir string `json:"Dir"` + GoMod string `json:"GoMod"` +} + +type dep struct { + Module struct { + Path string `json:"Path"` + Version string `json:"Version"` + Replace *replace `json:"Replace,omitempty"` + Indirect bool `json:"Indirect"` + Dir string `json:"Dir"` + GoMod string `json:"GoMod"` + } `json:"Module"` +} + +func getDeps(dir string) (map[string]string, error) { + // run go mod tidy first to ensure all dependencies are resolved + cmd := exec.Command("go", "mod", "tidy", "-e") + cmd.Dir = dir + output, err := cmd.CombinedOutput() + if err != nil { + return nil, fmt.Errorf("failed to execute 'go mod tidy', err: %v, output: %s", err, string(output)) + } + + cmd = exec.Command("go", "list", "-json", "all") + cmd.Dir = dir + output, err = cmd.CombinedOutput() + if err != nil { + return nil, fmt.Errorf("failed to execute 'go list -json all', err: %v, output: %s", err, string(output)) + } + + deps := make(map[string]string) + decoder := json.NewDecoder(bytes.NewReader(output)) + for { + var mod dep + if err := decoder.Decode(&mod); err != nil { + if err.Error() == "EOF" { + break + } + return nil, fmt.Errorf("failed to decode json: %v", err) + } + module := mod.Module + // golang internal package, ignore it. + if module.Path == "" { + continue + } + if module.Replace != nil { + deps[module.Path] = module.Replace.Path + "@" + module.Replace.Version + } else { + if module.Version != "" { + deps[module.Path] = module.Path + "@" + module.Version + } else { + // If no version, it's a local package. So we use local commit as version + commit, err := getCommitHash(dir) + if err != nil { + deps[module.Path] = module.Path + } else { + deps[module.Path] = module.Path + "@" + commit + } + } + } + } + + return deps, nil +} + // ParseRepo parse the entiry repo from homePageDir recursively until end func (p *GoParser) ParseRepo() (Repository, error) { for _, lib := range p.modules { diff --git a/lang/golang/parser/utils.go b/lang/golang/parser/utils.go index 87edce8..0f9ad39 100644 --- a/lang/golang/parser/utils.go +++ b/lang/golang/parser/utils.go @@ -24,6 +24,7 @@ import ( "go/types" "io" "os" + "os/exec" "path" "regexp" "strings" @@ -31,7 +32,6 @@ import ( "github.com/Knetic/govaluate" . "github.com/cloudwego/abcoder/lang/uniast" - "golang.org/x/mod/modfile" ) func shouldIgnoreDir(path string) bool { @@ -181,26 +181,6 @@ func getModuleName(modFilePath string) (string, []byte, error) { return "", data, nil } -// parse go.mod and get a map of module name to module_path@version -func parseModuleFile(data []byte) (map[string]string, error) { - ast, err := modfile.Parse("go.mod", data, nil) - if err != nil { - return nil, fmt.Errorf("failed to parse go.mod file: %v", err) - } - modules := make(map[string]string) - for _, req := range ast.Require { - // if req.Indirect { - // continue - // } - modules[req.Mod.Path] = req.Mod.Path + "@" + req.Mod.Version - } - // replaces - for _, replace := range ast.Replace { - modules[replace.Old.Path] = replace.New.Path + "@" + replace.New.Version - } - return modules, nil -} - func isGoBuiltins(name string) bool { switch name { case "append", "cap", "close", "complex", "copy", "delete", "imag", "len", "make", "new", "panic", "print", "println", "real", "recover": @@ -337,3 +317,13 @@ func newIdentity(mod, pkg, name string) Identity { func isUpperCase(c byte) bool { return c >= 'A' && c <= 'Z' } + +func getCommitHash(dir string) (string, error) { + cmd := exec.Command("git", "rev-parse", "HEAD") + cmd.Dir = dir + output, err := cmd.Output() + if err != nil { + return "", fmt.Errorf("failed to get commit hash: %v", err) + } + return strings.TrimSpace(string(output)), nil +}