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.
@@ -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