| 1 | package main |
|---|---|
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "fmt" |
| 6 | "go/build" |
| 7 | "go/types" |
| 8 | "path/filepath" |
| 9 | "strings" |
| 10 | |
| 11 | "golang.org/x/tools/go/callgraph" |
| 12 | "golang.org/x/tools/go/ssa" |
| 13 | ) |
| 14 | |
| 15 | func isSynthetic(edge *callgraph.Edge) bool { |
| 16 | return edge.Caller.Func.Pkg == nil || edge.Callee.Func.Synthetic != "" |
| 17 | } |
| 18 | |
| 19 | func inStd(node *callgraph.Node) bool { |
| 20 | pkg, _ := build.Import(node.Func.Pkg.Pkg.Path(), "", 0) |
| 21 | return pkg.Goroot |
| 22 | } |
| 23 | |
| 24 | func printOutput( |
| 25 | prog *ssa.Program, |
| 26 | mainPkg *ssa.Package, |
| 27 | cg *callgraph.Graph, |
| 28 | focusPkg *types.Package, |
| 29 | limitPaths, |
| 30 | ignorePaths, |
| 31 | includePaths []string, |
| 32 | groupBy []string, |
| 33 | nostd, |
| 34 | nointer bool, |
| 35 | ) ([]byte, error) { |
| 36 | var groupType, groupPkg bool |
| 37 | for _, g := range groupBy { |
| 38 | switch g { |
| 39 | case "pkg": |
| 40 | groupPkg = true |
| 41 | case "type": |
| 42 | groupType = true |
| 43 | } |
| 44 | } |
| 45 | |
| 46 | cluster := NewDotCluster("focus") |
| 47 | cluster.Attrs = dotAttrs{ |
| 48 | "bgcolor": "white", |
| 49 | "label": "", |
| 50 | "labelloc": "t", |
| 51 | "labeljust": "c", |
| 52 | "fontsize": "18", |
| 53 | } |
| 54 | if focusPkg != nil { |
| 55 | cluster.Attrs["bgcolor"] = "#e6ecfa" |
| 56 | cluster.Attrs["label"] = focusPkg.Name() |
| 57 | } |
| 58 | |
| 59 | var ( |
| 60 | nodes []*dotNode |
| 61 | edges []*dotEdge |
| 62 | ) |
| 63 | |
| 64 | nodeMap := make(map[string]*dotNode) |
| 65 | edgeMap := make(map[string]*dotEdge) |
| 66 | |
| 67 | cg.DeleteSyntheticNodes() |
| 68 | |
| 69 | logf("%d limit prefixes: %v", len(limitPaths), limitPaths) |
| 70 | logf("%d ignore prefixes: %v", len(ignorePaths), ignorePaths) |
| 71 | logf("%d include prefixes: %v", len(includePaths), includePaths) |
| 72 | logf("no std packages: %v", nostd) |
| 73 | |
| 74 | var isFocused = func(edge *callgraph.Edge) bool { |
| 75 | caller := edge.Caller |
| 76 | callee := edge.Callee |
| 77 | if focusPkg != nil && (caller.Func.Pkg.Pkg.Path() == focusPkg.Path() || callee.Func.Pkg.Pkg.Path() == focusPkg.Path()) { |
| 78 | return true |
| 79 | } |
| 80 | fromFocused := false |
| 81 | toFocused := false |
| 82 | for _, e := range caller.In { |
| 83 | if !isSynthetic(e) && focusPkg != nil && |
| 84 | e.Caller.Func.Pkg.Pkg.Path() == focusPkg.Path() { |
| 85 | fromFocused = true |
| 86 | break |
| 87 | } |
| 88 | } |
| 89 | for _, e := range callee.Out { |
| 90 | if !isSynthetic(e) && focusPkg != nil && |
| 91 | e.Callee.Func.Pkg.Pkg.Path() == focusPkg.Path() { |
| 92 | toFocused = true |
| 93 | break |
| 94 | } |
| 95 | } |
| 96 | if fromFocused && toFocused { |
| 97 | logf("edge semi-focus: %s", edge) |
| 98 | return true |
| 99 | } |
| 100 | return false |
| 101 | } |
| 102 | |
| 103 | var inIncludes = func(node *callgraph.Node) bool { |
| 104 | pkgPath := node.Func.Pkg.Pkg.Path() |
| 105 | for _, p := range includePaths { |
| 106 | if strings.HasPrefix(pkgPath, p) { |
| 107 | return true |
| 108 | } |
| 109 | } |
| 110 | return false |
| 111 | } |
| 112 | |
| 113 | var inLimits = func(node *callgraph.Node) bool { |
| 114 | pkgPath := node.Func.Pkg.Pkg.Path() |
| 115 | for _, p := range limitPaths { |
| 116 | if strings.HasPrefix(pkgPath, p) { |
| 117 | return true |
| 118 | } |
| 119 | } |
| 120 | return false |
| 121 | } |
| 122 | |
| 123 | var inIgnores = func(node *callgraph.Node) bool { |
| 124 | pkgPath := node.Func.Pkg.Pkg.Path() |
| 125 | for _, p := range ignorePaths { |
| 126 | if strings.HasPrefix(pkgPath, p) { |
| 127 | return true |
| 128 | } |
| 129 | } |
| 130 | return false |
| 131 | } |
| 132 | |
| 133 | var isInter = func(edge *callgraph.Edge) bool { |
| 134 | //caller := edge.Caller |
| 135 | callee := edge.Callee |
| 136 | if callee.Func.Object() != nil && !callee.Func.Object().Exported() { |
| 137 | return true |
| 138 | } |
| 139 | return false |
| 140 | } |
| 141 | |
| 142 | count := 0 |
| 143 | err := callgraph.GraphVisitEdges(cg, func(edge *callgraph.Edge) error { |
| 144 | count++ |
| 145 | |
| 146 | caller := edge.Caller |
| 147 | callee := edge.Callee |
| 148 | |
| 149 | posCaller := prog.Fset.Position(caller.Func.Pos()) |
| 150 | posCallee := prog.Fset.Position(callee.Func.Pos()) |
| 151 | posEdge := prog.Fset.Position(edge.Pos()) |
| 152 | //fileCaller := fmt.Sprintf("%s:%d", posCaller.Filename, posCaller.Line) |
| 153 | filenameCaller := filepath.Base(posCaller.Filename) |
| 154 | |
| 155 | // omit synthetic calls |
| 156 | if isSynthetic(edge) { |
| 157 | return nil |
| 158 | } |
| 159 | |
| 160 | callerPkg := caller.Func.Pkg.Pkg |
| 161 | calleePkg := callee.Func.Pkg.Pkg |
| 162 | |
| 163 | // focus specific pkg |
| 164 | if focusPkg != nil && |
| 165 | !isFocused(edge) { |
| 166 | return nil |
| 167 | } |
| 168 | |
| 169 | // omit std |
| 170 | if nostd && |
| 171 | (inStd(caller) || inStd(callee)) { |
| 172 | return nil |
| 173 | } |
| 174 | |
| 175 | // omit inter |
| 176 | if nointer && isInter(edge) { |
| 177 | return nil |
| 178 | } |
| 179 | |
| 180 | include := false |
| 181 | // include path prefixes |
| 182 | if len(includePaths) > 0 && |
| 183 | (inIncludes(caller) || inIncludes(callee)) { |
| 184 | logf("include: %s -> %s", caller, callee) |
| 185 | include = true |
| 186 | } |
| 187 | |
| 188 | if !include { |
| 189 | // limit path prefixes |
| 190 | if len(limitPaths) > 0 && |
| 191 | (!inLimits(caller) || !inLimits(callee)) { |
| 192 | logf("NOT in limit: %s -> %s", caller, callee) |
| 193 | return nil |
| 194 | } |
| 195 | |
| 196 | // ignore path prefixes |
| 197 | if len(ignorePaths) > 0 && |
| 198 | (inIgnores(caller) || inIgnores(callee)) { |
| 199 | logf("IS ignored: %s -> %s", caller, callee) |
| 200 | return nil |
| 201 | } |
| 202 | } |
| 203 | |
| 204 | //var buf bytes.Buffer |
| 205 | //data, _ := json.MarshalIndent(caller.Func, "", " ") |
| 206 | //logf("call node: %s -> %s\n %v", caller, callee, string(data)) |
| 207 | logf("call node: %s -> %s (%s -> %s) %v\n", caller.Func.Pkg, callee.Func.Pkg, caller, callee, filenameCaller) |
| 208 | |
| 209 | var sprintNode = func(node *callgraph.Node, isCaller bool) *dotNode { |
| 210 | // only once |
| 211 | key := node.Func.String() |
| 212 | nodeTooltip := "" |
| 213 | |
| 214 | fileCaller := fmt.Sprintf("%s:%d", filepath.Base(posCaller.Filename), posCaller.Line) |
| 215 | fileCallee := fmt.Sprintf("%s:%d", filepath.Base(posCallee.Filename), posCallee.Line) |
| 216 | |
| 217 | if isCaller { |
| 218 | nodeTooltip = fmt.Sprintf("%s | defined in %s", node.Func.String(), fileCaller) |
| 219 | } else { |
| 220 | nodeTooltip = fmt.Sprintf("%s | defined in %s", node.Func.String(), fileCallee) |
| 221 | } |
| 222 | |
| 223 | if n, ok := nodeMap[key]; ok { |
| 224 | return n |
| 225 | } |
| 226 | |
| 227 | // is focused |
| 228 | isFocused := focusPkg != nil && |
| 229 | node.Func.Pkg.Pkg.Path() == focusPkg.Path() |
| 230 | attrs := make(dotAttrs) |
| 231 | |
| 232 | // node label |
| 233 | label := node.Func.RelString(node.Func.Pkg.Pkg) |
| 234 | |
| 235 | // func signature |
| 236 | sign := node.Func.Signature |
| 237 | if node.Func.Parent() != nil { |
| 238 | sign = node.Func.Parent().Signature |
| 239 | } |
| 240 | |
| 241 | // omit type from label |
| 242 | if groupType && sign.Recv() != nil { |
| 243 | parts := strings.Split(label, ".") |
| 244 | label = parts[len(parts)-1] |
| 245 | } |
| 246 | |
| 247 | pkg, _ := build.Import(node.Func.Pkg.Pkg.Path(), "", 0) |
| 248 | // set node color |
| 249 | if isFocused { |
| 250 | attrs["fillcolor"] = "lightblue" |
| 251 | } else if pkg.Goroot { |
| 252 | attrs["fillcolor"] = "#adedad" |
| 253 | } else { |
| 254 | attrs["fillcolor"] = "moccasin" |
| 255 | } |
| 256 | |
| 257 | // include pkg name |
| 258 | if !groupPkg && !isFocused { |
| 259 | label = fmt.Sprintf("%s\n%s", node.Func.Pkg.Pkg.Name(), label) |
| 260 | } |
| 261 | |
| 262 | attrs["label"] = label |
| 263 | |
| 264 | // func styles |
| 265 | if node.Func.Parent() != nil { |
| 266 | attrs["style"] = "dotted,filled" |
| 267 | } else if node.Func.Object() != nil && node.Func.Object().Exported() { |
| 268 | attrs["penwidth"] = "1.5" |
| 269 | } else { |
| 270 | attrs["penwidth"] = "0.5" |
| 271 | } |
| 272 | |
| 273 | c := cluster |
| 274 | |
| 275 | // group by pkg |
| 276 | if groupPkg && !isFocused { |
| 277 | label := node.Func.Pkg.Pkg.Name() |
| 278 | if pkg.Goroot { |
| 279 | label = node.Func.Pkg.Pkg.Path() |
| 280 | } |
| 281 | key := node.Func.Pkg.Pkg.Path() |
| 282 | if _, ok := c.Clusters[key]; !ok { |
| 283 | c.Clusters[key] = &dotCluster{ |
| 284 | ID: key, |
| 285 | Clusters: make(map[string]*dotCluster), |
| 286 | Attrs: dotAttrs{ |
| 287 | "penwidth": "0.8", |
| 288 | "fontsize": "16", |
| 289 | "label": label, |
| 290 | "style": "filled", |
| 291 | "fillcolor": "lightyellow", |
| 292 | "URL": fmt.Sprintf("/?f=%s", key), |
| 293 | "fontname": "Tahoma bold", |
| 294 | "tooltip": fmt.Sprintf("package: %s", key), |
| 295 | "rank": "sink", |
| 296 | }, |
| 297 | } |
| 298 | if pkg.Goroot { |
| 299 | c.Clusters[key].Attrs["fillcolor"] = "#E0FFE1" |
| 300 | } |
| 301 | } |
| 302 | c = c.Clusters[key] |
| 303 | } |
| 304 | |
| 305 | // group by type |
| 306 | if groupType && sign.Recv() != nil { |
| 307 | label := strings.Split(node.Func.RelString(node.Func.Pkg.Pkg), ".")[0] |
| 308 | key := sign.Recv().Type().String() |
| 309 | if _, ok := c.Clusters[key]; !ok { |
| 310 | c.Clusters[key] = &dotCluster{ |
| 311 | ID: key, |
| 312 | Clusters: make(map[string]*dotCluster), |
| 313 | Attrs: dotAttrs{ |
| 314 | "penwidth": "0.5", |
| 315 | "fontsize": "15", |
| 316 | "fontcolor": "#222222", |
| 317 | "label": label, |
| 318 | "labelloc": "b", |
| 319 | "style": "rounded,filled", |
| 320 | "fillcolor": "wheat2", |
| 321 | "tooltip": fmt.Sprintf("type: %s", key), |
| 322 | }, |
| 323 | } |
| 324 | if isFocused { |
| 325 | c.Clusters[key].Attrs["fillcolor"] = "lightsteelblue" |
| 326 | } else if pkg.Goroot { |
| 327 | c.Clusters[key].Attrs["fillcolor"] = "#c2e3c2" |
| 328 | } |
| 329 | } |
| 330 | c = c.Clusters[key] |
| 331 | } |
| 332 | |
| 333 | attrs["tooltip"] = nodeTooltip |
| 334 | |
| 335 | n := &dotNode{ |
| 336 | ID: node.Func.String(), |
| 337 | Attrs: attrs, |
| 338 | } |
| 339 | |
| 340 | if c != nil { |
| 341 | c.Nodes = append(c.Nodes, n) |
| 342 | } else { |
| 343 | nodes = append(nodes, n) |
| 344 | } |
| 345 | |
| 346 | nodeMap[key] = n |
| 347 | return n |
| 348 | } |
| 349 | callerNode := sprintNode(edge.Caller, true) |
| 350 | calleeNode := sprintNode(edge.Callee, false) |
| 351 | |
| 352 | // edges |
| 353 | attrs := make(dotAttrs) |
| 354 | |
| 355 | // dynamic call |
| 356 | if edge.Site != nil && edge.Site.Common().StaticCallee() == nil { |
| 357 | attrs["style"] = "dashed" |
| 358 | } |
| 359 | |
| 360 | // go & defer calls |
| 361 | switch edge.Site.(type) { |
| 362 | case *ssa.Go: |
| 363 | attrs["arrowhead"] = "normalnoneodot" |
| 364 | case *ssa.Defer: |
| 365 | attrs["arrowhead"] = "normalnoneodiamond" |
| 366 | } |
| 367 | |
| 368 | // colorize calls outside focused pkg |
| 369 | if focusPkg != nil && |
| 370 | (calleePkg.Path() != focusPkg.Path() || callerPkg.Path() != focusPkg.Path()) { |
| 371 | attrs["color"] = "saddlebrown" |
| 372 | } |
| 373 | |
| 374 | // use position in file where callee is called as tooltip for the edge |
| 375 | fileEdge := fmt.Sprintf( |
| 376 | "at %s:%d: calling [%s]", |
| 377 | filepath.Base(posEdge.Filename), |
| 378 | posEdge.Line, |
| 379 | edge.Callee.Func.String(), |
| 380 | ) |
| 381 | |
| 382 | // omit duplicate calls, except for tooltip enhancements |
| 383 | key := fmt.Sprintf("%s = %s => %s", caller.Func, edge.Description(), callee.Func) |
| 384 | if _, ok := edgeMap[key]; !ok { |
| 385 | attrs["tooltip"] = fileEdge |
| 386 | e := &dotEdge{ |
| 387 | From: callerNode, |
| 388 | To: calleeNode, |
| 389 | Attrs: attrs, |
| 390 | } |
| 391 | edgeMap[key] = e |
| 392 | } else { |
| 393 | // make sure, tooltip is created correctly |
| 394 | if _, okk := edgeMap[key].Attrs["tooltip"]; !okk { |
| 395 | edgeMap[key].Attrs["tooltip"] = fileEdge |
| 396 | } else { |
| 397 | edgeMap[key].Attrs["tooltip"] = fmt.Sprintf( |
| 398 | "%s\n%s", |
| 399 | edgeMap[key].Attrs["tooltip"], |
| 400 | fileEdge, |
| 401 | ) |
| 402 | } |
| 403 | } |
| 404 | |
| 405 | return nil |
| 406 | }) |
| 407 | if err != nil { |
| 408 | return nil, err |
| 409 | } |
| 410 | |
| 411 | // get edges form edgeMap |
| 412 | for _, e := range edgeMap { |
| 413 | e.From.Attrs["tooltip"] = fmt.Sprintf( |
| 414 | "%s\n%s", |
| 415 | e.From.Attrs["tooltip"], |
| 416 | e.Attrs["tooltip"], |
| 417 | ) |
| 418 | edges = append(edges, e) |
| 419 | } |
| 420 | |
| 421 | logf("%d/%d edges", len(edges), count) |
| 422 | |
| 423 | title := "" |
| 424 | if mainPkg != nil && mainPkg.Pkg != nil { |
| 425 | title = mainPkg.Pkg.Path() |
| 426 | } |
| 427 | dot := &dotGraph{ |
| 428 | Title: title, |
| 429 | Minlen: minlen, |
| 430 | Cluster: cluster, |
| 431 | Nodes: nodes, |
| 432 | Edges: edges, |
| 433 | Options: map[string]string{ |
| 434 | "minlen": fmt.Sprint(minlen), |
| 435 | "nodesep": fmt.Sprint(nodesep), |
| 436 | "nodeshape": fmt.Sprint(nodeshape), |
| 437 | "nodestyle": fmt.Sprint(nodestyle), |
| 438 | "rankdir": fmt.Sprint(rankdir), |
| 439 | }, |
| 440 | } |
| 441 | |
| 442 | var buf bytes.Buffer |
| 443 | if err := dot.WriteDot(&buf); err != nil { |
| 444 | return nil, err |
| 445 | } |
| 446 | |
| 447 | return buf.Bytes(), nil |
| 448 | } |
| 449 |
Members