code-compass-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,294 @@
1
+ """Flow tracer for visualizing code execution paths."""
2
+
3
+ import os
4
+ import re
5
+ from pathlib import Path
6
+ from typing import Dict, List, Set, Tuple, Optional
7
+ from dataclasses import dataclass
8
+
9
+
10
+ @dataclass
11
+ class Node:
12
+ """Represents a node in the execution flow graph."""
13
+
14
+ name: str
15
+ node_type: str
16
+ file: str
17
+ line: int
18
+
19
+
20
+ class FlowTracer:
21
+ """Traces and visualizes execution flow in code."""
22
+
23
+ def __init__(self, repo_path: str = "."):
24
+ """Initialize the flow tracer.
25
+
26
+ Args:
27
+ repo_path: Path to repository to analyze
28
+ """
29
+ self.repo_path = Path(repo_path)
30
+ self.nodes: Dict[str, Node] = {}
31
+ self.edges: List[Tuple[str, str]] = []
32
+ self.definitions: Dict[str, Node] = {}
33
+ self.calls: List[Tuple[str, str, str, int]] = [] # (caller, callee, file, line)
34
+
35
+ def trace(self, entry_point: str) -> Dict[str, any]:
36
+ """Trace execution flow starting from an entry point.
37
+
38
+ Args:
39
+ entry_point: Name of the entry point function/method
40
+
41
+ Returns:
42
+ Dictionary representing the execution flow
43
+ """
44
+ # First, scan the codebase for definitions and calls
45
+ self._scan_codebase()
46
+
47
+ # Then build the call chain
48
+ visited: Set[str] = set()
49
+ flow = self._build_call_chain(entry_point, visited, depth=0)
50
+
51
+ return {
52
+ "entry_point": entry_point,
53
+ "flow": flow,
54
+ "total_calls": len(self.calls),
55
+ }
56
+
57
+ def _scan_codebase(self) -> None:
58
+ """Scan codebase to find function definitions and calls."""
59
+ for root, _, files in os.walk(self.repo_path):
60
+ # Skip venv and hidden directories
61
+ if "venv" in root or "/.git" in root:
62
+ continue
63
+
64
+ for file in files:
65
+ if file.endswith(".py"):
66
+ filepath = Path(root) / file
67
+ rel_path = filepath.relative_to(self.repo_path)
68
+
69
+ try:
70
+ with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
71
+ content = f.read()
72
+ self._extract_definitions(content, str(rel_path))
73
+ self._extract_calls(content, str(rel_path))
74
+ except (IOError, OSError):
75
+ continue
76
+
77
+ def _extract_definitions(self, content: str, filepath: str) -> None:
78
+ """Extract function and class definitions.
79
+
80
+ Args:
81
+ content: File content
82
+ filepath: Path to the file
83
+ """
84
+ lines = content.split("\n")
85
+
86
+ # Find function definitions
87
+ for i, line in enumerate(lines, 1):
88
+ # Match function definitions
89
+ func_match = re.match(r"^\s*def\s+(\w+)\s*\(", line)
90
+ if func_match:
91
+ func_name = func_match.group(1)
92
+ self.definitions[func_name] = Node(
93
+ name=func_name,
94
+ node_type="function",
95
+ file=filepath,
96
+ line=i,
97
+ )
98
+
99
+ # Match class definitions
100
+ class_match = re.match(r"^\s*class\s+(\w+)\s*[\(:]", line)
101
+ if class_match:
102
+ class_name = class_match.group(1)
103
+ self.definitions[class_name] = Node(
104
+ name=class_name,
105
+ node_type="class",
106
+ file=filepath,
107
+ line=i,
108
+ )
109
+
110
+ def _extract_calls(self, content: str, filepath: str) -> None:
111
+ """Extract function and method calls.
112
+
113
+ Args:
114
+ content: File content
115
+ filepath: Path to the file
116
+ """
117
+ lines = content.split("\n")
118
+
119
+ # Built-in functions and methods to skip
120
+ builtins = {
121
+ "append", "extend", "insert", "remove", "pop", "clear", "index", "count",
122
+ "sort", "reverse", "copy", "len", "str", "int", "float", "bool", "list",
123
+ "dict", "set", "tuple", "range", "enumerate", "zip", "map", "filter",
124
+ "open", "read", "write", "close", "strip", "split", "join", "replace",
125
+ "upper", "lower", "startswith", "endswith", "find", "format", "items",
126
+ "keys", "values", "get", "update", "exists", "mkdir", "rmdir", "walk",
127
+ "exit", "print", "input", "any", "all", "sum", "max", "min", "abs",
128
+ "round", "isinstance", "type", "hasattr", "getattr", "setattr", "callable",
129
+ "sorted", "reversed", "group", "match", "search", "sub", "compile",
130
+ "relative_to", "with", "except", "try", "assert"
131
+ }
132
+
133
+ for i, line in enumerate(lines, 1):
134
+ # Skip comments and docstrings
135
+ if line.strip().startswith("#") or '"""' in line or "'''" in line:
136
+ continue
137
+
138
+ # Find function calls - both direct and method calls
139
+ # Patterns: func_name(), obj.method(), module.func()
140
+ call_matches = re.findall(r"(\w+)\s*\(", line)
141
+
142
+ for match in call_matches:
143
+ # Skip built-in functions
144
+ if match in builtins:
145
+ continue
146
+
147
+ # Get the context - find which function we're in
148
+ caller = self._find_current_function(content, i)
149
+ if caller:
150
+ self.calls.append((caller, match, filepath, i))
151
+
152
+ def _find_current_function(self, content: str, line_num: int) -> Optional[str]:
153
+ """Find which function a line belongs to.
154
+
155
+ Args:
156
+ content: File content
157
+ line_num: Line number (1-indexed)
158
+
159
+ Returns:
160
+ Name of the function or None
161
+ """
162
+ lines = content.split("\n")
163
+ current_func = None
164
+ current_indent = float("inf")
165
+
166
+ for i in range(line_num - 1, -1, -1):
167
+ line = lines[i]
168
+ if line.strip() == "":
169
+ continue
170
+
171
+ indent = len(line) - len(line.lstrip())
172
+
173
+ # Looking for function definition at lower indentation
174
+ if indent < current_indent:
175
+ func_match = re.match(r"^\s*def\s+(\w+)\s*\(", line)
176
+ if func_match:
177
+ return func_match.group(1)
178
+
179
+ if current_indent == float("inf"):
180
+ current_indent = indent
181
+
182
+ return None
183
+
184
+ def _build_call_chain(self, func_name: str, visited: Set[str], depth: int = 0) -> Dict[str, any]:
185
+ """Build the call chain for a function.
186
+
187
+ Args:
188
+ func_name: Name of the function
189
+ visited: Set of already visited functions
190
+ depth: Current depth in the call tree
191
+
192
+ Returns:
193
+ Dictionary representing the call chain
194
+ """
195
+ if func_name in visited or depth > 5: # Prevent infinite recursion
196
+ return None
197
+
198
+ visited.add(func_name)
199
+
200
+ # Get the definition
201
+ definition = self.definitions.get(func_name)
202
+ if not definition:
203
+ return {
204
+ "name": func_name,
205
+ "type": "unknown",
206
+ "file": None,
207
+ "line": None,
208
+ "calls": [],
209
+ }
210
+
211
+ # Find all functions called by this function
212
+ called_functions = []
213
+ for caller, callee, call_file, call_line in self.calls:
214
+ if caller == func_name:
215
+ called_functions.append((callee, call_file, call_line))
216
+
217
+ # Remove duplicates and sort
218
+ called_functions = sorted(set(called_functions))
219
+
220
+ # Build call chain for each called function
221
+ children = []
222
+ for callee, call_file, call_line in called_functions:
223
+ child_flow = self._build_call_chain(callee, visited.copy(), depth + 1)
224
+ if child_flow:
225
+ child_flow["called_from"] = {
226
+ "file": call_file,
227
+ "line": call_line,
228
+ }
229
+ children.append(child_flow)
230
+
231
+ return {
232
+ "name": func_name,
233
+ "type": definition.node_type,
234
+ "file": definition.file,
235
+ "line": definition.line,
236
+ "calls": children,
237
+ }
238
+
239
+ def add_node(self, node: Node) -> None:
240
+ """Add a node to the flow graph.
241
+
242
+ Args:
243
+ node: Node to add
244
+ """
245
+ self.nodes[node.name] = node
246
+
247
+ def add_edge(self, source: str, target: str) -> None:
248
+ """Add an edge between two nodes.
249
+
250
+ Args:
251
+ source: Source node name
252
+ target: Target node name
253
+ """
254
+ self.edges.append((source, target))
255
+
256
+ def get_flow(self, entry_point: str) -> Dict[str, any]:
257
+ """Get the execution flow starting from an entry point.
258
+
259
+ Args:
260
+ entry_point: Name of the entry point node
261
+
262
+ Returns:
263
+ Dictionary representing the execution flow
264
+ """
265
+ visited: Set[str] = set()
266
+ flow = self._traverse(entry_point, visited)
267
+ return flow
268
+
269
+ def _traverse(self, node_name: str, visited: Set[str]) -> Dict[str, any]:
270
+ """Traverse the flow graph from a node.
271
+
272
+ Args:
273
+ node_name: Name of the node to traverse from
274
+ visited: Set of already visited nodes
275
+
276
+ Returns:
277
+ Dictionary representing the traversal
278
+ """
279
+ if node_name in visited or node_name not in self.nodes:
280
+ return {}
281
+
282
+ visited.add(node_name)
283
+ node = self.nodes[node_name]
284
+ children = [target for source, target in self.edges if source == node_name]
285
+
286
+ return {
287
+ "node": {
288
+ "name": node.name,
289
+ "type": node.node_type,
290
+ "file": node.file,
291
+ "line": node.line,
292
+ },
293
+ "children": [self._traverse(child, visited) for child in children],
294
+ }