diff --git a/internal/golang/golang.go b/internal/golang/golang.go index 05c8704..54ec3ad 100644 --- a/internal/golang/golang.go +++ b/internal/golang/golang.go @@ -12,6 +12,9 @@ func init() { // go.sum - lockfile core.Register("golang", core.Lockfile, &goSumParser{}, core.ExactMatch("go.sum")) + + // go.graph - lockfile (go mod graph output) + core.Register("golang", core.Lockfile, &goGraphParser{}, core.ExactMatch("go.graph")) } // goModParser parses go.mod files. @@ -145,3 +148,76 @@ func (p *goSumParser) Parse(filename string, content []byte) ([]core.Dependency, return deps, nil } + +// goGraphParser parses go.graph files (go mod graph output). +type goGraphParser struct{} + +func (p *goGraphParser) Parse(filename string, content []byte) ([]core.Dependency, error) { + var deps []core.Dependency + seen := make(map[string]bool) + directDeps := make(map[string]bool) + lines := strings.Split(string(content), "\n") + + // First pass: identify direct dependencies (those required by the main module) + // The main module appears without a version in the first column + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + + parts := strings.Fields(line) + if len(parts) != 2 { + continue + } + + parent := parts[0] + dep := parts[1] + + // If parent has no @version, it's the main module + if !strings.Contains(parent, "@") { + // Extract just the name from dep (before @) + if idx := strings.LastIndex(dep, "@"); idx > 0 { + directDeps[dep[:idx]] = true + } + } + } + + // Second pass: collect all dependencies + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + + parts := strings.Fields(line) + if len(parts) != 2 { + continue + } + + dep := parts[1] + + // Parse name@version + idx := strings.LastIndex(dep, "@") + if idx <= 0 { + continue + } + + name := dep[:idx] + version := dep[idx+1:] + + if seen[name] { + continue + } + seen[name] = true + + deps = append(deps, core.Dependency{ + Name: name, + Version: version, + Scope: core.Runtime, + Direct: directDeps[name], + }) + } + + return deps, nil +} diff --git a/internal/golang/golang_test.go b/internal/golang/golang_test.go index 4aaa531..d568ee4 100644 --- a/internal/golang/golang_test.go +++ b/internal/golang/golang_test.go @@ -574,3 +574,76 @@ func TestGodepsText(t *testing.T) { } } } + +func TestGoGraph(t *testing.T) { + content, err := os.ReadFile("../../testdata/golang/go.graph") + if err != nil { + t.Fatalf("failed to read fixture: %v", err) + } + + parser := &goGraphParser{} + deps, err := parser.Parse("go.graph", content) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + // Should have 8 unique dependencies + if len(deps) != 8 { + t.Fatalf("expected 8 dependencies, got %d", len(deps)) + } + + depMap := make(map[string]core.Dependency) + for _, d := range deps { + depMap[d.Name] = d + } + + // Verify direct dependencies (from main module) + directDeps := []struct { + name string + version string + }{ + {"golang.org/x/text", "v0.14.0"}, + {"github.com/google/uuid", "v1.4.0"}, + {"github.com/stretchr/testify", "v1.8.4"}, + } + + for _, exp := range directDeps { + dep, ok := depMap[exp.name] + if !ok { + t.Errorf("expected %s dependency", exp.name) + continue + } + if dep.Version != exp.version { + t.Errorf("%s version = %q, want %q", exp.name, dep.Version, exp.version) + } + if !dep.Direct { + t.Errorf("%s should be direct dependency", exp.name) + } + } + + // Verify transitive dependencies + transitiveDeps := []struct { + name string + version string + }{ + {"golang.org/x/tools", "v0.0.0-20180917221912-90fa682c2a6e"}, + {"github.com/davecgh/go-spew", "v1.1.1"}, + {"github.com/pmezard/go-difflib", "v1.0.0"}, + {"github.com/stretchr/objx", "v0.5.0"}, + {"gopkg.in/yaml.v3", "v3.0.1"}, + } + + for _, exp := range transitiveDeps { + dep, ok := depMap[exp.name] + if !ok { + t.Errorf("expected %s dependency", exp.name) + continue + } + if dep.Version != exp.version { + t.Errorf("%s version = %q, want %q", exp.name, dep.Version, exp.version) + } + if dep.Direct { + t.Errorf("%s should be transitive (indirect) dependency", exp.name) + } + } +} diff --git a/testdata/golang/go.graph b/testdata/golang/go.graph new file mode 100644 index 0000000..4bfd1a6 --- /dev/null +++ b/testdata/golang/go.graph @@ -0,0 +1,8 @@ +example.com/myproject golang.org/x/text@v0.14.0 +example.com/myproject github.com/google/uuid@v1.4.0 +example.com/myproject github.com/stretchr/testify@v1.8.4 +golang.org/x/text@v0.14.0 golang.org/x/tools@v0.0.0-20180917221912-90fa682c2a6e +github.com/stretchr/testify@v1.8.4 github.com/davecgh/go-spew@v1.1.1 +github.com/stretchr/testify@v1.8.4 github.com/pmezard/go-difflib@v1.0.0 +github.com/stretchr/testify@v1.8.4 github.com/stretchr/objx@v0.5.0 +github.com/stretchr/testify@v1.8.4 gopkg.in/yaml.v3@v3.0.1