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