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,315 @@
|
|
|
1
|
+
"""Import resolution for multiple languages.
|
|
2
|
+
|
|
3
|
+
Resolves import statements to actual file paths on disk.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional, List, Dict
|
|
10
|
+
|
|
11
|
+
from roma_debug.core.models import Language, Import
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ImportResolver:
|
|
15
|
+
"""Resolves import statements to file paths.
|
|
16
|
+
|
|
17
|
+
Handles import resolution for Python, JavaScript/TypeScript, and Go.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, project_root: Optional[str] = None):
|
|
21
|
+
"""Initialize the resolver.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
project_root: Root directory of the project for relative imports
|
|
25
|
+
"""
|
|
26
|
+
self.project_root = Path(project_root) if project_root else Path.cwd()
|
|
27
|
+
self._cache: Dict[str, Optional[str]] = {}
|
|
28
|
+
|
|
29
|
+
def resolve_imports(
|
|
30
|
+
self,
|
|
31
|
+
imports: List[Import],
|
|
32
|
+
source_file: Path,
|
|
33
|
+
) -> List[Import]:
|
|
34
|
+
"""Resolve a list of imports to file paths.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
imports: List of Import objects
|
|
38
|
+
source_file: Path to the file containing the imports
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
List of Import objects with resolved_path populated
|
|
42
|
+
"""
|
|
43
|
+
resolved = []
|
|
44
|
+
for imp in imports:
|
|
45
|
+
resolved_imp = self.resolve_import(imp, source_file)
|
|
46
|
+
resolved.append(resolved_imp)
|
|
47
|
+
return resolved
|
|
48
|
+
|
|
49
|
+
def resolve_import(self, imp: Import, source_file: Path) -> Import:
|
|
50
|
+
"""Resolve a single import to a file path.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
imp: Import object to resolve
|
|
54
|
+
source_file: Path to the file containing the import
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Import object with resolved_path populated (may be None if not found)
|
|
58
|
+
"""
|
|
59
|
+
# Create a copy to avoid mutating the original
|
|
60
|
+
resolved = Import(
|
|
61
|
+
module_name=imp.module_name,
|
|
62
|
+
alias=imp.alias,
|
|
63
|
+
imported_names=imp.imported_names.copy(),
|
|
64
|
+
is_relative=imp.is_relative,
|
|
65
|
+
relative_level=imp.relative_level,
|
|
66
|
+
line_number=imp.line_number,
|
|
67
|
+
resolved_path=None,
|
|
68
|
+
language=imp.language,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Use cache if available
|
|
72
|
+
cache_key = f"{imp.language.value}:{source_file}:{imp.module_name}:{imp.relative_level}"
|
|
73
|
+
if cache_key in self._cache:
|
|
74
|
+
resolved.resolved_path = self._cache[cache_key]
|
|
75
|
+
return resolved
|
|
76
|
+
|
|
77
|
+
# Resolve based on language
|
|
78
|
+
if imp.language == Language.PYTHON:
|
|
79
|
+
path = self._resolve_python_import(imp, source_file)
|
|
80
|
+
elif imp.language in (Language.JAVASCRIPT, Language.TYPESCRIPT):
|
|
81
|
+
path = self._resolve_js_import(imp, source_file)
|
|
82
|
+
elif imp.language == Language.GO:
|
|
83
|
+
path = self._resolve_go_import(imp, source_file)
|
|
84
|
+
else:
|
|
85
|
+
path = None
|
|
86
|
+
|
|
87
|
+
resolved.resolved_path = path
|
|
88
|
+
self._cache[cache_key] = path
|
|
89
|
+
return resolved
|
|
90
|
+
|
|
91
|
+
def _resolve_python_import(self, imp: Import, source_file: Path) -> Optional[str]:
|
|
92
|
+
"""Resolve a Python import to a file path.
|
|
93
|
+
|
|
94
|
+
Handles:
|
|
95
|
+
- Absolute imports: import foo, from foo import bar
|
|
96
|
+
- Relative imports: from . import bar, from ..foo import bar
|
|
97
|
+
- Package imports: import foo.bar.baz
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
imp: Import object
|
|
101
|
+
source_file: Path to source file
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Resolved file path or None
|
|
105
|
+
"""
|
|
106
|
+
if imp.is_relative:
|
|
107
|
+
return self._resolve_python_relative_import(imp, source_file)
|
|
108
|
+
return self._resolve_python_absolute_import(imp)
|
|
109
|
+
|
|
110
|
+
def _resolve_python_relative_import(self, imp: Import, source_file: Path) -> Optional[str]:
|
|
111
|
+
"""Resolve a Python relative import."""
|
|
112
|
+
source_dir = source_file.parent
|
|
113
|
+
|
|
114
|
+
# Go up directories based on relative level
|
|
115
|
+
# level 1 = current package (.), level 2 = parent package (..), etc.
|
|
116
|
+
target_dir = source_dir
|
|
117
|
+
for _ in range(imp.relative_level - 1):
|
|
118
|
+
target_dir = target_dir.parent
|
|
119
|
+
|
|
120
|
+
# Now resolve the module name from this directory
|
|
121
|
+
if imp.module_name:
|
|
122
|
+
parts = imp.module_name.split('.')
|
|
123
|
+
target_path = target_dir / '/'.join(parts)
|
|
124
|
+
else:
|
|
125
|
+
target_path = target_dir
|
|
126
|
+
|
|
127
|
+
# Check for module file or package
|
|
128
|
+
candidates = [
|
|
129
|
+
target_path.with_suffix('.py'),
|
|
130
|
+
target_path / '__init__.py',
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
for candidate in candidates:
|
|
134
|
+
if candidate.exists():
|
|
135
|
+
return str(candidate)
|
|
136
|
+
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
def _resolve_python_absolute_import(self, imp: Import) -> Optional[str]:
|
|
140
|
+
"""Resolve a Python absolute import."""
|
|
141
|
+
parts = imp.module_name.split('.')
|
|
142
|
+
|
|
143
|
+
# Check in project root first
|
|
144
|
+
project_path = self.project_root / '/'.join(parts)
|
|
145
|
+
candidates = [
|
|
146
|
+
project_path.with_suffix('.py'),
|
|
147
|
+
project_path / '__init__.py',
|
|
148
|
+
]
|
|
149
|
+
|
|
150
|
+
for candidate in candidates:
|
|
151
|
+
if candidate.exists():
|
|
152
|
+
return str(candidate)
|
|
153
|
+
|
|
154
|
+
# Check common source directories
|
|
155
|
+
src_dirs = ['src', 'lib', 'app', '.']
|
|
156
|
+
for src_dir in src_dirs:
|
|
157
|
+
base = self.project_root / src_dir
|
|
158
|
+
if not base.exists():
|
|
159
|
+
continue
|
|
160
|
+
|
|
161
|
+
target_path = base / '/'.join(parts)
|
|
162
|
+
candidates = [
|
|
163
|
+
target_path.with_suffix('.py'),
|
|
164
|
+
target_path / '__init__.py',
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
for candidate in candidates:
|
|
168
|
+
if candidate.exists():
|
|
169
|
+
return str(candidate)
|
|
170
|
+
|
|
171
|
+
# Check in sys.path (installed packages - skip for now as they're external)
|
|
172
|
+
# We focus on local project files for debugging
|
|
173
|
+
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
def _resolve_js_import(self, imp: Import, source_file: Path) -> Optional[str]:
|
|
177
|
+
"""Resolve a JavaScript/TypeScript import to a file path.
|
|
178
|
+
|
|
179
|
+
Handles:
|
|
180
|
+
- Relative imports: ./foo, ../bar
|
|
181
|
+
- Absolute imports from project root
|
|
182
|
+
- Index files
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
imp: Import object
|
|
186
|
+
source_file: Path to source file
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Resolved file path or None
|
|
190
|
+
"""
|
|
191
|
+
module = imp.module_name
|
|
192
|
+
|
|
193
|
+
# Skip npm packages (don't start with . or /)
|
|
194
|
+
if not module.startswith('.') and not module.startswith('/'):
|
|
195
|
+
# Could be an npm package or alias, skip for now
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
source_dir = source_file.parent
|
|
199
|
+
|
|
200
|
+
if module.startswith('./') or module.startswith('../'):
|
|
201
|
+
# Relative import
|
|
202
|
+
target = (source_dir / module).resolve()
|
|
203
|
+
elif module.startswith('/'):
|
|
204
|
+
# Absolute from project root
|
|
205
|
+
target = self.project_root / module.lstrip('/')
|
|
206
|
+
else:
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
# Try various extensions
|
|
210
|
+
extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '']
|
|
211
|
+
for ext in extensions:
|
|
212
|
+
candidate = Path(str(target) + ext)
|
|
213
|
+
if candidate.exists() and candidate.is_file():
|
|
214
|
+
return str(candidate)
|
|
215
|
+
|
|
216
|
+
# Try index files
|
|
217
|
+
index_names = ['index.ts', 'index.tsx', 'index.js', 'index.jsx']
|
|
218
|
+
for index_name in index_names:
|
|
219
|
+
candidate = target / index_name
|
|
220
|
+
if candidate.exists():
|
|
221
|
+
return str(candidate)
|
|
222
|
+
|
|
223
|
+
return None
|
|
224
|
+
|
|
225
|
+
def _resolve_go_import(self, imp: Import, source_file: Path) -> Optional[str]:
|
|
226
|
+
"""Resolve a Go import to a file path.
|
|
227
|
+
|
|
228
|
+
Go imports are package paths. We look for local packages
|
|
229
|
+
relative to the project root.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
imp: Import object
|
|
233
|
+
source_file: Path to source file
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
Resolved file path or None (returns directory for Go packages)
|
|
237
|
+
"""
|
|
238
|
+
module = imp.module_name
|
|
239
|
+
|
|
240
|
+
# Skip standard library and external packages
|
|
241
|
+
if not module.startswith('.') and '/' not in module:
|
|
242
|
+
return None # Standard library
|
|
243
|
+
|
|
244
|
+
# Check if it's a local package
|
|
245
|
+
# Look for go.mod to find module path
|
|
246
|
+
go_mod = self._find_go_mod()
|
|
247
|
+
if go_mod:
|
|
248
|
+
module_path = self._get_go_module_path(go_mod)
|
|
249
|
+
if module_path and module.startswith(module_path):
|
|
250
|
+
# Local package
|
|
251
|
+
relative_path = module[len(module_path):].lstrip('/')
|
|
252
|
+
package_dir = self.project_root / relative_path
|
|
253
|
+
if package_dir.exists() and package_dir.is_dir():
|
|
254
|
+
# Return directory path (Go packages are directories)
|
|
255
|
+
# Find the first .go file
|
|
256
|
+
go_files = list(package_dir.glob('*.go'))
|
|
257
|
+
if go_files:
|
|
258
|
+
return str(go_files[0])
|
|
259
|
+
return str(package_dir)
|
|
260
|
+
|
|
261
|
+
# Try relative to project root
|
|
262
|
+
parts = module.split('/')
|
|
263
|
+
# Skip first part if it looks like a domain
|
|
264
|
+
if '.' in parts[0]:
|
|
265
|
+
parts = parts[1:]
|
|
266
|
+
|
|
267
|
+
package_dir = self.project_root / '/'.join(parts)
|
|
268
|
+
if package_dir.exists() and package_dir.is_dir():
|
|
269
|
+
go_files = list(package_dir.glob('*.go'))
|
|
270
|
+
if go_files:
|
|
271
|
+
return str(go_files[0])
|
|
272
|
+
|
|
273
|
+
return None
|
|
274
|
+
|
|
275
|
+
def _find_go_mod(self) -> Optional[Path]:
|
|
276
|
+
"""Find go.mod file in project."""
|
|
277
|
+
go_mod = self.project_root / 'go.mod'
|
|
278
|
+
if go_mod.exists():
|
|
279
|
+
return go_mod
|
|
280
|
+
return None
|
|
281
|
+
|
|
282
|
+
def _get_go_module_path(self, go_mod: Path) -> Optional[str]:
|
|
283
|
+
"""Extract module path from go.mod."""
|
|
284
|
+
try:
|
|
285
|
+
content = go_mod.read_text()
|
|
286
|
+
for line in content.splitlines():
|
|
287
|
+
line = line.strip()
|
|
288
|
+
if line.startswith('module '):
|
|
289
|
+
return line[7:].strip()
|
|
290
|
+
except Exception:
|
|
291
|
+
pass
|
|
292
|
+
return None
|
|
293
|
+
|
|
294
|
+
def clear_cache(self):
|
|
295
|
+
"""Clear the resolution cache."""
|
|
296
|
+
self._cache.clear()
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def resolve_import(
|
|
300
|
+
imp: Import,
|
|
301
|
+
source_file: str,
|
|
302
|
+
project_root: Optional[str] = None,
|
|
303
|
+
) -> Import:
|
|
304
|
+
"""Convenience function to resolve a single import.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
imp: Import object to resolve
|
|
308
|
+
source_file: Path to the file containing the import
|
|
309
|
+
project_root: Optional project root directory
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
Import with resolved_path populated
|
|
313
|
+
"""
|
|
314
|
+
resolver = ImportResolver(project_root)
|
|
315
|
+
return resolver.resolve_import(imp, Path(source_file))
|