diff --git a/README.md b/README.md index 271179b..910c6cc 100644 --- a/README.md +++ b/README.md @@ -53,17 +53,17 @@ To install go-callvis, run: ```sh # Latest release -go install github.com/ofabry/go-callvis@latest +go install github.com/OscarBohlin/go-callvis@latest # Development version -go install github.com/ofabry/go-callvis@master +go install github.com/OscarBohlin/go-callvis@master ``` Alternatively, clone the repository and compile the source code: ```sh # Clone repository -git clone https://github.com/ofabry/go-callvis.git +git clone https://github.com/OscarBohlin/go-callvis.git cd go-callvis # Compile and install @@ -129,7 +129,7 @@ Usage of go-callvis: -tests Include test code. -algo string - Use specific algorithm for package analyzer: static, cha or rta (default "static") + Use specific algorithm for package analyzer: static, cha, rta or vta (default "static") -version Show version and exit. ``` @@ -142,31 +142,31 @@ Here you can find descriptions for various types of output. ### Packages / Types -|Represents | Style| -|----------: | :-------------| -|`focused` | **blue** color| -|`stdlib` | **green** color| -|`other` | **yellow** color| +| Represents | Style | +| ---------: | :--------------- | +| `focused` | **blue** color | +| `stdlib` | **green** color | +| `other` | **yellow** color | ### Functions / Methods -|Represents | Style| -|-----------: | :--------------| -|`exported` | **bold** border| -|`unexported` | **normal** border| -|`anonymous` | **dotted** border| +| Represents | Style | +| -----------: | :---------------- | +| `exported` | **bold** border | +| `unexported` | **normal** border | +| `anonymous` | **dotted** border | ### Calls -|Represents | Style| -|-----------: | :-------------| -|`internal` | **black** color| -|`external` | **brown** color| -|`static` | **solid** line| -|`dynamic` | **dashed** line| -|`regular` | **simple** arrow| -|`concurrent` | arrow with **circle**| -|`deferred` | arrow with **diamond**| +| Represents | Style | +| -----------: | :--------------------- | +| `internal` | **black** color | +| `external` | **brown** color | +| `static` | **solid** line | +| `dynamic` | **dashed** line | +| `regular` | **simple** arrow | +| `concurrent` | arrow with **circle** | +| `deferred` | arrow with **diamond** | ## Examples diff --git a/analysis.go b/analysis.go index 86ad102..6df6994 100644 --- a/analysis.go +++ b/analysis.go @@ -16,6 +16,7 @@ import ( "golang.org/x/tools/go/callgraph" "golang.org/x/tools/go/callgraph/cha" "golang.org/x/tools/go/callgraph/rta" + "golang.org/x/tools/go/callgraph/vta" "golang.org/x/tools/go/callgraph/static" "golang.org/x/tools/go/packages" @@ -29,6 +30,7 @@ const ( CallGraphTypeStatic CallGraphType = "static" CallGraphTypeCha CallGraphType = "cha" CallGraphTypeRta CallGraphType = "rta" + CallGraphTypeVta CallGraphType = "vta" ) // ==[ type def/func: analysis ]=============================================== @@ -87,6 +89,7 @@ type analysis struct { pkgs []*ssa.Package mainPkg *ssa.Package callgraph *callgraph.Graph + imports []*packages.Package } var Analysis *analysis @@ -117,6 +120,14 @@ func (a *analysis) DoAnalysis( return fmt.Errorf("packages contain errors") } + if *importFlag != "" { + for _, p := range initial { + for _, i := range p.Imports { + a.imports = append(a.imports, i) + } + } + } + logf("loaded %d initial packages, building program", len(initial)) // Create and build SSA-form program representation. @@ -134,6 +145,8 @@ func (a *analysis) DoAnalysis( graph = static.CallGraph(prog) case CallGraphTypeCha: graph = cha.CallGraph(prog) + case CallGraphTypeVta: + fallthrough case CallGraphTypeRta: mains, err := mainPackages(prog.AllPackages()) if err != nil { @@ -152,8 +165,15 @@ func (a *analysis) DoAnalysis( for _, init := range inits { roots = append(roots, init) } - graph = rta.Analyze(roots, true).CallGraph + + if algo == CallGraphTypeVta { + funcs := make(map[*ssa.Function]bool) + for fun := range graph.Nodes { + funcs[fun] = true + } + graph = vta.CallGraph(funcs, graph) + } default: return fmt.Errorf("invalid call graph type: %s", a.opts.algo) } diff --git a/examples/main/main.go b/examples/main/main.go index 2e7457e..86a854c 100644 --- a/examples/main/main.go +++ b/examples/main/main.go @@ -1,7 +1,7 @@ package main import ( - "github.com/ofabry/go-callvis/examples/main/mypkg" + "github.com/OscarBohlin/go-callvis/examples/main/mypkg" ) func main() { diff --git a/go.mod b/go.mod index 55e664b..ea138fd 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/ofabry/go-callvis +module github.com/OscarBohlin/go-callvis go 1.22.0 diff --git a/main.go b/main.go index 92fe911..5ed5b68 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( + "encoding/csv" "flag" "fmt" "go/build" @@ -14,6 +15,8 @@ import ( "github.com/pkg/browser" "golang.org/x/tools/go/buildutil" + "golang.org/x/tools/go/callgraph" + "golang.org/x/tools/go/ssa" ) const Usage = `go-callvis: visualize call graph of a Go program. @@ -42,9 +45,10 @@ var ( skipBrowser = flag.Bool("skipbrowser", false, "Skip opening browser.") outputFile = flag.String("file", "", "output filename - omit to use server mode") outputFormat = flag.String("format", "svg", "output file format [svg | png | jpg | ...]") + importFlag = flag.String("exportImport", "", "Writes the imports found in provided name or import path to specified file.") cacheDir = flag.String("cacheDir", "", "Enable caching to avoid unnecessary re-rendering, you can force rendering by adding 'refresh=true' to the URL query or emptying the cache directory") - callgraphAlgo = flag.String("algo", string(CallGraphTypeStatic), fmt.Sprintf("The algorithm used to construct the call graph. Possible values inlcude: %q, %q, %q", - CallGraphTypeStatic, CallGraphTypeCha, CallGraphTypeRta)) + callgraphAlgo = flag.String("algo", string(CallGraphTypeStatic), fmt.Sprintf("The algorithm used to construct the call graph. Possible values inlcude: %q, %q, %q, %q", + CallGraphTypeStatic, CallGraphTypeCha, CallGraphTypeRta, CallGraphTypeVta)) debugFlag = flag.Bool("debug", false, "Enable verbose log.") versionFlag = flag.Bool("version", false, "Show version and exit.") @@ -110,12 +114,61 @@ func outputDot(fname string, outputFormat string) { log.Printf("converting dot to %s\n", outputFormat) + if outputFormat == "csv" { + dotToCsv(fmt.Sprintf("%v.csv", fname)) + return + } + _, err = dotToImage(fname, outputFormat, output) if err != nil { log.Fatalf("%v\n", err) } } +func writeCsv(writer *csv.Writer, nodesMap map[*ssa.Function]*callgraph.Node) { + for fun, node := range nodesMap { + if node.Out == nil { + continue + } + + line := []string{fun.String()} + + for _, edge := range node.Out { + line = append(line, edge.Callee.Func.String()) + } + err := writer.Write(line) + if err != nil { + log.Fatalf("error writing csv: %v\n", err) + } + } +} + +//Creates a adjacency list in csv format for all nodes and edges +func dotToCsv(fname string) (error) { + f, err := os.Create(fname) + if err != nil { + log.Fatalf("could not create file %v: %v\n", fname, err) + } + + writer := csv.NewWriter(f) + writeCsv(writer, Analysis.callgraph.Nodes) + writer.Flush() + f.Close() + return nil +} + +func outputImports(fname string) { + output := "" + for _, p := range Analysis.imports { + output += fmt.Sprintf("%v\n", p.String()) + } + bytes := []byte(output) + err := os.WriteFile(fname, bytes, 0755) + if err != nil { + log.Fatalf("%v\n", err) + } +} + //noinspection GoUnhandledErrorResult func main() { flag.Parse() @@ -144,6 +197,10 @@ func main() { log.Fatal(err) } + if *importFlag != "" { + outputImports(*importFlag) + } + http.HandleFunc("/", handler) if *outputFile == "" {