From 25ddacda2a53ba3e044273a25864086397ee05c8 Mon Sep 17 00:00:00 2001 From: Ondrej Fabry Date: Wed, 1 Jan 2025 18:03:05 +0100 Subject: [PATCH] Fix issues with latest Go and generics (#202) * Updated dependencies; removed `pointer` analyzer as it panics in go 1.22; updated docs * Updated pipeline * Alwaysa use InstantiateGenerics mode * Improve margin when using go graphviz for rendering * Fix detecting std packages * Update deps with fixes for go callgraph analysis * Handle synthetic function detection where callee pkg is nil * Add logging for analysis process Signed-off-by: Ondrej Fabry * Update CI workflow Signed-off-by: Ondrej Fabry --------- Signed-off-by: Ondrej Fabry Co-authored-by: Egor Aristov --- .github/workflows/ci.yml | 9 +- .gitignore | 3 + Makefile | 2 +- README.md | 2 + analysis.go | 63 +++++++------- dot.go | 176 +++++++++++++++++++-------------------- dot_cgo.go | 6 +- go.mod | 17 ++-- go.sum | 64 +++++--------- handler.go | 8 +- main.go | 12 ++- output.go | 46 ++++++---- 12 files changed, 200 insertions(+), 208 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 18b7b73..cc92e94 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,11 +2,10 @@ name: CI on: push: - branches: [ master ] pull_request: branches: [ master ] schedule: - - cron: '0 */12 * * *' + - cron: '0 3 * * *' jobs: build-test: @@ -15,14 +14,14 @@ jobs: strategy: fail-fast: false matrix: - go: [ '1.20', '1.19' ] + go: [ 'stable', 'oldstable', '1.21' ] steps: - name: "Checkout" - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: "Setup Go ${{ matrix.go }}" - uses: actions/setup-go@v3 + uses: actions/setup-go@v5 with: go-version: ${{ matrix.go }} diff --git a/.gitignore b/.gitignore index f4e7b10..4e82328 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,6 @@ _testmain.go # Builds /.build/ go-callvis + +# IDE +/.idea/ diff --git a/Makefile b/Makefile index 5240250..d81824e 100644 --- a/Makefile +++ b/Makefile @@ -38,7 +38,7 @@ help: @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' build: ## Build go-callvis - go build -tags $(GO_BUILD_TAGS) -ldflags "$(GO_LDFLAGS)" $(GO_BUILD_ARGS) + go build -v -tags $(GO_BUILD_TAGS) -ldflags "$(GO_LDFLAGS)" $(GO_BUILD_ARGS) test: ## Run unit tests go test -tags $(GO_BUILD_TAGS) -ldflags "$(GO_LDFLAGS)" $(GO_BUILD_ARGS) -short -race ./... diff --git a/README.md b/README.md index d8c7068..be41df9 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,8 @@ Usage of go-callvis: a list of build tags to consider satisfied during the build. For more information about build tags, see the description of build constraints in the documentation for the go/build package -tests Include test code. + -algo string + Use specific algorithm for package analyzer: static, cha or rta (default "static") -version Show version and exit. ``` diff --git a/analysis.go b/analysis.go index 66d4b08..86ad102 100644 --- a/analysis.go +++ b/analysis.go @@ -5,19 +5,20 @@ import ( "fmt" "go/build" "go/types" - "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/static" "io" "log" "net/http" "os" "path/filepath" "strings" + "time" + + "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/static" "golang.org/x/tools/go/packages" - "golang.org/x/tools/go/pointer" "golang.org/x/tools/go/ssa" "golang.org/x/tools/go/ssa/ssautil" ) @@ -25,13 +26,12 @@ import ( type CallGraphType string const ( - CallGraphTypeStatic CallGraphType = "static" - CallGraphTypeCha = "cha" - CallGraphTypeRta = "rta" - CallGraphTypePointer = "pointer" + CallGraphTypeStatic CallGraphType = "static" + CallGraphTypeCha CallGraphType = "cha" + CallGraphTypeRta CallGraphType = "rta" ) -//==[ type def/func: analysis ]=============================================== +// ==[ type def/func: analysis ]=============================================== type renderOpts struct { cacheDir string focus string @@ -80,7 +80,7 @@ func initFuncs(pkgs []*ssa.Package) ([]*ssa.Function, error) { return inits, nil } -//==[ type def/func: analysis ]=============================================== +// ==[ type def/func: analysis ]=============================================== type analysis struct { opts *renderOpts prog *ssa.Program @@ -97,6 +97,9 @@ func (a *analysis) DoAnalysis( tests bool, args []string, ) error { + logf("begin analysis") + defer logf("analysis done") + cfg := &packages.Config{ Mode: packages.LoadAllSyntax, Tests: tests, @@ -104,23 +107,25 @@ func (a *analysis) DoAnalysis( BuildFlags: getBuildFlags(), } + logf("loading packages") + initial, err := packages.Load(cfg, args...) if err != nil { return err } - if packages.PrintErrors(initial) > 0 { return fmt.Errorf("packages contain errors") } + logf("loaded %d initial packages, building program", len(initial)) + // Create and build SSA-form program representation. - mode := ssa.BuilderMode(0) - if algo == CallGraphTypeRta { - mode = ssa.InstantiateGenerics - } + mode := ssa.InstantiateGenerics prog, pkgs := ssautil.AllPackages(initial, mode) prog.Build() + logf("build done, computing callgraph (algo: %v)", algo) + var graph *callgraph.Graph var mainPkg *ssa.Package @@ -139,7 +144,7 @@ func (a *analysis) DoAnalysis( for _, main := range mains { roots = append(roots, main.Func("main")) } - + inits, err := initFuncs(prog.AllPackages()) if err != nil { return err @@ -149,26 +154,11 @@ func (a *analysis) DoAnalysis( } graph = rta.Analyze(roots, true).CallGraph - case CallGraphTypePointer: - mains, err := mainPackages(prog.AllPackages()) - if err != nil { - return err - } - mainPkg = mains[0] - config := &pointer.Config{ - Mains: mains, - BuildCallGraph: true, - } - ptares, err := pointer.Analyze(config) - if err != nil { - return err - } - graph = ptares.CallGraph default: return fmt.Errorf("invalid call graph type: %s", a.opts.algo) } - //cg.DeleteSyntheticNodes() + logf("callgraph resolved with %d nodes", len(graph.Nodes)) a.prog = prog a.pkgs = pkgs @@ -276,6 +266,9 @@ func (a *analysis) Render() ([]byte, error) { focusPkg *types.Package ) + start := time.Now() + logf("begin rendering") + if a.opts.focus != "" { if ssaPkg = a.prog.ImportedPackage(a.opts.focus); ssaPkg == nil { if strings.Contains(a.opts.focus, "/") { @@ -302,7 +295,7 @@ func (a *analysis) Render() ([]byte, error) { } } focusPkg = ssaPkg.Pkg - logf("focusing: %v", focusPkg.Path()) + logf("focusing package: %v (path: %v)", focusPkg.Name(), focusPkg.Path()) } dot, err := printOutput( @@ -321,6 +314,8 @@ func (a *analysis) Render() ([]byte, error) { return nil, fmt.Errorf("processing failed: %v", err) } + logf("rendering done (took %v sec)", time.Since(start).Round(time.Millisecond).Seconds()) + return dot, nil } diff --git a/dot.go b/dot.go index 43e8da4..87145d7 100644 --- a/dot.go +++ b/dot.go @@ -1,23 +1,23 @@ package main import ( - "bytes" - "fmt" - "io" - "log" - "os" - "os/exec" - "path/filepath" - "strings" - "text/template" + "bytes" + "fmt" + "io" + "log" + "os" + "os/exec" + "path/filepath" + "strings" + "text/template" ) var ( - minlen uint - nodesep float64 - nodeshape string - nodestyle string - rankdir string + minlen uint + nodesep float64 + nodeshape string + nodestyle string + rankdir string ) const tmplCluster = `{{define "cluster" -}} @@ -52,7 +52,7 @@ const tmplGraph = `digraph gocallvis { pad="0.0"; nodesep="{{.Options.nodesep}}"; - node [shape="{{.Options.nodeshape}}" style="{{.Options.nodestyle}}" fillcolor="honeydew" fontname="Verdana" penwidth="1.0" margin="0.05,0.0"]; + node [shape="{{.Options.nodeshape}}" style="{{.Options.nodestyle}}" fillcolor="honeydew" fontname="Verdana" penwidth="1.0" margin="0.16,0.0"]; edge [minlen="{{.Options.minlen}}"] {{template "cluster" .Cluster}} @@ -63,94 +63,94 @@ const tmplGraph = `digraph gocallvis { } ` -//==[ type def/func: dotCluster ]=============================================== +// ==[ type def/func: dotCluster ]=============================================== type dotCluster struct { - ID string - Clusters map[string]*dotCluster - Nodes []*dotNode - Attrs dotAttrs + ID string + Clusters map[string]*dotCluster + Nodes []*dotNode + Attrs dotAttrs } func NewDotCluster(id string) *dotCluster { - return &dotCluster{ - ID: id, - Clusters: make(map[string]*dotCluster), - Attrs: make(dotAttrs), - } + return &dotCluster{ + ID: id, + Clusters: make(map[string]*dotCluster), + Attrs: make(dotAttrs), + } } func (c *dotCluster) String() string { - return fmt.Sprintf("cluster_%s", c.ID) + return fmt.Sprintf("cluster_%s", c.ID) } -//==[ type def/func: dotNode ]=============================================== +// ==[ type def/func: dotNode ]=============================================== type dotNode struct { - ID string - Attrs dotAttrs + ID string + Attrs dotAttrs } func (n *dotNode) String() string { - return n.ID + return n.ID } -//==[ type def/func: dotEdge ]=============================================== +// ==[ type def/func: dotEdge ]=============================================== type dotEdge struct { - From *dotNode - To *dotNode - Attrs dotAttrs + From *dotNode + To *dotNode + Attrs dotAttrs } -//==[ type def/func: dotAttrs ]=============================================== +// ==[ type def/func: dotAttrs ]=============================================== type dotAttrs map[string]string func (p dotAttrs) List() []string { - l := []string{} - for k, v := range p { - l = append(l, fmt.Sprintf("%s=%q", k, v)) - } - return l + l := []string{} + for k, v := range p { + l = append(l, fmt.Sprintf("%s=%q", k, v)) + } + return l } func (p dotAttrs) String() string { - return strings.Join(p.List(), " ") + return strings.Join(p.List(), " ") } func (p dotAttrs) Lines() string { - return fmt.Sprintf("%s;", strings.Join(p.List(), ";\n")) + return fmt.Sprintf("%s;", strings.Join(p.List(), ";\n")) } -//==[ type def/func: dotGraph ]=============================================== +// ==[ type def/func: dotGraph ]=============================================== type dotGraph struct { - Title string - Minlen uint - Attrs dotAttrs - Cluster *dotCluster - Nodes []*dotNode - Edges []*dotEdge - Options map[string]string + Title string + Minlen uint + Attrs dotAttrs + Cluster *dotCluster + Nodes []*dotNode + Edges []*dotEdge + Options map[string]string } func (g *dotGraph) WriteDot(w io.Writer) error { - t := template.New("dot") - for _, s := range []string{tmplCluster, tmplNode, tmplEdge, tmplGraph} { - if _, err := t.Parse(s); err != nil { - return err - } - } - var buf bytes.Buffer - if err := t.Execute(&buf, g); err != nil { - return err - } - _, err := buf.WriteTo(w) - return err + t := template.New("dot") + for _, s := range []string{tmplCluster, tmplNode, tmplEdge, tmplGraph} { + if _, err := t.Parse(s); err != nil { + return err + } + } + var buf bytes.Buffer + if err := t.Execute(&buf, g); err != nil { + return err + } + _, err := buf.WriteTo(w) + return err } func dotToImage(outfname string, format string, dot []byte) (string, error) { - if *graphvizFlag { - return runDotToImageCallSystemGraphviz(outfname, format, dot) - } + if *graphvizFlag { + return runDotToImageCallSystemGraphviz(outfname, format, dot) + } - return runDotToImage(outfname, format, dot) + return runDotToImage(outfname, format, dot) } // location of dot executable for converting from .dot to .svg @@ -159,26 +159,26 @@ var dotSystemBinary string // runDotToImageCallSystemGraphviz generates a SVG using the 'dot' utility, returning the filepath func runDotToImageCallSystemGraphviz(outfname string, format string, dot []byte) (string, error) { - if dotSystemBinary == "" { - dot, err := exec.LookPath("dot") - if err != nil { - log.Fatalln("unable to find program 'dot', please install it or check your PATH") - } - dotSystemBinary = dot - } - - var img string - if outfname == "" { - img = filepath.Join(os.TempDir(), fmt.Sprintf("go-callvis_export.%s", format)) - } else { - img = fmt.Sprintf("%s.%s", outfname, format) - } - cmd := exec.Command(dotSystemBinary, fmt.Sprintf("-T%s", format), "-o", img) - cmd.Stdin = bytes.NewReader(dot) - var stderr bytes.Buffer - cmd.Stderr = &stderr - if err := cmd.Run(); err != nil { - return "", fmt.Errorf("command '%v': %v\n%v", cmd, err, stderr.String()) - } - return img, nil + if dotSystemBinary == "" { + dot, err := exec.LookPath("dot") + if err != nil { + log.Fatalln("unable to find program 'dot', please install it or check your PATH") + } + dotSystemBinary = dot + } + + var img string + if outfname == "" { + img = filepath.Join(os.TempDir(), fmt.Sprintf("go-callvis_export.%s", format)) + } else { + img = fmt.Sprintf("%s.%s", outfname, format) + } + cmd := exec.Command(dotSystemBinary, fmt.Sprintf("-T%s", format), "-o", img) + cmd.Stdin = bytes.NewReader(dot) + var stderr bytes.Buffer + cmd.Stderr = &stderr + if err := cmd.Run(); err != nil { + return "", fmt.Errorf("command '%v': %v\n%v", cmd, err, stderr.String()) + } + return img, nil } diff --git a/dot_cgo.go b/dot_cgo.go index a93435e..4821bdc 100644 --- a/dot_cgo.go +++ b/dot_cgo.go @@ -20,9 +20,11 @@ func runDotToImage(outfname string, format string, dot []byte) (string, error) { } defer func() { if err := graph.Close(); err != nil { - log.Fatal(err) + log.Printf("error closing graph: %v", err) + } + if err := g.Close(); err != nil { + log.Printf("error closing graphviz: %v", err) } - g.Close() }() var img string if outfname == "" { diff --git a/go.mod b/go.mod index cafc0b3..d8784c8 100644 --- a/go.mod +++ b/go.mod @@ -1,18 +1,21 @@ module github.com/ofabry/go-callvis -go 1.19 +go 1.22.0 + +toolchain go1.23.1 require ( - github.com/goccy/go-graphviz v0.1.1 - github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 - golang.org/x/tools v0.8.0 + github.com/goccy/go-graphviz v0.1.2 + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c + golang.org/x/tools v0.28.0 ) require ( github.com/fogleman/gg v1.3.0 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/pkg/errors v0.9.1 // indirect - golang.org/x/image v0.6.0 // indirect - golang.org/x/mod v0.10.0 // indirect - golang.org/x/sys v0.7.0 // indirect + golang.org/x/image v0.15.0 // indirect + golang.org/x/mod v0.22.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.28.0 // indirect ) diff --git a/go.sum b/go.sum index 89dcb03..84bfe8b 100644 --- a/go.sum +++ b/go.sum @@ -1,53 +1,27 @@ github.com/corona10/goimagehash v1.0.2 h1:pUfB0LnsJASMPGEZLj7tGY251vF+qLGqOgEP4rUs6kA= +github.com/corona10/goimagehash v1.0.2/go.mod h1:/l9umBhvcHQXVtQO1V6Gp1yD20STawkhRnnX0D1bvVI= github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= -github.com/goccy/go-graphviz v0.1.1 h1:MGrsnzBxTyt7KG8FhHsFPDTGvF7UaQMmSa6A610DqPg= -github.com/goccy/go-graphviz v0.1.1/go.mod h1:lpnwvVDjskayq84ZxG8tGCPeZX/WxP88W+OJajh+gFk= +github.com/goccy/go-graphviz v0.1.2 h1:sWSJ6w13BCm/ZOUTHDVrdvbsxqN8yyzaFcHrH/hQ9Yg= +github.com/goccy/go-graphviz v0.1.2/go.mod h1:pMYpbAqJT10V8dzV1JN/g/wUlG/0imKPzn3ZsrchGCI= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5 h1:BvoENQQU+fZ9uukda/RzCAL/191HHwJA5b13R6diVlY= -github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= -github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= +github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/image v0.6.0 h1:bR8b5okrPI3g/gyZakLZHeWxAR8Dn5CyxXv1hLH5g/4= -golang.org/x/image v0.6.0/go.mod h1:MXLdDR43H7cDJq5GEGXEVeeNhPgi+YYEQ2pC1byI1x0= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= -golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y= -golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8= +golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= +golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= +golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= +golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= diff --git a/handler.go b/handler.go index 70975d8..948920c 100644 --- a/handler.go +++ b/handler.go @@ -25,7 +25,7 @@ func handler(w http.ResponseWriter, r *http.Request) { var img string if img = Analysis.FindCachedImg(); img != "" { - log.Println("serving file:", img) + log.Println("serving cached file:", img) http.ServeFile(w, r, img) return } @@ -38,17 +38,17 @@ func handler(w http.ResponseWriter, r *http.Request) { output, err := Analysis.Render() if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + http.Error(w, fmt.Sprintf("rendering failed: %v", err.Error()), http.StatusInternalServerError) return } if r.Form.Get("format") == "dot" { - log.Println("writing dot output..") + log.Println("writing dot output") fmt.Fprint(w, string(output)) return } - log.Printf("converting dot to %s..\n", *outputFormat) + log.Printf("converting dot to %s\n", *outputFormat) img, err = dotToImage("", *outputFormat, output) if err != nil { diff --git a/main.go b/main.go index 1e8e7db..92fe911 100644 --- a/main.go +++ b/main.go @@ -1,12 +1,10 @@ // go-callvis: a tool to help visualize the call graph of a Go program. -// package main import ( "flag" "fmt" "go/build" - "io/ioutil" "log" "net" "net/http" @@ -45,8 +43,8 @@ var ( outputFile = flag.String("file", "", "output filename - omit to use server mode") outputFormat = flag.String("format", "svg", "output file format [svg | png | jpg | ...]") 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", CallGraphTypePointer, fmt.Sprintf("The algorithm used to construct the call graph. Possible values inlcude: %q, %q, %q, %q", - CallGraphTypeStatic, CallGraphTypeCha, CallGraphTypeRta, CallGraphTypePointer)) + 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)) debugFlag = flag.Bool("debug", false, "Enable verbose log.") versionFlag = flag.Bool("version", false, "Show version and exit.") @@ -103,14 +101,14 @@ func outputDot(fname string, outputFormat string) { log.Fatalf("%v\n", err) } - log.Println("writing dot output..") + log.Println("writing dot output") - writeErr := ioutil.WriteFile(fmt.Sprintf("%s.gv", fname), output, 0755) + writeErr := os.WriteFile(fmt.Sprintf("%s.gv", fname), output, 0755) if writeErr != nil { log.Fatalf("%v\n", writeErr) } - log.Printf("converting dot to %s..\n", outputFormat) + log.Printf("converting dot to %s\n", outputFormat) _, err = dotToImage(fname, outputFormat, output) if err != nil { diff --git a/output.go b/output.go index 9e44436..579a874 100644 --- a/output.go +++ b/output.go @@ -13,12 +13,22 @@ import ( ) func isSynthetic(edge *callgraph.Edge) bool { - return edge.Caller.Func.Pkg == nil || edge.Callee.Func.Synthetic != "" + // TODO: consider handling callee.Func.Pkg == nil + // this could still generate a node for the call, might be useful + return edge.Caller.Func.Pkg == nil || edge.Callee.Func.Pkg == nil || edge.Callee.Func.Synthetic != "" } func inStd(node *callgraph.Node) bool { - pkg, _ := build.Import(node.Func.Pkg.Pkg.Path(), "", 0) - return pkg.Goroot + //pkg, _ := build.Import(node.Func.Pkg.Pkg.Path(), "", 0) + //return pkg.Goroot + return isStdPkgPath(node.Func.Pkg.Pkg.Path()) +} + +func isStdPkgPath(path string) bool { + if strings.Contains(path, ".") { + return false + } + return true } func printOutput( @@ -33,6 +43,10 @@ func printOutput( nostd, nointer bool, ) ([]byte, error) { + + logf("printing output for: %v", focusPkg) + logf("src dirs: %+v, default build context: %+v", build.Default.SrcDirs(), build.Default) + var groupType, groupPkg bool for _, g := range groupBy { switch g { @@ -78,17 +92,15 @@ func printOutput( return true } fromFocused := false - toFocused := false for _, e := range caller.In { - if !isSynthetic(e) && focusPkg != nil && - e.Caller.Func.Pkg.Pkg.Path() == focusPkg.Path() { + if !isSynthetic(e) && focusPkg != nil && e.Caller.Func.Pkg.Pkg.Path() == focusPkg.Path() { fromFocused = true break } } + toFocused := false for _, e := range callee.Out { - if !isSynthetic(e) && focusPkg != nil && - e.Callee.Func.Pkg.Pkg.Path() == focusPkg.Path() { + if !isSynthetic(e) && focusPkg != nil && e.Callee.Func.Pkg.Pkg.Path() == focusPkg.Path() { toFocused = true break } @@ -157,6 +169,8 @@ func printOutput( return nil } + //logf(" - %s -> %s (%s -> %s) %v\n", caller.Func.Pkg, callee.Func.Pkg, caller, callee, filenameCaller) + callerPkg := caller.Func.Pkg.Pkg calleePkg := callee.Func.Pkg.Pkg @@ -167,8 +181,7 @@ func printOutput( } // omit std - if nostd && - (inStd(caller) || inStd(callee)) { + if nostd && (inStd(caller) || inStd(callee)) { return nil } @@ -244,11 +257,13 @@ func printOutput( label = parts[len(parts)-1] } - pkg, _ := build.Import(node.Func.Pkg.Pkg.Path(), "", 0) + pkgPath := node.Func.Pkg.Pkg.Path() + isStdPkg := isStdPkgPath(pkgPath) + // set node color if isFocused { attrs["fillcolor"] = "lightblue" - } else if pkg.Goroot { + } else if isStdPkg { attrs["fillcolor"] = "#adedad" } else { attrs["fillcolor"] = "moccasin" @@ -275,7 +290,7 @@ func printOutput( // group by pkg if groupPkg && !isFocused { label := node.Func.Pkg.Pkg.Name() - if pkg.Goroot { + if isStdPkg { label = node.Func.Pkg.Pkg.Path() } key := node.Func.Pkg.Pkg.Path() @@ -295,7 +310,7 @@ func printOutput( "rank": "sink", }, } - if pkg.Goroot { + if isStdPkg { c.Clusters[key].Attrs["fillcolor"] = "#E0FFE1" } } @@ -323,7 +338,7 @@ func printOutput( } if isFocused { c.Clusters[key].Attrs["fillcolor"] = "lightsteelblue" - } else if pkg.Goroot { + } else if isStdPkg { c.Clusters[key].Attrs["fillcolor"] = "#c2e3c2" } } @@ -418,6 +433,7 @@ func printOutput( edges = append(edges, e) } + logf("%d/%d nodes", len(nodeMap), len(cg.Nodes)) logf("%d/%d edges", len(edges), count) title := ""