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,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))