Skip to content

Commit 27cc2cf

Browse files
authored
Add callgraphutil.WriteCSV (#29)
1 parent 0c5541b commit 27cc2cf

File tree

3 files changed

+169
-20
lines changed

3 files changed

+169
-20
lines changed

callgraphutil/csv.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package callgraphutil
2+
3+
import (
4+
"encoding/csv"
5+
"fmt"
6+
"io"
7+
"runtime"
8+
"strings"
9+
10+
"golang.org/x/tools/go/callgraph"
11+
)
12+
13+
// WriteCSV writes the given callgraph.Graph to the given io.Writer in CSV
14+
// format. This format can be used to generate a visual representation of the
15+
// call graph using many different tools.
16+
func WriteCSV(w io.Writer, g *callgraph.Graph) error {
17+
cw := csv.NewWriter(w)
18+
cw.Comma = ','
19+
defer cw.Flush()
20+
21+
// Write header.
22+
if err := cw.Write([]string{
23+
"source_pkg",
24+
"source_pkg_go_version",
25+
"source_pkg_origin",
26+
"source_func",
27+
"source_func_name",
28+
"source_func_signature",
29+
"target_pkg",
30+
"target_pkg_go_version",
31+
"target_pkg_origin",
32+
"target_func",
33+
"target_func_name",
34+
"target_func_signature",
35+
}); err != nil {
36+
return fmt.Errorf("failed to write header: %w", err)
37+
}
38+
39+
// Write edges.
40+
for _, n := range g.Nodes {
41+
source, err := getNodeInfo(n)
42+
if err != nil {
43+
return fmt.Errorf("failed to get node info: %w", err)
44+
}
45+
46+
for _, e := range n.Out {
47+
target, err := getNodeInfo(e.Callee)
48+
if err != nil {
49+
return fmt.Errorf("failed to get node info: %w", err)
50+
}
51+
52+
record := []string{}
53+
record = append(record, source.CSV()...)
54+
record = append(record, target.CSV()...)
55+
56+
// Write edge.
57+
if err := cw.Write(record); err != nil {
58+
return fmt.Errorf("failed to write edge: %w", err)
59+
}
60+
}
61+
}
62+
63+
return nil
64+
}
65+
66+
// nodeInfo is a struct that contains information about a callgraph.Node used
67+
// to generate CSV output.
68+
type nodeInfo struct {
69+
pkgPath string
70+
pkgGoVersion string
71+
pkgOrigin string
72+
pkgFunc string
73+
pkgFuncName string
74+
pkgFuncSignature string
75+
}
76+
77+
// CSV returns single record for the node.
78+
func (n *nodeInfo) CSV() []string {
79+
return []string{
80+
n.pkgPath,
81+
n.pkgGoVersion,
82+
n.pkgOrigin,
83+
n.pkgFunc,
84+
n.pkgFuncName,
85+
n.pkgFuncSignature,
86+
}
87+
}
88+
89+
// getNodeInfo returns a nodeInfo struct for the given callgraph.Node.
90+
func getNodeInfo(n *callgraph.Node) (*nodeInfo, error) {
91+
info := &nodeInfo{
92+
pkgPath: "unknown",
93+
pkgGoVersion: runtime.Version(),
94+
pkgOrigin: "unknown",
95+
pkgFunc: n.Func.String(),
96+
pkgFuncName: n.Func.Name(),
97+
pkgFuncSignature: n.Func.Signature.String(),
98+
}
99+
100+
if n.Func.Pkg != nil {
101+
info.pkgPath = n.Func.Pkg.Pkg.Path()
102+
103+
if goVersion := n.Func.Pkg.Pkg.GoVersion(); goVersion != "" {
104+
info.pkgGoVersion = goVersion
105+
}
106+
}
107+
108+
if strings.Contains(info.pkgPath, ".") {
109+
info.pkgOrigin = strings.Split(info.pkgPath, "/")[0]
110+
} else {
111+
// If the package path doesn't contain a dot, then it's
112+
// probably a standard library package? This is a pattern
113+
// I've used and seen elsewhere.
114+
info.pkgOrigin = "stdlib"
115+
}
116+
117+
return info, nil
118+
}

callgraphutil/csv_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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 TestWriteCSV(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+
fh, err := os.Create(fmt.Sprintf("%s.csv", repoName))
39+
if err != nil {
40+
t.Fatal(err)
41+
}
42+
defer fh.Close()
43+
44+
err = callgraphutil.WriteCSV(fh, cg)
45+
if err != nil {
46+
t.Fatal(err)
47+
}
48+
}

callgraphutil/dot_test.go

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"go/parser"
99
"go/token"
1010
"os"
11-
"path/filepath"
1211
"testing"
1312

1413
"github.com/go-git/go-git/v5"
@@ -24,25 +23,9 @@ func cloneGitHubRepository(ctx context.Context, ownerName, repoName string) (str
2423
ownerAndRepo := ownerName + "/" + repoName
2524

2625
// Get the directory path.
27-
dir := filepath.Join(os.TempDir(), "taint", "github", ownerAndRepo)
28-
29-
// Check if the directory exists.
30-
_, err := os.Stat(dir)
31-
if err == nil {
32-
// If the directory exists, we'll assume it's a valid repository,
33-
// and return the directory. Open the directory to
34-
repo, err := git.PlainOpen(dir)
35-
if err != nil {
36-
return dir, "", fmt.Errorf("%w", err)
37-
}
38-
39-
// Get the repository's HEAD.
40-
head, err := repo.Head()
41-
if err != nil {
42-
return dir, "", fmt.Errorf("%w", err)
43-
}
44-
45-
return dir, head.Hash().String(), nil
26+
dir, err := os.MkdirTemp(os.TempDir(), fmt.Sprintf("callgraphutil_csv-%s-%s", ownerName, repoName))
27+
if err != nil {
28+
return "", "", fmt.Errorf("failed to create temp dir: %w", err)
4629
}
4730

4831
// Clone the repository.

0 commit comments

Comments
 (0)