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.
- code_compass_cli-0.1.0.dist-info/METADATA +46 -0
- code_compass_cli-0.1.0.dist-info/RECORD +31 -0
- code_compass_cli-0.1.0.dist-info/WHEEL +5 -0
- code_compass_cli-0.1.0.dist-info/entry_points.txt +2 -0
- code_compass_cli-0.1.0.dist-info/top_level.txt +1 -0
- src/__init__.py +3 -0
- src/__pycache__/__init__.cpython-313.pyc +0 -0
- src/cli/__init__.py +1 -0
- src/cli/__pycache__/__init__.cpython-313.pyc +0 -0
- src/cli/__pycache__/main.cpython-313.pyc +0 -0
- src/cli/main.py +431 -0
- src/docs/__init__.py +1 -0
- src/docs/__pycache__/__init__.cpython-313.pyc +0 -0
- src/docs/__pycache__/doc_generator.cpython-313.pyc +0 -0
- src/docs/doc_generator.py +734 -0
- src/quality/__init__.py +1 -0
- src/quality/__pycache__/__init__.cpython-313.pyc +0 -0
- src/quality/__pycache__/analyzer.cpython-313.pyc +0 -0
- src/quality/analyzer.py +300 -0
- src/query/__init__.py +1 -0
- src/query/__pycache__/__init__.cpython-313.pyc +0 -0
- src/query/__pycache__/copilot_query.cpython-313.pyc +0 -0
- src/query/copilot_query.py +475 -0
- src/scanner/__init__.py +1 -0
- src/scanner/__pycache__/__init__.cpython-313.pyc +0 -0
- src/scanner/__pycache__/repo_scanner.cpython-313.pyc +0 -0
- src/scanner/repo_scanner.py +139 -0
- src/visualizer/__init__.py +1 -0
- src/visualizer/__pycache__/__init__.cpython-313.pyc +0 -0
- src/visualizer/__pycache__/flow_tracer.cpython-313.pyc +0 -0
- src/visualizer/flow_tracer.py +294 -0
|
@@ -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
|
+
}
|