Skip to content

Commit 0c5541b

Browse files
authored
Add callgraphutil.WriteCosmograph (#28)
* Add `callgraphutil.WriteCosmograph` * Update dot_test.go
1 parent e98ee4e commit 0c5541b

File tree

3 files changed

+143
-0
lines changed

3 files changed

+143
-0
lines changed

callgraphutil/cosmograph.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package callgraphutil
2+
3+
import (
4+
"encoding/csv"
5+
"fmt"
6+
"io"
7+
8+
"golang.org/x/tools/go/callgraph"
9+
)
10+
11+
// WriteComsmograph writes the given callgraph.Graph to the given io.Writer in CSV
12+
// format, which can be used to generate a visual representation of the call
13+
// graph using Comsmograph.
14+
//
15+
// https://cosmograph.app/run/
16+
func WriteCosmograph(graph, metadata io.Writer, g *callgraph.Graph) error {
17+
graphWriter := csv.NewWriter(graph)
18+
graphWriter.Comma = ','
19+
defer graphWriter.Flush()
20+
21+
metadataWriter := csv.NewWriter(metadata)
22+
metadataWriter.Comma = ','
23+
defer metadataWriter.Flush()
24+
25+
// Write header.
26+
if err := graphWriter.Write([]string{"source", "target"}); err != nil {
27+
return fmt.Errorf("failed to write header: %w", err)
28+
}
29+
30+
// Write metadata header.
31+
if err := metadataWriter.Write([]string{"id", "pkg", "func"}); err != nil {
32+
return fmt.Errorf("failed to write metadata header: %w", err)
33+
}
34+
35+
// Write edges.
36+
for _, n := range g.Nodes {
37+
// TODO: fix this so there's not so many "shared" functions?
38+
//
39+
// It is a bit of a hack, but it works for now.
40+
var pkgPath string
41+
if n.Func.Pkg != nil {
42+
pkgPath = n.Func.Pkg.Pkg.Path()
43+
} else {
44+
pkgPath = "shared"
45+
}
46+
47+
// Write metadata.
48+
if err := metadataWriter.Write([]string{
49+
fmt.Sprintf("%d", n.ID),
50+
pkgPath,
51+
n.Func.String(),
52+
}); err != nil {
53+
return fmt.Errorf("failed to write metadata: %w", err)
54+
}
55+
56+
for _, e := range n.Out {
57+
// Write edge.
58+
if err := graphWriter.Write([]string{
59+
fmt.Sprintf("%d", n.ID),
60+
fmt.Sprintf("%d", e.Callee.ID),
61+
}); err != nil {
62+
return fmt.Errorf("failed to write edge: %w", err)
63+
}
64+
}
65+
}
66+
67+
return nil
68+
}

callgraphutil/cosmograph_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package callgraphutil_test
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"testing"
8+
9+
"github.com/picatz/taint/callgraphutil"
10+
)
11+
12+
func TestWriteCosmograph(t *testing.T) {
13+
var (
14+
ownerName = "picatz"
15+
repoName = "taint"
16+
)
17+
18+
repo, _, err := cloneGitHubRepository(context.Background(), ownerName, repoName)
19+
if err != nil {
20+
t.Fatal(err)
21+
}
22+
23+
pkgs, err := loadPackages(context.Background(), repo, "./...")
24+
if err != nil {
25+
t.Fatal(err)
26+
}
27+
28+
mainFn, srcFns, err := loadSSA(context.Background(), pkgs)
29+
if err != nil {
30+
t.Fatal(err)
31+
}
32+
33+
cg, err := loadCallGraph(context.Background(), mainFn, srcFns)
34+
if err != nil {
35+
t.Fatal(err)
36+
}
37+
38+
graphOutput, err := os.Create(fmt.Sprintf("%s.csv", repoName))
39+
if err != nil {
40+
t.Fatal(err)
41+
}
42+
defer graphOutput.Close()
43+
44+
metadataOutput, err := os.Create(fmt.Sprintf("%s-metadata.csv", repoName))
45+
if err != nil {
46+
t.Fatal(err)
47+
}
48+
defer metadataOutput.Close()
49+
50+
err = callgraphutil.WriteCosmograph(graphOutput, metadataOutput, cg)
51+
if err != nil {
52+
t.Fatal(err)
53+
}
54+
}

callgraphutil/dot_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,12 +111,33 @@ func loadSSA(ctx context.Context, pkgs []*packages.Package) (mainFn *ssa.Functio
111111
// Analyze the package.
112112
ssaProg, ssaPkgs := ssautil.Packages(pkgs, ssaBuildMode)
113113

114+
// It's possible that the ssaProg is nil?
115+
if ssaProg == nil {
116+
err = fmt.Errorf("failed to create new ssa program")
117+
return
118+
}
119+
114120
ssaProg.Build()
115121

116122
for _, pkg := range ssaPkgs {
123+
if pkg == nil {
124+
continue
125+
}
117126
pkg.Build()
118127
}
119128

129+
// Remove nil ssaPkgs by iterating over the slice of packages
130+
// and for each nil package, we append the slice up to that
131+
// index and then append the slice from the next index to the
132+
// end of the slice. This effectively removes the nil package
133+
// from the slice without having to allocate a new slice.
134+
for i := 0; i < len(ssaPkgs); i++ {
135+
if ssaPkgs[i] == nil {
136+
ssaPkgs = append(ssaPkgs[:i], ssaPkgs[i+1:]...)
137+
i--
138+
}
139+
}
140+
120141
mainPkgs := ssautil.MainPackages(ssaPkgs)
121142

122143
mainFn = mainPkgs[0].Members["main"].(*ssa.Function)

0 commit comments

Comments
 (0)