Coverage for call_graph/formatters.py: 80%

1"""Formatters for call graph output."""

2

3 from __future__ import annotations

4

5 from pathlib import Path

6

7

8 def _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 ]

15

16

17 def 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];")

22

23 internal_calls = _filter_internal_calls(graph)

24

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)

31

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

47

48 cluster_name = f"cluster_{idx}"

49

50 lines.append(f" subgraph {cluster_name} {{")

51 lines.append(f' label = "{safe_path}";')

52

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}"];')

61

62 lines.append(" }")

63

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}"

68

69 lines.append(f' "{caller_id}" -> "{callee_id}";')

70

71 lines.append("}")

72 return "\n".join(lines)