codeui-python 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.
Files changed (52) hide show
  1. codeui/__init__.py +23 -0
  2. codeui/agent/__init__.py +15 -0
  3. codeui/agent/context.py +207 -0
  4. codeui/agent/contribution.py +224 -0
  5. codeui/agent/edit_tools.py +280 -0
  6. codeui/agent/integrations.py +243 -0
  7. codeui/agent/mcp_server.py +94 -0
  8. codeui/analysis/__init__.py +1 -0
  9. codeui/analysis/api_drift.py +79 -0
  10. codeui/analysis/base.py +42 -0
  11. codeui/analysis/circular.py +116 -0
  12. codeui/analysis/dead_code.py +63 -0
  13. codeui/analysis/duplicates.py +69 -0
  14. codeui/analysis/runner.py +57 -0
  15. codeui/analysis/shadowing.py +49 -0
  16. codeui/analysis/undefined.py +97 -0
  17. codeui/analysis/unreachable.py +28 -0
  18. codeui/analysis/unresolved_import.py +153 -0
  19. codeui/cli/__init__.py +1 -0
  20. codeui/cli/main.py +437 -0
  21. codeui/core/__init__.py +1 -0
  22. codeui/core/cache.py +135 -0
  23. codeui/core/graph.py +435 -0
  24. codeui/core/ir.py +218 -0
  25. codeui/core/override.py +230 -0
  26. codeui/core/repo.py +85 -0
  27. codeui/core/resolver.py +281 -0
  28. codeui/errors.py +98 -0
  29. codeui/lang/__init__.py +1 -0
  30. codeui/lang/base.py +117 -0
  31. codeui/lang/generic.py +90 -0
  32. codeui/lang/go.py +167 -0
  33. codeui/lang/python.py +294 -0
  34. codeui/lang/registry.py +83 -0
  35. codeui/lang/rust.py +185 -0
  36. codeui/lang/ts.py +396 -0
  37. codeui/plugins/__init__.py +1 -0
  38. codeui/plugins/registry.py +31 -0
  39. codeui/report/__init__.py +1 -0
  40. codeui/report/json_emitter.py +24 -0
  41. codeui/report/markdown_emitter.py +33 -0
  42. codeui/report/sarif_emitter.py +56 -0
  43. codeui/server/__init__.py +1 -0
  44. codeui/server/server.py +705 -0
  45. codeui/tracer/__init__.py +1 -0
  46. codeui/tracer/tracer.py +147 -0
  47. codeui_python-0.1.0.dist-info/METADATA +191 -0
  48. codeui_python-0.1.0.dist-info/RECORD +52 -0
  49. codeui_python-0.1.0.dist-info/WHEEL +5 -0
  50. codeui_python-0.1.0.dist-info/entry_points.txt +2 -0
  51. codeui_python-0.1.0.dist-info/licenses/LICENSE +21 -0
  52. codeui_python-0.1.0.dist-info/top_level.txt +1 -0
codeui/lang/go.py ADDED
@@ -0,0 +1,167 @@
1
+ """Go language adapter for codeui."""
2
+ import hashlib
3
+ import re
4
+ from pathlib import Path
5
+ from typing import ClassVar, Iterable, List, Sequence
6
+ from codeui.core.ir import Edge, EdgeKind, Location, Symbol, SymbolKind, Visibility
7
+ from codeui.core.resolver import ResolveContext
8
+ from codeui.lang.base import ImportRef, LanguageAnalyzer, ParseResult
9
+
10
+ class GoLanguageAnalyzer(LanguageAnalyzer):
11
+ """Go language analyzer.
12
+ Example:
13
+ >>> analyzer = GoLanguageAnalyzer()
14
+ >>> res = analyzer.parse(Path("main.go"), "package main\\nfunc main() {}")
15
+ >>> syms = list(analyzer.extract_symbols(res))
16
+ >>> len(syms) >= 1
17
+ True
18
+ """
19
+ language: ClassVar[str] = "go"
20
+ extensions: ClassVar[tuple[str, ...]] = (".go",)
21
+
22
+ def parse(self, path: Path, source: str) -> ParseResult:
23
+ """Parse Go source text.
24
+ Example:
25
+ >>> analyzer = GoLanguageAnalyzer()
26
+ >>> res = analyzer.parse(Path("main.go"), "package main")
27
+ >>> res.errors
28
+ []
29
+ """
30
+ content_hash = hashlib.sha256(source.encode("utf-8")).hexdigest()
31
+ return ParseResult(
32
+ file_path=str(path),
33
+ source=source,
34
+ ast=source,
35
+ content_hash=content_hash,
36
+ errors=[],
37
+ )
38
+
39
+ def extract_symbols(self, parse: ParseResult) -> Iterable[Symbol]:
40
+ """Extract Go packages, functions, structs, interfaces, and imports.
41
+ Example:
42
+ >>> analyzer = GoLanguageAnalyzer()
43
+ >>> res = analyzer.parse(Path("main.go"), "package main\\ntype User struct{ Name string }")
44
+ >>> syms = list(analyzer.extract_symbols(res))
45
+ >>> any(s.kind == SymbolKind.STRUCT for s in syms)
46
+ True
47
+ """
48
+ source = parse.source
49
+ file_path = parse.file_path
50
+ lines = source.splitlines()
51
+ max_line = len(lines) if lines else 1
52
+ symbols: List[Symbol] = []
53
+ file_sym = Symbol(
54
+ id=file_path,
55
+ name=Path(file_path).name,
56
+ qualified_name=file_path,
57
+ kind=SymbolKind.FILE,
58
+ language=self.language,
59
+ location=Location(file_path, 1, 0, max_line, 0),
60
+ parent_id=None,
61
+ signature=None,
62
+ visibility=Visibility.PUBLIC,
63
+ modifiers=(),
64
+ content_hash=parse.content_hash,
65
+ )
66
+ symbols.append(file_sym)
67
+ func_pattern = re.compile(r'func\s+(?:\(([^)]+)\)\s+)?([A-Za-z0-9_]+)\s*(?:\[[^\]]+\])?\s*\(([^)]*)\)')
68
+ struct_pattern = re.compile(r'type\s+([A-Za-z0-9_]+)\s*(?:\[[^\]]+\])?\s+struct\b')
69
+ interface_pattern = re.compile(r'type\s+([A-Za-z0-9_]+)\s*(?:\[[^\]]+\])?\s+interface\b')
70
+ for idx, line in enumerate(lines, start=1):
71
+ line_str = line.strip()
72
+ for match in func_pattern.finditer(line_str):
73
+ receiver = match.group(1)
74
+ name = match.group(2)
75
+ params = match.group(3)
76
+ sym_id = f"{file_path}::{name}"
77
+ end_line = self._find_block_end(lines, idx - 1)
78
+ loc = Location(file_path, idx, line.find(name), end_line, line.find(name) + len(name))
79
+ vis = Visibility.PUBLIC if name[0].isupper() else Visibility.PRIVATE
80
+ kind = SymbolKind.METHOD if receiver else SymbolKind.FUNCTION
81
+ symbols.append(Symbol(
82
+ id=sym_id,
83
+ name=name,
84
+ qualified_name=f"{receiver}.{name}" if receiver else name,
85
+ kind=kind,
86
+ language=self.language,
87
+ location=loc,
88
+ parent_id=file_path,
89
+ signature=f"func ({receiver or ''}) {name}({params})",
90
+ visibility=vis,
91
+ modifiers=(),
92
+ content_hash=self._extract_body_hash(lines, idx - 1),
93
+ ))
94
+ for match in struct_pattern.finditer(line_str):
95
+ name = match.group(1)
96
+ sym_id = f"{file_path}::{name}"
97
+ end_line = self._find_block_end(lines, idx - 1)
98
+ loc = Location(file_path, idx, line.find(name), end_line, line.find(name) + len(name))
99
+ vis = Visibility.PUBLIC if name[0].isupper() else Visibility.PRIVATE
100
+ symbols.append(Symbol(
101
+ id=sym_id,
102
+ name=name,
103
+ qualified_name=name,
104
+ kind=SymbolKind.STRUCT,
105
+ language=self.language,
106
+ location=loc,
107
+ parent_id=file_path,
108
+ signature=f"type {name} struct",
109
+ visibility=vis,
110
+ modifiers=(),
111
+ content_hash=self._extract_body_hash(lines, idx - 1),
112
+ ))
113
+ for match in interface_pattern.finditer(line_str):
114
+ name = match.group(1)
115
+ sym_id = f"{file_path}::{name}"
116
+ end_line = self._find_block_end(lines, idx - 1)
117
+ loc = Location(file_path, idx, line.find(name), end_line, line.find(name) + len(name))
118
+ vis = Visibility.PUBLIC if name[0].isupper() else Visibility.PRIVATE
119
+ symbols.append(Symbol(
120
+ id=sym_id,
121
+ name=name,
122
+ qualified_name=name,
123
+ kind=SymbolKind.INTERFACE,
124
+ language=self.language,
125
+ location=loc,
126
+ parent_id=file_path,
127
+ signature=f"type {name} interface",
128
+ visibility=vis,
129
+ modifiers=(),
130
+ content_hash=self._extract_body_hash(lines, idx - 1),
131
+ ))
132
+ return symbols
133
+
134
+ def extract_edges(self, parse: ParseResult, symbols: Sequence[Symbol]) -> Iterable[Edge]:
135
+ """Extract containment and call relationships in Go.
136
+ Example:
137
+ >>> analyzer = GoLanguageAnalyzer()
138
+ >>> res = analyzer.parse(Path("main.go"), "package main\\nfunc main() {}")
139
+ >>> syms = list(analyzer.extract_symbols(res))
140
+ >>> edges = list(analyzer.extract_edges(res, syms))
141
+ >>> len(edges) >= 1
142
+ True
143
+ """
144
+ edges: List[Edge] = []
145
+ sym_map = {s.id: s for s in symbols}
146
+ for s in symbols:
147
+ if s.parent_id and s.parent_id in sym_map:
148
+ edges.append(Edge(
149
+ source_id=s.parent_id,
150
+ target_id=s.id,
151
+ kind=EdgeKind.CONTAINS,
152
+ weight=1.0,
153
+ confidence=1.0,
154
+ location=s.location,
155
+ ))
156
+ return edges
157
+
158
+ def resolve_import(self, ref: ImportRef, ctx: ResolveContext) -> Iterable[str]:
159
+ """Resolve Go module import references.
160
+ Example:
161
+ >>> analyzer = GoLanguageAnalyzer()
162
+ >>> ctx = ResolveContext(Path("."))
163
+ >>> ref = ImportRef("main.go", "fmt", None, False)
164
+ >>> list(analyzer.resolve_import(ref, ctx))
165
+ []
166
+ """
167
+ return []
codeui/lang/python.py ADDED
@@ -0,0 +1,294 @@
1
+ """Python language adapter using standard library ast."""
2
+ import ast
3
+ import hashlib
4
+ import re
5
+ from pathlib import Path
6
+ from typing import Any, ClassVar, Dict, Iterable, List, Optional, Sequence, Tuple
7
+ from codeui.core.ir import Edge, EdgeKind, Location, Symbol, SymbolKind, Visibility
8
+ from codeui.core.resolver import ResolveContext
9
+ from codeui.lang.base import ImportRef, LanguageAnalyzer, ParseResult
10
+
11
+ class PythonLanguageAnalyzer(LanguageAnalyzer):
12
+ """Python language analyzer using standard library ast.
13
+ Example:
14
+ >>> analyzer = PythonLanguageAnalyzer()
15
+ >>> res = analyzer.parse(Path("test.py"), "def hello(): pass")
16
+ >>> syms = list(analyzer.extract_symbols(res))
17
+ >>> len(syms) >= 1
18
+ True
19
+ """
20
+ language: ClassVar[str] = "python"
21
+ extensions: ClassVar[tuple[str, ...]] = (".py", ".pyi")
22
+
23
+ def parse(self, path: Path, source: str) -> ParseResult:
24
+ """Parse Python source text into ast.AST object.
25
+ Example:
26
+ >>> analyzer = PythonLanguageAnalyzer()
27
+ >>> res = analyzer.parse(Path("a.py"), "x = 1")
28
+ >>> res.errors
29
+ []
30
+ """
31
+ content_hash = hashlib.sha256(source.encode("utf-8")).hexdigest()
32
+ errors: List[str] = []
33
+ parsed_ast: Any = None
34
+ try:
35
+ parsed_ast = ast.parse(source, filename=str(path))
36
+ except SyntaxError as e:
37
+ errors.append(f"SyntaxError at line {e.lineno}, col {e.offset}: {e.msg}")
38
+ return ParseResult(
39
+ file_path=str(path),
40
+ source=source,
41
+ ast=parsed_ast,
42
+ content_hash=content_hash,
43
+ errors=errors,
44
+ )
45
+
46
+ def extract_symbols(self, parse: ParseResult) -> Iterable[Symbol]:
47
+ """Extract all symbols from parsed Python AST.
48
+ Example:
49
+ >>> analyzer = PythonLanguageAnalyzer()
50
+ >>> res = analyzer.parse(Path("a.py"), "def foo(): pass")
51
+ >>> syms = list(analyzer.extract_symbols(res))
52
+ >>> syms[0].kind == SymbolKind.FILE
53
+ True
54
+ """
55
+ if parse.ast is None:
56
+ return []
57
+ symbols: List[Symbol] = []
58
+ file_path = parse.file_path
59
+ lines = parse.source.splitlines()
60
+ max_line = len(lines) if lines else 1
61
+ file_sym = Symbol(
62
+ id=file_path,
63
+ name=Path(file_path).name,
64
+ qualified_name=file_path,
65
+ kind=SymbolKind.FILE,
66
+ language=self.language,
67
+ location=Location(file_path, 1, 0, max_line, 0),
68
+ parent_id=None,
69
+ signature=None,
70
+ visibility=Visibility.PUBLIC,
71
+ modifiers=(),
72
+ content_hash=parse.content_hash,
73
+ )
74
+ symbols.append(file_sym)
75
+ self._walk_ast(parse.ast, file_path, file_path, "", symbols)
76
+ return symbols
77
+
78
+ def _walk_ast(self, node: ast.AST, file_path: str, parent_id: str, scope_prefix: str, symbols: List[Symbol]) -> None:
79
+ """Traverse AST nodes recursively to construct Symbol records."""
80
+ for child in ast.iter_child_nodes(node):
81
+ if isinstance(child, ast.ClassDef):
82
+ qname = f"{scope_prefix}.{child.name}" if scope_prefix else child.name
83
+ sym_id = f"{file_path}::{qname}"
84
+ loc = Location(
85
+ file_id=file_path,
86
+ start_line=child.lineno,
87
+ start_col=child.col_offset,
88
+ end_line=getattr(child, "end_lineno", child.lineno),
89
+ end_col=getattr(child, "end_col_offset", child.col_offset),
90
+ )
91
+ vis = Visibility.PRIVATE if child.name.startswith("_") else Visibility.PUBLIC
92
+ body_str = ast.dump(child)
93
+ s = Symbol(
94
+ id=sym_id,
95
+ name=child.name,
96
+ qualified_name=qname,
97
+ kind=SymbolKind.CLASS,
98
+ language=self.language,
99
+ location=loc,
100
+ parent_id=parent_id,
101
+ signature=f"class {child.name}",
102
+ visibility=vis,
103
+ modifiers=(),
104
+ content_hash=hashlib.sha256(body_str.encode()).hexdigest()[:16],
105
+ )
106
+ symbols.append(s)
107
+ self._walk_ast(child, file_path, sym_id, qname, symbols)
108
+ elif isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
109
+ qname = f"{scope_prefix}.{child.name}" if scope_prefix else child.name
110
+ sym_id = f"{file_path}::{qname}"
111
+ kind = SymbolKind.METHOD if scope_prefix and "." in scope_prefix or any(s.kind == SymbolKind.CLASS for s in symbols if s.id == parent_id) else SymbolKind.FUNCTION
112
+ loc = Location(
113
+ file_id=file_path,
114
+ start_line=child.lineno,
115
+ start_col=child.col_offset,
116
+ end_line=getattr(child, "end_lineno", child.lineno),
117
+ end_col=getattr(child, "end_col_offset", child.col_offset),
118
+ )
119
+ vis = Visibility.PRIVATE if child.name.startswith("_") else Visibility.PUBLIC
120
+ body_str = ast.dump(child)
121
+ s = Symbol(
122
+ id=sym_id,
123
+ name=child.name,
124
+ qualified_name=qname,
125
+ kind=kind,
126
+ language=self.language,
127
+ location=loc,
128
+ parent_id=parent_id,
129
+ signature=f"def {child.name}(...)",
130
+ visibility=vis,
131
+ modifiers=("async",) if isinstance(child, ast.AsyncFunctionDef) else (),
132
+ content_hash=hashlib.sha256(body_str.encode()).hexdigest()[:16],
133
+ )
134
+ symbols.append(s)
135
+ self._walk_ast(child, file_path, sym_id, qname, symbols)
136
+
137
+ def extract_edges(self, parse: ParseResult, symbols: Sequence[Symbol]) -> Iterable[Edge]:
138
+ """Extract relationship edges from parse AST and symbols.
139
+ Example:
140
+ >>> analyzer = PythonLanguageAnalyzer()
141
+ >>> res = analyzer.parse(Path("a.py"), "def f(): pass\\ndef g(): f()")
142
+ >>> syms = list(analyzer.extract_symbols(res))
143
+ >>> edges = list(analyzer.extract_edges(res, syms))
144
+ >>> any(e.source_id == "a.py::g" and e.target_id == "a.py::f" for e in edges)
145
+ True
146
+ """
147
+ if parse.ast is None:
148
+ return []
149
+ edges: List[Edge] = []
150
+ sym_map = {s.id: s for s in symbols}
151
+ for s in symbols:
152
+ if s.parent_id and s.parent_id in sym_map:
153
+ edges.append(Edge(
154
+ source_id=s.parent_id,
155
+ target_id=s.id,
156
+ kind=EdgeKind.CONTAINS,
157
+ weight=1.0,
158
+ confidence=1.0,
159
+ location=s.location,
160
+ ))
161
+ import_map: Dict[str, str] = {}
162
+ for node in ast.walk(parse.ast):
163
+ if isinstance(node, ast.Import):
164
+ for alias in node.names:
165
+ local_name = alias.asname or alias.name
166
+ import_map[local_name] = f"module::{alias.name}"
167
+ weight = float(self._count_symbol_usages(parse.source, local_name))
168
+ edges.append(Edge(
169
+ source_id=parse.file_path,
170
+ target_id=f"module::{alias.name}",
171
+ kind=EdgeKind.IMPORTS,
172
+ weight=weight,
173
+ confidence=1.0,
174
+ location=Location(parse.file_path, node.lineno, node.col_offset, node.lineno, node.col_offset),
175
+ ))
176
+ elif isinstance(node, ast.ImportFrom):
177
+ level = getattr(node, "level", 0) or 0
178
+ dots = "." * level if level > 0 else ""
179
+ raw_mod = node.module or ""
180
+ mod_name = f"{dots}{raw_mod}" if dots else raw_mod
181
+ for alias in node.names:
182
+ local_name = alias.asname or alias.name
183
+ target = f"module::{mod_name}.{alias.name}" if mod_name else f"module::{alias.name}"
184
+ import_map[local_name] = target
185
+ weight = float(self._count_symbol_usages(parse.source, local_name))
186
+ edges.append(Edge(
187
+ source_id=parse.file_path,
188
+ target_id=target,
189
+ kind=EdgeKind.IMPORTS,
190
+ weight=weight,
191
+ confidence=1.0,
192
+ location=Location(parse.file_path, node.lineno, node.col_offset, node.lineno, node.col_offset),
193
+ ))
194
+ class_names = {s.name for s in symbols if s.kind == SymbolKind.CLASS}
195
+ def walk_calls(node: ast.AST, current_scope: str, class_scope: str, locals_in_scope: set[str]) -> None:
196
+ for child in ast.iter_child_nodes(node):
197
+ if isinstance(child, ast.ClassDef):
198
+ class_sym_id = f"{parse.file_path}::{child.name}"
199
+ for base in child.bases:
200
+ if isinstance(base, ast.Name):
201
+ target_id = f"{parse.file_path}::{base.id}"
202
+ edges.append(Edge(
203
+ source_id=class_sym_id,
204
+ target_id=target_id,
205
+ kind=EdgeKind.INHERITS,
206
+ weight=1.0,
207
+ confidence=0.9,
208
+ location=Location(parse.file_path, base.lineno, base.col_offset, base.lineno, base.col_offset + len(base.id)),
209
+ ))
210
+ walk_calls(child, class_sym_id, child.name, set())
211
+ elif isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
212
+ qname = f"{class_scope}.{child.name}" if class_scope else child.name
213
+ func_sym_id = f"{parse.file_path}::{qname}"
214
+ new_locals = set(locals_in_scope)
215
+ for arg in getattr(child.args, "posonlyargs", []) + getattr(child.args, "args", []) + getattr(child.args, "kwonlyargs", []):
216
+ if hasattr(arg, "arg"):
217
+ new_locals.add(arg.arg)
218
+ if getattr(child.args, "vararg", None):
219
+ new_locals.add(child.args.vararg.arg)
220
+ if getattr(child.args, "kwarg", None):
221
+ new_locals.add(child.args.kwarg.arg)
222
+ for subnode in ast.walk(child):
223
+ if isinstance(subnode, ast.Assign):
224
+ for target in subnode.targets:
225
+ if isinstance(target, ast.Name):
226
+ new_locals.add(target.id)
227
+ elif isinstance(subnode, ast.AnnAssign):
228
+ if isinstance(subnode.target, ast.Name):
229
+ new_locals.add(subnode.target.id)
230
+ walk_calls(child, func_sym_id, class_scope, new_locals)
231
+ elif isinstance(child, ast.Call):
232
+ caller_id = current_scope if current_scope else parse.file_path
233
+ if isinstance(child.func, ast.Name):
234
+ callee_name = child.func.id
235
+ if callee_name not in locals_in_scope:
236
+ target_id = import_map.get(callee_name, f"{parse.file_path}::{callee_name}")
237
+ weight = float(self._count_symbol_usages(parse.source, callee_name))
238
+ edges.append(Edge(
239
+ source_id=caller_id,
240
+ target_id=target_id,
241
+ kind=EdgeKind.CALLS,
242
+ weight=weight,
243
+ confidence=0.85,
244
+ location=Location(parse.file_path, child.lineno, child.col_offset, child.lineno, child.col_offset + len(callee_name)),
245
+ ))
246
+ elif isinstance(child.func, ast.Attribute):
247
+ attr_name = child.func.attr
248
+ target_id = None
249
+ if isinstance(child.func.value, ast.Name):
250
+ val_name = child.func.value.id
251
+ if val_name in ("self", "cls") and class_scope:
252
+ target_id = f"{parse.file_path}::{class_scope}.{attr_name}"
253
+ elif val_name in import_map:
254
+ target_id = f"{import_map[val_name]}.{attr_name}"
255
+ elif val_name in class_names:
256
+ target_id = f"{parse.file_path}::{val_name}.{attr_name}"
257
+ if target_id:
258
+ weight = float(self._count_symbol_usages(parse.source, attr_name))
259
+ edges.append(Edge(
260
+ source_id=caller_id,
261
+ target_id=target_id,
262
+ kind=EdgeKind.CALLS,
263
+ weight=weight,
264
+ confidence=0.8,
265
+ location=Location(parse.file_path, child.lineno, child.col_offset, child.lineno, child.col_offset + len(attr_name)),
266
+ ))
267
+ walk_calls(child, current_scope, class_scope, locals_in_scope)
268
+ else:
269
+ walk_calls(child, current_scope, class_scope, locals_in_scope)
270
+ walk_calls(parse.ast, "", "", set())
271
+ return edges
272
+
273
+ def _count_symbol_usages(self, source: str, symbol_name: str) -> int:
274
+ if not symbol_name or symbol_name == "*":
275
+ return 1
276
+ pattern = r"\b" + re.escape(symbol_name) + r"\b"
277
+ count = 0
278
+ for line in source.splitlines():
279
+ stripped = line.strip()
280
+ if stripped.startswith(("import ", "from ", "#")):
281
+ continue
282
+ count += len(re.findall(pattern, line))
283
+ return max(1, count)
284
+
285
+ def resolve_import(self, ref: ImportRef, ctx: ResolveContext) -> Iterable[str]:
286
+ """Resolve Python import module path to candidate files.
287
+ Example:
288
+ >>> analyzer = PythonLanguageAnalyzer()
289
+ >>> ctx = ResolveContext(Path("."))
290
+ >>> ref = ImportRef("app.py", "utils", None, False)
291
+ >>> isinstance(list(analyzer.resolve_import(ref, ctx)), list)
292
+ True
293
+ """
294
+ return ctx.resolve_path(ref.file_path, ref.module_name, "python")
@@ -0,0 +1,83 @@
1
+ """Registry mapping file paths to appropriate language analyzers."""
2
+ from pathlib import Path
3
+ from typing import Dict, List, Optional, Type
4
+ from codeui.lang.base import LanguageAnalyzer
5
+ from codeui.lang.python import PythonLanguageAnalyzer
6
+ from codeui.lang.ts import TSLanguageAnalyzer
7
+ from codeui.lang.go import GoLanguageAnalyzer
8
+ from codeui.lang.rust import RustLanguageAnalyzer
9
+ from codeui.lang.generic import GenericLanguageAnalyzer
10
+
11
+ class LanguageRegistry:
12
+ """Registry maintaining active language adapters.
13
+ Example:
14
+ >>> reg = LanguageRegistry()
15
+ >>> analyzer = reg.get_analyzer(Path("app.py"))
16
+ >>> analyzer.language
17
+ 'python'
18
+ """
19
+ def __init__(self) -> None:
20
+ self._analyzers: List[LanguageAnalyzer] = [
21
+ PythonLanguageAnalyzer(),
22
+ TSLanguageAnalyzer(),
23
+ GoLanguageAnalyzer(),
24
+ RustLanguageAnalyzer(),
25
+ GenericLanguageAnalyzer(),
26
+ ]
27
+ self._ext_map: Dict[str, LanguageAnalyzer] = {}
28
+ for analyzer in self._analyzers:
29
+ for ext in analyzer.extensions:
30
+ self._ext_map[ext.lower()] = analyzer
31
+
32
+ def register(self, analyzer: LanguageAnalyzer) -> None:
33
+ """Register a custom language analyzer.
34
+ Example:
35
+ >>> reg = LanguageRegistry()
36
+ >>> reg.register(GenericLanguageAnalyzer())
37
+ """
38
+ self._analyzers.append(analyzer)
39
+ for ext in analyzer.extensions:
40
+ self._ext_map[ext.lower()] = analyzer
41
+
42
+ def get_analyzer(self, path: Path) -> LanguageAnalyzer:
43
+ """Find language analyzer matching file extension or filename.
44
+ Example:
45
+ >>> reg = LanguageRegistry()
46
+ >>> reg.get_analyzer(Path("main.ts")).language
47
+ 'typescript'
48
+ """
49
+ ext = path.suffix.lower()
50
+ if not ext and path.name:
51
+ ext = path.name.lower()
52
+ return self._ext_map.get(ext, self._analyzers[-1])
53
+
54
+ def supported_languages(self) -> List[str]:
55
+ """List registered supported languages.
56
+ Example:
57
+ >>> reg = LanguageRegistry()
58
+ >>> 'python' in reg.supported_languages()
59
+ True
60
+ """
61
+ return sorted(list(set(a.language for a in self._analyzers)))
62
+
63
+ def get_supported_extensions(self) -> set[str]:
64
+ """Return file extensions supported by first-class AST language analyzers.
65
+ Example:
66
+ >>> reg = LanguageRegistry()
67
+ >>> '.py' in reg.get_supported_extensions()
68
+ True
69
+ """
70
+ exts: set[str] = set()
71
+ for a in self._analyzers:
72
+ if getattr(a, "has_ast_support", True) and getattr(a, "is_supported", True) and a.language != "generic":
73
+ exts.update(a.extensions)
74
+ return exts
75
+
76
+ def get_all_extensions(self) -> set[str]:
77
+ """Return all registered file extensions including fallback analyzers.
78
+ Example:
79
+ >>> reg = LanguageRegistry()
80
+ >>> '.py' in reg.get_all_extensions()
81
+ True
82
+ """
83
+ return set(self._ext_map.keys())