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

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

2 

3from __future__ import annotations 

4 

5from pathlib import Path 

6 

7 

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 ] 

15 

16 

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

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)