Coverage for call_graph / formatters.py: 80%
40 statements
« prev ^ index » next coverage.py v7.13.3, created at 2026-02-08 15:04 -0800
« prev ^ index » next coverage.py v7.13.3, created at 2026-02-08 15:04 -0800
1"""Formatters for call graph output."""
3from __future__ import annotations
5from pathlib import Path
8def _filter_internal_calls(graph):
9 """Return only internal calls (excluding external/built-in)."""
10 return [
11 c
12 for c in graph.calls
13 if c.callee_file is not None and c.call_type != "external"
14 ]
17def format_dot(graph, base_path=None):
18 """Format call graph in GraphViz DOT language with file grouping."""
19 lines = ["digraph call_graph {"]
20 lines.append(" node [shape=box, fontname=Arial];")
21 lines.append(" edge [fontname=Arial];")
23 internal_calls = _filter_internal_calls(graph)
25 # Collect all files with functions
26 all_files = set()
27 for call in internal_calls:
28 all_files.add(call.caller_file)
29 if call.callee_file:
30 all_files.add(call.callee_file)
32 # Create a subgraph (cluster) for each file
33 for idx, filepath in enumerate(sorted(all_files), 1):
34 # Get relative path for display
35 if base_path:
36 try:
37 filepath_obj = Path(filepath)
38 if filepath_obj.is_absolute():
39 rel_path = filepath_obj.relative_to(base_path)
40 safe_path = str(rel_path).replace('"', '\\"')
41 else:
42 safe_path = filepath
43 except ValueError:
44 safe_path = filepath
45 else:
46 safe_path = filepath
48 cluster_name = f"cluster_{idx}"
50 lines.append(f" subgraph {cluster_name} {{")
51 lines.append(f' label = "{safe_path}";')
53 # Find all functions defined in this file
54 for (func_name, func_file), func_def in graph.functions.items():
55 if func_file == filepath:
56 # Create node ID with filepath:func_name format
57 node_id = f"{filepath}:{func_name}"
58 # Label shows function name with relative path
59 label = f"{func_name}\\n({safe_path})"
60 lines.append(f' "{node_id}" [label = "{label}"];')
62 lines.append(" }")
64 # Add edges between nodes
65 for call in internal_calls:
66 caller_id = f"{call.caller_file}:{call.caller_function}"
67 callee_id = f"{call.callee_file}:{call.callee_function}"
69 lines.append(f' "{caller_id}" -> "{callee_id}";')
71 lines.append("}")
72 return "\n".join(lines)