roma-debug 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.
- roma_debug/__init__.py +3 -0
- roma_debug/config.py +79 -0
- roma_debug/core/__init__.py +5 -0
- roma_debug/core/engine.py +423 -0
- roma_debug/core/models.py +313 -0
- roma_debug/main.py +753 -0
- roma_debug/parsers/__init__.py +21 -0
- roma_debug/parsers/base.py +189 -0
- roma_debug/parsers/python_ast_parser.py +268 -0
- roma_debug/parsers/registry.py +196 -0
- roma_debug/parsers/traceback_patterns.py +314 -0
- roma_debug/parsers/treesitter_parser.py +598 -0
- roma_debug/prompts.py +153 -0
- roma_debug/server.py +247 -0
- roma_debug/tracing/__init__.py +28 -0
- roma_debug/tracing/call_chain.py +278 -0
- roma_debug/tracing/context_builder.py +672 -0
- roma_debug/tracing/dependency_graph.py +298 -0
- roma_debug/tracing/error_analyzer.py +399 -0
- roma_debug/tracing/import_resolver.py +315 -0
- roma_debug/tracing/project_scanner.py +569 -0
- roma_debug/utils/__init__.py +5 -0
- roma_debug/utils/context.py +422 -0
- roma_debug-0.1.0.dist-info/METADATA +34 -0
- roma_debug-0.1.0.dist-info/RECORD +36 -0
- roma_debug-0.1.0.dist-info/WHEEL +5 -0
- roma_debug-0.1.0.dist-info/entry_points.txt +2 -0
- roma_debug-0.1.0.dist-info/licenses/LICENSE +201 -0
- roma_debug-0.1.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +1 -0
- tests/test_context.py +208 -0
- tests/test_engine.py +296 -0
- tests/test_parsers.py +534 -0
- tests/test_project_scanner.py +275 -0
- tests/test_traceback_patterns.py +222 -0
- tests/test_tracing.py +296 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""Call chain analysis for tracing error origins.
|
|
2
|
+
|
|
3
|
+
Analyzes function call chains from tracebacks to understand
|
|
4
|
+
the flow of execution leading to an error.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional, List, Dict
|
|
10
|
+
|
|
11
|
+
from roma_debug.core.models import (
|
|
12
|
+
Language, Symbol, TraceFrame, ParsedTraceback, FileContext
|
|
13
|
+
)
|
|
14
|
+
from roma_debug.parsers.registry import get_parser
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class CallSite:
|
|
19
|
+
"""A single call site in the call chain."""
|
|
20
|
+
filepath: str
|
|
21
|
+
line_number: int
|
|
22
|
+
function_name: Optional[str]
|
|
23
|
+
called_function: Optional[str]
|
|
24
|
+
arguments: Optional[str] = None
|
|
25
|
+
language: Language = Language.UNKNOWN
|
|
26
|
+
|
|
27
|
+
def __str__(self) -> str:
|
|
28
|
+
func = self.function_name or "<module>"
|
|
29
|
+
called = f" -> {self.called_function}" if self.called_function else ""
|
|
30
|
+
return f"{Path(self.filepath).name}:{self.line_number} {func}{called}"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class CallChain:
|
|
35
|
+
"""A chain of function calls from entry point to error."""
|
|
36
|
+
sites: List[CallSite] = field(default_factory=list)
|
|
37
|
+
error_frame: Optional[TraceFrame] = None
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def entry_point(self) -> Optional[CallSite]:
|
|
41
|
+
"""Get the first call in the chain."""
|
|
42
|
+
return self.sites[0] if self.sites else None
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def error_site(self) -> Optional[CallSite]:
|
|
46
|
+
"""Get the call site where the error occurred."""
|
|
47
|
+
return self.sites[-1] if self.sites else None
|
|
48
|
+
|
|
49
|
+
def to_string_list(self) -> List[str]:
|
|
50
|
+
"""Get chain as list of strings for AI prompt."""
|
|
51
|
+
return [str(site) for site in self.sites]
|
|
52
|
+
|
|
53
|
+
def __str__(self) -> str:
|
|
54
|
+
return " -> ".join(self.to_string_list())
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class CallChainAnalyzer:
|
|
58
|
+
"""Analyzes call chains from tracebacks and source code.
|
|
59
|
+
|
|
60
|
+
Combines traceback information with source analysis to build
|
|
61
|
+
a detailed picture of the call flow.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(self, project_root: Optional[str] = None):
|
|
65
|
+
"""Initialize the analyzer.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
project_root: Root directory for file resolution
|
|
69
|
+
"""
|
|
70
|
+
self.project_root = Path(project_root) if project_root else Path.cwd()
|
|
71
|
+
self._source_cache: Dict[str, str] = {}
|
|
72
|
+
|
|
73
|
+
def analyze_traceback(self, traceback: ParsedTraceback) -> CallChain:
|
|
74
|
+
"""Build a call chain from a parsed traceback.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
traceback: Parsed traceback with frames
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
CallChain representing the execution flow
|
|
81
|
+
"""
|
|
82
|
+
chain = CallChain()
|
|
83
|
+
chain.error_frame = traceback.primary_frame
|
|
84
|
+
|
|
85
|
+
for i, frame in enumerate(traceback.frames):
|
|
86
|
+
# Determine what function was called (from next frame)
|
|
87
|
+
called_function = None
|
|
88
|
+
if i + 1 < len(traceback.frames):
|
|
89
|
+
next_frame = traceback.frames[i + 1]
|
|
90
|
+
called_function = next_frame.function_name
|
|
91
|
+
|
|
92
|
+
site = CallSite(
|
|
93
|
+
filepath=frame.filepath,
|
|
94
|
+
line_number=frame.line_number,
|
|
95
|
+
function_name=frame.function_name,
|
|
96
|
+
called_function=called_function,
|
|
97
|
+
language=frame.language,
|
|
98
|
+
)
|
|
99
|
+
chain.sites.append(site)
|
|
100
|
+
|
|
101
|
+
return chain
|
|
102
|
+
|
|
103
|
+
def analyze_from_contexts(
|
|
104
|
+
self,
|
|
105
|
+
contexts: List[FileContext],
|
|
106
|
+
traceback: Optional[ParsedTraceback] = None,
|
|
107
|
+
) -> CallChain:
|
|
108
|
+
"""Build a call chain from file contexts.
|
|
109
|
+
|
|
110
|
+
Uses source analysis to extract additional call information.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
contexts: List of FileContext objects from traceback files
|
|
114
|
+
traceback: Optional parsed traceback for additional info
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
CallChain with enhanced information
|
|
118
|
+
"""
|
|
119
|
+
chain = CallChain()
|
|
120
|
+
|
|
121
|
+
for ctx in contexts:
|
|
122
|
+
# Get function/class names from context
|
|
123
|
+
function_name = ctx.function_name
|
|
124
|
+
if not function_name and ctx.class_name:
|
|
125
|
+
function_name = f"{ctx.class_name}.__init__"
|
|
126
|
+
|
|
127
|
+
# Try to find what function is called at the error line
|
|
128
|
+
called_function = self._find_called_function(ctx)
|
|
129
|
+
|
|
130
|
+
site = CallSite(
|
|
131
|
+
filepath=ctx.filepath,
|
|
132
|
+
line_number=ctx.line_number,
|
|
133
|
+
function_name=function_name,
|
|
134
|
+
called_function=called_function,
|
|
135
|
+
language=ctx.language,
|
|
136
|
+
)
|
|
137
|
+
chain.sites.append(site)
|
|
138
|
+
|
|
139
|
+
return chain
|
|
140
|
+
|
|
141
|
+
def _find_called_function(self, context: FileContext) -> Optional[str]:
|
|
142
|
+
"""Find the function called at the error line.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
context: FileContext with source content
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Name of the called function or None
|
|
149
|
+
"""
|
|
150
|
+
if not context.symbol:
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
# Get the parser for this language
|
|
154
|
+
parser = get_parser(context.language, create_new=True)
|
|
155
|
+
if not parser:
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
# Parse the source if available
|
|
159
|
+
source = self._get_source(context.filepath)
|
|
160
|
+
if not source:
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
if not parser.parse(source, context.filepath):
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
# For Python, we can use the AST parser's call extraction
|
|
167
|
+
if hasattr(parser, 'get_function_calls_in_symbol'):
|
|
168
|
+
calls = parser.get_function_calls_in_symbol(context.symbol)
|
|
169
|
+
if calls:
|
|
170
|
+
# Return the most likely called function
|
|
171
|
+
# (this is a simplification - ideally we'd analyze the specific line)
|
|
172
|
+
return calls[0]
|
|
173
|
+
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
def _get_source(self, filepath: str) -> Optional[str]:
|
|
177
|
+
"""Get source code for a file with caching.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
filepath: Path to the file
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Source code string or None
|
|
184
|
+
"""
|
|
185
|
+
if filepath in self._source_cache:
|
|
186
|
+
return self._source_cache[filepath]
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
path = Path(filepath)
|
|
190
|
+
if not path.exists():
|
|
191
|
+
# Try relative to project root
|
|
192
|
+
path = self.project_root / filepath
|
|
193
|
+
if not path.exists():
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
source = path.read_text(encoding='utf-8', errors='replace')
|
|
197
|
+
self._source_cache[filepath] = source
|
|
198
|
+
return source
|
|
199
|
+
except Exception:
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
def find_data_flow(
|
|
203
|
+
self,
|
|
204
|
+
chain: CallChain,
|
|
205
|
+
variable_name: str,
|
|
206
|
+
) -> List[CallSite]:
|
|
207
|
+
"""Trace where a variable's value comes from in the call chain.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
chain: The call chain to analyze
|
|
211
|
+
variable_name: Name of the variable to trace
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
List of call sites where the variable is assigned/modified
|
|
215
|
+
"""
|
|
216
|
+
# This is a simplified implementation
|
|
217
|
+
# Full data flow analysis would require proper static analysis
|
|
218
|
+
|
|
219
|
+
relevant_sites = []
|
|
220
|
+
|
|
221
|
+
for site in chain.sites:
|
|
222
|
+
source = self._get_source(site.filepath)
|
|
223
|
+
if not source:
|
|
224
|
+
continue
|
|
225
|
+
|
|
226
|
+
lines = source.splitlines()
|
|
227
|
+
if 0 <= site.line_number - 1 < len(lines):
|
|
228
|
+
line = lines[site.line_number - 1]
|
|
229
|
+
|
|
230
|
+
# Simple check: does this line assign to the variable?
|
|
231
|
+
if f"{variable_name} =" in line or f"{variable_name}=" in line:
|
|
232
|
+
relevant_sites.append(site)
|
|
233
|
+
# Check for function parameter
|
|
234
|
+
elif f"{variable_name}" in line and ("def " in line or "func " in line):
|
|
235
|
+
relevant_sites.append(site)
|
|
236
|
+
|
|
237
|
+
return relevant_sites
|
|
238
|
+
|
|
239
|
+
def get_upstream_callers(
|
|
240
|
+
self,
|
|
241
|
+
filepath: str,
|
|
242
|
+
function_name: str,
|
|
243
|
+
contexts: List[FileContext],
|
|
244
|
+
) -> List[CallSite]:
|
|
245
|
+
"""Find callers of a function from the provided contexts.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
filepath: File containing the function
|
|
249
|
+
function_name: Name of the function
|
|
250
|
+
contexts: File contexts to search
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
List of call sites that call this function
|
|
254
|
+
"""
|
|
255
|
+
callers = []
|
|
256
|
+
|
|
257
|
+
for ctx in contexts:
|
|
258
|
+
if ctx.filepath == filepath:
|
|
259
|
+
continue
|
|
260
|
+
|
|
261
|
+
source = self._get_source(ctx.filepath)
|
|
262
|
+
if not source:
|
|
263
|
+
continue
|
|
264
|
+
|
|
265
|
+
# Simple search for function calls
|
|
266
|
+
lines = source.splitlines()
|
|
267
|
+
for i, line in enumerate(lines, 1):
|
|
268
|
+
# Look for function call patterns
|
|
269
|
+
if f"{function_name}(" in line:
|
|
270
|
+
callers.append(CallSite(
|
|
271
|
+
filepath=ctx.filepath,
|
|
272
|
+
line_number=i,
|
|
273
|
+
function_name=ctx.function_name,
|
|
274
|
+
called_function=function_name,
|
|
275
|
+
language=ctx.language,
|
|
276
|
+
))
|
|
277
|
+
|
|
278
|
+
return callers
|