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.
- codeui/__init__.py +23 -0
- codeui/agent/__init__.py +15 -0
- codeui/agent/context.py +207 -0
- codeui/agent/contribution.py +224 -0
- codeui/agent/edit_tools.py +280 -0
- codeui/agent/integrations.py +243 -0
- codeui/agent/mcp_server.py +94 -0
- codeui/analysis/__init__.py +1 -0
- codeui/analysis/api_drift.py +79 -0
- codeui/analysis/base.py +42 -0
- codeui/analysis/circular.py +116 -0
- codeui/analysis/dead_code.py +63 -0
- codeui/analysis/duplicates.py +69 -0
- codeui/analysis/runner.py +57 -0
- codeui/analysis/shadowing.py +49 -0
- codeui/analysis/undefined.py +97 -0
- codeui/analysis/unreachable.py +28 -0
- codeui/analysis/unresolved_import.py +153 -0
- codeui/cli/__init__.py +1 -0
- codeui/cli/main.py +437 -0
- codeui/core/__init__.py +1 -0
- codeui/core/cache.py +135 -0
- codeui/core/graph.py +435 -0
- codeui/core/ir.py +218 -0
- codeui/core/override.py +230 -0
- codeui/core/repo.py +85 -0
- codeui/core/resolver.py +281 -0
- codeui/errors.py +98 -0
- codeui/lang/__init__.py +1 -0
- codeui/lang/base.py +117 -0
- codeui/lang/generic.py +90 -0
- codeui/lang/go.py +167 -0
- codeui/lang/python.py +294 -0
- codeui/lang/registry.py +83 -0
- codeui/lang/rust.py +185 -0
- codeui/lang/ts.py +396 -0
- codeui/plugins/__init__.py +1 -0
- codeui/plugins/registry.py +31 -0
- codeui/report/__init__.py +1 -0
- codeui/report/json_emitter.py +24 -0
- codeui/report/markdown_emitter.py +33 -0
- codeui/report/sarif_emitter.py +56 -0
- codeui/server/__init__.py +1 -0
- codeui/server/server.py +705 -0
- codeui/tracer/__init__.py +1 -0
- codeui/tracer/tracer.py +147 -0
- codeui_python-0.1.0.dist-info/METADATA +191 -0
- codeui_python-0.1.0.dist-info/RECORD +52 -0
- codeui_python-0.1.0.dist-info/WHEEL +5 -0
- codeui_python-0.1.0.dist-info/entry_points.txt +2 -0
- codeui_python-0.1.0.dist-info/licenses/LICENSE +21 -0
- codeui_python-0.1.0.dist-info/top_level.txt +1 -0
codeui/core/resolver.py
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"""Project structure and import reference resolution helper."""
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Dict, Iterable, List, Optional, Set, Tuple
|
|
5
|
+
|
|
6
|
+
class ResolveContext:
|
|
7
|
+
"""Context container for cross-file and cross-language import resolution.
|
|
8
|
+
Example:
|
|
9
|
+
>>> ctx = ResolveContext(Path("."))
|
|
10
|
+
>>> isinstance(ctx.root_path, Path)
|
|
11
|
+
True
|
|
12
|
+
"""
|
|
13
|
+
def __init__(self, root_path: Path) -> None:
|
|
14
|
+
self.root_path = root_path.resolve()
|
|
15
|
+
self.root_dir = self.root_path
|
|
16
|
+
self.alias_mappings: Dict[str, str] = {}
|
|
17
|
+
self.workspace_packages: Dict[str, Path] = {}
|
|
18
|
+
self.entry_points: List[str] = []
|
|
19
|
+
self.layer_rules: Dict[str, Any] = {}
|
|
20
|
+
self._resolve_cache: Dict[Tuple[str, str, str], List[str]] = {}
|
|
21
|
+
self._known_files: Optional[Set[str]] = None
|
|
22
|
+
self._search_roots: List[Path] = [self.root_path]
|
|
23
|
+
self._load_configs()
|
|
24
|
+
|
|
25
|
+
def set_known_files(self, files: Iterable[str]) -> None:
|
|
26
|
+
"""Register known project files to accelerate import resolution without disk checks.
|
|
27
|
+
Example:
|
|
28
|
+
>>> ctx = ResolveContext(Path("."))
|
|
29
|
+
>>> ctx.set_known_files(["src/index.ts"])
|
|
30
|
+
>>> ctx.has_known_file("src/index.ts")
|
|
31
|
+
True
|
|
32
|
+
"""
|
|
33
|
+
self._known_files = {f.replace("\\", "/").lstrip("./") for f in files}
|
|
34
|
+
|
|
35
|
+
def has_known_file(self, file_path: str) -> bool:
|
|
36
|
+
"""Check if relative path exists in registered known files.
|
|
37
|
+
Example:
|
|
38
|
+
>>> ctx = ResolveContext(Path("."))
|
|
39
|
+
>>> ctx.set_known_files(["a.py"])
|
|
40
|
+
>>> ctx.has_known_file("a.py")
|
|
41
|
+
True
|
|
42
|
+
"""
|
|
43
|
+
if self._known_files is None:
|
|
44
|
+
return (self.root_path / file_path).exists()
|
|
45
|
+
return file_path.replace("\\", "/").lstrip("./") in self._known_files
|
|
46
|
+
|
|
47
|
+
def _load_configs(self) -> None:
|
|
48
|
+
"""Discover tsconfig.json, package.json, codeui.json, and pyproject.toml configurations."""
|
|
49
|
+
self._search_roots = [self.root_path]
|
|
50
|
+
try:
|
|
51
|
+
for entry in self.root_path.iterdir():
|
|
52
|
+
if entry.is_dir() and not entry.name.startswith((".", "__")) and entry.name not in ("node_modules", "dist", "build", "target", "vendor", "venv", ".venv"):
|
|
53
|
+
self._search_roots.append(entry)
|
|
54
|
+
except Exception:
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
codeui_config = self.root_path / ".codeui.json"
|
|
58
|
+
if not codeui_config.exists():
|
|
59
|
+
codeui_config = self.root_path / "codeui.json"
|
|
60
|
+
if codeui_config.exists():
|
|
61
|
+
try:
|
|
62
|
+
cdata = json.loads(codeui_config.read_text(encoding="utf-8"))
|
|
63
|
+
if "entry" in cdata:
|
|
64
|
+
entries = cdata["entry"] if isinstance(cdata["entry"], list) else [cdata["entry"]]
|
|
65
|
+
self.entry_points.extend(entries)
|
|
66
|
+
if "entries" in cdata:
|
|
67
|
+
self.entry_points.extend(cdata["entries"])
|
|
68
|
+
if "layers" in cdata and isinstance(cdata["layers"], dict):
|
|
69
|
+
self.layer_rules.update(cdata["layers"])
|
|
70
|
+
except Exception:
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
tsconfig_path = self.root_path / "tsconfig.json"
|
|
74
|
+
if tsconfig_path.exists():
|
|
75
|
+
try:
|
|
76
|
+
content = tsconfig_path.read_text(encoding="utf-8")
|
|
77
|
+
data = json.loads(content)
|
|
78
|
+
compiler_opts = data.get("compilerOptions", {})
|
|
79
|
+
baseUrl = compiler_opts.get("baseUrl", ".")
|
|
80
|
+
base_dir = (self.root_path / baseUrl).resolve()
|
|
81
|
+
paths = compiler_opts.get("paths", {})
|
|
82
|
+
for alias, targets in paths.items():
|
|
83
|
+
alias_clean = alias.rstrip("*").rstrip("/")
|
|
84
|
+
if targets and isinstance(targets, list):
|
|
85
|
+
target_clean = targets[0].rstrip("*").rstrip("/")
|
|
86
|
+
resolved_target = (base_dir / target_clean).resolve()
|
|
87
|
+
self.alias_mappings[alias_clean] = str(resolved_target)
|
|
88
|
+
except Exception:
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
package_json = self.root_path / "package.json"
|
|
92
|
+
if not package_json.exists() and self.root_path.parent != self.root_path:
|
|
93
|
+
parent_pkg = self.root_path.parent / "package.json"
|
|
94
|
+
if parent_pkg.exists():
|
|
95
|
+
package_json = parent_pkg
|
|
96
|
+
if package_json.exists():
|
|
97
|
+
try:
|
|
98
|
+
data = json.loads(package_json.read_text(encoding="utf-8"))
|
|
99
|
+
cui = data.get("codeui") or data.get("appConfig")
|
|
100
|
+
if cui and isinstance(cui, dict):
|
|
101
|
+
if "entry" in cui:
|
|
102
|
+
entries = cui["entry"] if isinstance(cui["entry"], list) else [cui["entry"]]
|
|
103
|
+
self.entry_points.extend(entries)
|
|
104
|
+
if "entries" in cui:
|
|
105
|
+
self.entry_points.extend(cui["entries"])
|
|
106
|
+
if "layers" in cui and isinstance(cui["layers"], dict):
|
|
107
|
+
self.layer_rules.update(cui["layers"])
|
|
108
|
+
workspaces = data.get("workspaces", [])
|
|
109
|
+
if isinstance(workspaces, list):
|
|
110
|
+
for ws_pattern in workspaces:
|
|
111
|
+
ws_clean = ws_pattern.replace("/*", "")
|
|
112
|
+
ws_dir = self.root_path / ws_clean
|
|
113
|
+
if ws_dir.exists() and ws_dir.is_dir():
|
|
114
|
+
for sub in ws_dir.iterdir():
|
|
115
|
+
sub_pkg = sub / "package.json"
|
|
116
|
+
if sub_pkg.exists():
|
|
117
|
+
sub_data = json.loads(sub_pkg.read_text(encoding="utf-8"))
|
|
118
|
+
pkg_name = sub_data.get("name")
|
|
119
|
+
if pkg_name:
|
|
120
|
+
self.workspace_packages[pkg_name] = sub.resolve()
|
|
121
|
+
except Exception:
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
def resolve_path(self, current_file: str, import_path: str, language: str = "") -> List[str]:
|
|
125
|
+
"""Resolve import reference string to candidate file paths relative to root.
|
|
126
|
+
Example:
|
|
127
|
+
>>> ctx = ResolveContext(Path("."))
|
|
128
|
+
>>> candidates = ctx.resolve_path("src/app.ts", "./utils", "typescript")
|
|
129
|
+
>>> isinstance(candidates, list)
|
|
130
|
+
True
|
|
131
|
+
"""
|
|
132
|
+
cache_key = (current_file, import_path, language)
|
|
133
|
+
if cache_key in self._resolve_cache:
|
|
134
|
+
return self._resolve_cache[cache_key]
|
|
135
|
+
|
|
136
|
+
resolved: List[str] = []
|
|
137
|
+
current_dir = (self.root_path / Path(current_file).parent).resolve() if current_file else self.root_path
|
|
138
|
+
clean_mod = import_path.replace("module::", "").replace("import:", "").strip()
|
|
139
|
+
|
|
140
|
+
if clean_mod.startswith("."):
|
|
141
|
+
num_dots = len(clean_mod) - len(clean_mod.lstrip("."))
|
|
142
|
+
rest = clean_mod.lstrip(".")
|
|
143
|
+
target_dir = current_dir
|
|
144
|
+
for _ in range(num_dots - 1):
|
|
145
|
+
if target_dir.parent and target_dir.parent != target_dir:
|
|
146
|
+
target_dir = target_dir.parent
|
|
147
|
+
|
|
148
|
+
parts = [p for p in rest.split(".") if p]
|
|
149
|
+
if not parts:
|
|
150
|
+
candidate_paths = [target_dir / "__init__.py", target_dir / "index.ts", target_dir / "index.js"]
|
|
151
|
+
else:
|
|
152
|
+
sub_str = "/".join(parts)
|
|
153
|
+
parent_sub = "/".join(parts[:-1]) if len(parts) > 1 else ""
|
|
154
|
+
candidate_paths = [
|
|
155
|
+
target_dir / f"{sub_str}.py",
|
|
156
|
+
target_dir / f"{sub_str}.ts",
|
|
157
|
+
target_dir / f"{sub_str}.js",
|
|
158
|
+
target_dir / sub_str / "__init__.py",
|
|
159
|
+
target_dir / sub_str / "index.ts",
|
|
160
|
+
target_dir / sub_str / "index.js",
|
|
161
|
+
]
|
|
162
|
+
if parent_sub:
|
|
163
|
+
candidate_paths.extend([
|
|
164
|
+
target_dir / f"{parent_sub}.py",
|
|
165
|
+
target_dir / f"{parent_sub}.ts",
|
|
166
|
+
target_dir / f"{parent_sub}.js",
|
|
167
|
+
target_dir / parent_sub / "__init__.py",
|
|
168
|
+
])
|
|
169
|
+
for cp in candidate_paths:
|
|
170
|
+
if self._known_files is not None:
|
|
171
|
+
try:
|
|
172
|
+
rel = str(cp.relative_to(self.root_path)).replace("\\", "/")
|
|
173
|
+
if rel in self._known_files:
|
|
174
|
+
resolved.append(rel)
|
|
175
|
+
except (ValueError, RuntimeError):
|
|
176
|
+
pass
|
|
177
|
+
else:
|
|
178
|
+
if cp.exists() and cp.is_file():
|
|
179
|
+
try:
|
|
180
|
+
resolved.append(str(cp.resolve().relative_to(self.root_path)).replace("\\", "/"))
|
|
181
|
+
except (ValueError, RuntimeError):
|
|
182
|
+
pass
|
|
183
|
+
|
|
184
|
+
for alias, target in self.alias_mappings.items():
|
|
185
|
+
if import_path.startswith(alias):
|
|
186
|
+
rel = import_path[len(alias):].lstrip("/")
|
|
187
|
+
base = Path(target) / rel
|
|
188
|
+
from codeui.lang.registry import LanguageRegistry
|
|
189
|
+
extensions = sorted(list(LanguageRegistry().get_supported_extensions() | {".ts", ".tsx", ".js", ".jsx", ".py"}))
|
|
190
|
+
for ext in extensions:
|
|
191
|
+
p = base.with_suffix(ext)
|
|
192
|
+
if self._known_files is not None:
|
|
193
|
+
try:
|
|
194
|
+
rel_p = str(p.relative_to(self.root_path)).replace("\\", "/")
|
|
195
|
+
if rel_p in self._known_files:
|
|
196
|
+
resolved.append(rel_p)
|
|
197
|
+
except (ValueError, RuntimeError):
|
|
198
|
+
pass
|
|
199
|
+
else:
|
|
200
|
+
if p.exists() and p.is_file():
|
|
201
|
+
try:
|
|
202
|
+
resolved.append(str(p.resolve().relative_to(self.root_path)).replace("\\", "/"))
|
|
203
|
+
except (ValueError, RuntimeError):
|
|
204
|
+
pass
|
|
205
|
+
|
|
206
|
+
for pkg_name, pkg_path in self.workspace_packages.items():
|
|
207
|
+
if import_path == pkg_name or import_path.startswith(pkg_name + "/"):
|
|
208
|
+
rel = import_path[len(pkg_name):].lstrip("/")
|
|
209
|
+
base = pkg_path / (rel if rel else "src/index.ts")
|
|
210
|
+
if self._known_files is not None:
|
|
211
|
+
try:
|
|
212
|
+
rel_base = str(base.relative_to(self.root_path)).replace("\\", "/")
|
|
213
|
+
if rel_base in self._known_files:
|
|
214
|
+
resolved.append(rel_base)
|
|
215
|
+
except (ValueError, RuntimeError):
|
|
216
|
+
pass
|
|
217
|
+
else:
|
|
218
|
+
if base.exists() and base.is_file():
|
|
219
|
+
try:
|
|
220
|
+
resolved.append(str(base.resolve().relative_to(self.root_path)).replace("\\", "/"))
|
|
221
|
+
except (ValueError, RuntimeError):
|
|
222
|
+
pass
|
|
223
|
+
|
|
224
|
+
clean_mod = import_path.replace("module::", "").replace("import:", "").strip()
|
|
225
|
+
if not clean_mod.startswith("."):
|
|
226
|
+
mod_path_str = clean_mod.replace(".", "/")
|
|
227
|
+
parts = [p for p in mod_path_str.split("/") if p]
|
|
228
|
+
from codeui.lang.registry import LanguageRegistry
|
|
229
|
+
code_extensions = sorted(list(LanguageRegistry().get_supported_extensions() | {".py", ".ts", ".tsx", ".js", ".jsx", ".go", ".rs", ".mjs", ".cjs"}))
|
|
230
|
+
index_filenames = ["__init__.py", "index.ts", "index.tsx", "index.js", "index.jsx", "mod.rs", "lib.rs"]
|
|
231
|
+
|
|
232
|
+
if self._known_files is not None:
|
|
233
|
+
search_root_prefixes = [""] + [f"{r.name}/" for r in self._search_roots if r != self.root_path]
|
|
234
|
+
for i in range(len(parts)):
|
|
235
|
+
sub_str = "/".join(parts[i:])
|
|
236
|
+
parent_sub = "/".join(parts[i:-1]) if len(parts[i:]) > 1 else ""
|
|
237
|
+
for prefix in search_root_prefixes:
|
|
238
|
+
for ext in code_extensions:
|
|
239
|
+
cand1 = f"{prefix}{sub_str}{ext}"
|
|
240
|
+
if cand1 in self._known_files:
|
|
241
|
+
resolved.append(cand1)
|
|
242
|
+
if parent_sub:
|
|
243
|
+
cand2 = f"{prefix}{parent_sub}{ext}"
|
|
244
|
+
if cand2 in self._known_files:
|
|
245
|
+
resolved.append(cand2)
|
|
246
|
+
for idx_name in index_filenames:
|
|
247
|
+
cand3 = f"{prefix}{sub_str}/{idx_name}"
|
|
248
|
+
if cand3 in self._known_files:
|
|
249
|
+
resolved.append(cand3)
|
|
250
|
+
if parent_sub:
|
|
251
|
+
cand4 = f"{prefix}{parent_sub}/{idx_name}"
|
|
252
|
+
if cand4 in self._known_files:
|
|
253
|
+
resolved.append(cand4)
|
|
254
|
+
if resolved:
|
|
255
|
+
break
|
|
256
|
+
else:
|
|
257
|
+
candidate_paths: List[Path] = []
|
|
258
|
+
for i in range(len(parts)):
|
|
259
|
+
sub_parts = parts[i:]
|
|
260
|
+
sub_str = "/".join(sub_parts)
|
|
261
|
+
parent_sub = "/".join(sub_parts[:-1]) if len(sub_parts) > 1 else ""
|
|
262
|
+
for root in self._search_roots:
|
|
263
|
+
for ext in code_extensions:
|
|
264
|
+
candidate_paths.append(root / f"{sub_str}{ext}")
|
|
265
|
+
if parent_sub:
|
|
266
|
+
candidate_paths.append(root / f"{parent_sub}{ext}")
|
|
267
|
+
for idx_name in index_filenames:
|
|
268
|
+
candidate_paths.append(root / sub_str / idx_name)
|
|
269
|
+
if parent_sub:
|
|
270
|
+
candidate_paths.append(root / parent_sub / idx_name)
|
|
271
|
+
|
|
272
|
+
for cp in candidate_paths:
|
|
273
|
+
if cp.exists() and cp.is_file():
|
|
274
|
+
try:
|
|
275
|
+
resolved.append(str(cp.resolve().relative_to(self.root_path)).replace("\\", "/"))
|
|
276
|
+
except (ValueError, RuntimeError):
|
|
277
|
+
pass
|
|
278
|
+
|
|
279
|
+
result = list(dict.fromkeys(resolved))
|
|
280
|
+
self._resolve_cache[cache_key] = result
|
|
281
|
+
return result
|
codeui/errors.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Typed exception hierarchy for codeui errors."""
|
|
2
|
+
|
|
3
|
+
class CodeUIError(Exception):
|
|
4
|
+
"""Base class for all codeui exceptions.
|
|
5
|
+
Example:
|
|
6
|
+
>>> err = CodeUIError("An error occurred")
|
|
7
|
+
>>> str(err)
|
|
8
|
+
'An error occurred'
|
|
9
|
+
"""
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
class SymbolNotFoundError(CodeUIError):
|
|
13
|
+
"""Raised when a requested symbol cannot be found in the graph.
|
|
14
|
+
Example:
|
|
15
|
+
>>> err = SymbolNotFoundError("sym_123")
|
|
16
|
+
>>> err.symbol_id
|
|
17
|
+
'sym_123'
|
|
18
|
+
"""
|
|
19
|
+
def __init__(self, symbol_id: str) -> None:
|
|
20
|
+
super().__init__(f"Symbol not found: {symbol_id}")
|
|
21
|
+
self.symbol_id = symbol_id
|
|
22
|
+
|
|
23
|
+
class FileNotFoundInGraphError(CodeUIError):
|
|
24
|
+
"""Raised when a requested file cannot be found in the graph.
|
|
25
|
+
Example:
|
|
26
|
+
>>> err = FileNotFoundInGraphError("src/index.ts")
|
|
27
|
+
>>> err.file_id
|
|
28
|
+
'src/index.ts'
|
|
29
|
+
"""
|
|
30
|
+
def __init__(self, file_id: str) -> None:
|
|
31
|
+
super().__init__(f"File not found in graph: {file_id}")
|
|
32
|
+
self.file_id = file_id
|
|
33
|
+
|
|
34
|
+
class OverrideConflictError(CodeUIError):
|
|
35
|
+
"""Raised when an override target has changed on disk.
|
|
36
|
+
Example:
|
|
37
|
+
>>> err = OverrideConflictError("src/app.ts", "hash_a", "hash_b")
|
|
38
|
+
>>> err.file_path
|
|
39
|
+
'src/app.ts'
|
|
40
|
+
"""
|
|
41
|
+
def __init__(self, file_path: str, expected_hash: str, actual_hash: str) -> None:
|
|
42
|
+
super().__init__(f"Override conflict on {file_path}: expected {expected_hash}, got {actual_hash}")
|
|
43
|
+
self.file_path = file_path
|
|
44
|
+
self.expected_hash = expected_hash
|
|
45
|
+
self.actual_hash = actual_hash
|
|
46
|
+
|
|
47
|
+
class ValidationError(CodeUIError):
|
|
48
|
+
"""Raised when edit validation or IR validation fails.
|
|
49
|
+
Example:
|
|
50
|
+
>>> err = ValidationError("Syntax error in edit")
|
|
51
|
+
>>> str(err)
|
|
52
|
+
'Syntax error in edit'
|
|
53
|
+
"""
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
class SecurityError(CodeUIError):
|
|
57
|
+
"""Raised when path traversal or CSRF security checks fail.
|
|
58
|
+
Example:
|
|
59
|
+
>>> err = SecurityError("Path traversal blocked")
|
|
60
|
+
>>> str(err)
|
|
61
|
+
'Path traversal blocked'
|
|
62
|
+
"""
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
class ParseError(CodeUIError):
|
|
66
|
+
"""Raised when language parsing fails.
|
|
67
|
+
Example:
|
|
68
|
+
>>> err = ParseError("main.py", 10, 5, "Unexpected token")
|
|
69
|
+
>>> err.line
|
|
70
|
+
10
|
|
71
|
+
"""
|
|
72
|
+
def __init__(self, file_path: str, line: int, col: int, message: str) -> None:
|
|
73
|
+
super().__init__(f"Parse error in {file_path}:{line}:{col}: {message}")
|
|
74
|
+
self.file_path = file_path
|
|
75
|
+
self.line = line
|
|
76
|
+
self.col = col
|
|
77
|
+
self.message = message
|
|
78
|
+
|
|
79
|
+
class RepositoryCloneError(CodeUIError):
|
|
80
|
+
"""Raised when cloning a remote repository fails.
|
|
81
|
+
Example:
|
|
82
|
+
>>> err = RepositoryCloneError("https://github.com/invalid/repo", "fatal: repository not found")
|
|
83
|
+
>>> err.repo_url
|
|
84
|
+
'https://github.com/invalid/repo'
|
|
85
|
+
"""
|
|
86
|
+
def __init__(self, repo_url: str, message: str) -> None:
|
|
87
|
+
super().__init__(f"Failed to clone repository '{repo_url}': {message}")
|
|
88
|
+
self.repo_url = repo_url
|
|
89
|
+
self.message = message
|
|
90
|
+
|
|
91
|
+
class ContributionError(CodeUIError):
|
|
92
|
+
"""Raised when AI agent contribution or git mutation fails.
|
|
93
|
+
Example:
|
|
94
|
+
>>> err = ContributionError("Failed to push changes")
|
|
95
|
+
>>> str(err)
|
|
96
|
+
'Failed to push changes'
|
|
97
|
+
"""
|
|
98
|
+
pass
|
codeui/lang/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Language adapters package for codeui."""
|
codeui/lang/base.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Base interface for language AST adapters."""
|
|
2
|
+
import hashlib
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, ClassVar, Iterable, List, Sequence
|
|
7
|
+
from codeui.core.ir import Edge, Location, Symbol
|
|
8
|
+
from codeui.core.resolver import ResolveContext
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class ParseResult:
|
|
12
|
+
"""Encapsulates parse AST result and errors.
|
|
13
|
+
Example:
|
|
14
|
+
>>> res = ParseResult("app.py", "x = 1", None, "hash1", [])
|
|
15
|
+
>>> res.file_path
|
|
16
|
+
'app.py'
|
|
17
|
+
"""
|
|
18
|
+
file_path: str
|
|
19
|
+
source: str
|
|
20
|
+
ast: Any
|
|
21
|
+
content_hash: str
|
|
22
|
+
errors: List[str]
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class ImportRef:
|
|
26
|
+
"""Import reference descriptor.
|
|
27
|
+
Example:
|
|
28
|
+
>>> ref = ImportRef("app.py", "math", None, False)
|
|
29
|
+
>>> ref.module_name
|
|
30
|
+
'math'
|
|
31
|
+
"""
|
|
32
|
+
file_path: str
|
|
33
|
+
module_name: str
|
|
34
|
+
symbol_name: str | None
|
|
35
|
+
is_relative: bool
|
|
36
|
+
|
|
37
|
+
class LanguageAnalyzer(ABC):
|
|
38
|
+
"""Abstract base class for language analyzers.
|
|
39
|
+
Example:
|
|
40
|
+
>>> class MockLang(LanguageAnalyzer):
|
|
41
|
+
... language = "mock"
|
|
42
|
+
... extensions = (".mock",)
|
|
43
|
+
... def parse(self, path, source): return ParseResult(str(path), source, None, "h", [])
|
|
44
|
+
... def extract_symbols(self, parse): return []
|
|
45
|
+
... def extract_edges(self, parse, symbols): return []
|
|
46
|
+
... def resolve_import(self, ref, ctx): return []
|
|
47
|
+
>>> mock = MockLang()
|
|
48
|
+
>>> mock.language
|
|
49
|
+
'mock'
|
|
50
|
+
"""
|
|
51
|
+
language: ClassVar[str]
|
|
52
|
+
extensions: ClassVar[tuple[str, ...]]
|
|
53
|
+
has_ast_support: ClassVar[bool] = True
|
|
54
|
+
is_supported: ClassVar[bool] = True
|
|
55
|
+
|
|
56
|
+
@abstractmethod
|
|
57
|
+
def parse(self, path: Path, source: str) -> ParseResult:
|
|
58
|
+
"""Parse source code into AST payload."""
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
@abstractmethod
|
|
62
|
+
def extract_symbols(self, parse: ParseResult) -> Iterable[Symbol]:
|
|
63
|
+
"""Extract symbol hierarchy from parse result."""
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
@abstractmethod
|
|
67
|
+
def extract_edges(self, parse: ParseResult, symbols: Sequence[Symbol]) -> Iterable[Edge]:
|
|
68
|
+
"""Extract relationship edges between symbols."""
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
@abstractmethod
|
|
72
|
+
def resolve_import(self, ref: ImportRef, ctx: ResolveContext) -> Iterable[str]:
|
|
73
|
+
"""Resolve import reference to candidate project files."""
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
def _extract_body_hash(self, lines: Sequence[str], start_idx: int) -> str:
|
|
77
|
+
"""Extract a deterministic hash of code body starting at start_idx."""
|
|
78
|
+
snippet_lines = []
|
|
79
|
+
brace_count = 0
|
|
80
|
+
found_open = False
|
|
81
|
+
for i in range(start_idx, min(len(lines), start_idx + 80)):
|
|
82
|
+
l = lines[i]
|
|
83
|
+
snippet_lines.append(l)
|
|
84
|
+
brace_count += l.count("{") - l.count("}")
|
|
85
|
+
if "{" in l:
|
|
86
|
+
found_open = True
|
|
87
|
+
if found_open and brace_count <= 0:
|
|
88
|
+
break
|
|
89
|
+
body = "\n".join(snippet_lines).strip()
|
|
90
|
+
if not body and start_idx < len(lines):
|
|
91
|
+
body = lines[start_idx].strip()
|
|
92
|
+
return hashlib.sha256(body.encode("utf-8")).hexdigest()[:16]
|
|
93
|
+
|
|
94
|
+
def _find_block_end(self, lines: Sequence[str], start_idx: int) -> int:
|
|
95
|
+
"""Find the 1-based end line of a block starting at 0-based start_idx.
|
|
96
|
+
Example:
|
|
97
|
+
>>> class MockLang(LanguageAnalyzer):
|
|
98
|
+
... language = "mock"
|
|
99
|
+
... extensions = ()
|
|
100
|
+
... def parse(self, p, s): pass
|
|
101
|
+
... def extract_symbols(self, p): pass
|
|
102
|
+
... def extract_edges(self, p, s): pass
|
|
103
|
+
... def resolve_import(self, r, c): pass
|
|
104
|
+
>>> m = MockLang()
|
|
105
|
+
>>> m._find_block_end(["func foo() {", " return 1", "}"], 0)
|
|
106
|
+
3
|
|
107
|
+
"""
|
|
108
|
+
brace_count = 0
|
|
109
|
+
found_open = False
|
|
110
|
+
for i in range(start_idx, len(lines)):
|
|
111
|
+
l = lines[i]
|
|
112
|
+
brace_count += l.count("{") - l.count("}")
|
|
113
|
+
if "{" in l:
|
|
114
|
+
found_open = True
|
|
115
|
+
if found_open and brace_count <= 0:
|
|
116
|
+
return i + 1
|
|
117
|
+
return min(len(lines), start_idx + 1)
|
codeui/lang/generic.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Generic markup, config, and fallback language analyzer for codeui."""
|
|
2
|
+
import hashlib
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import ClassVar, Iterable, List, Sequence
|
|
5
|
+
from codeui.core.ir import Edge, EdgeKind, Location, Symbol, SymbolKind, Visibility
|
|
6
|
+
from codeui.core.resolver import ResolveContext
|
|
7
|
+
from codeui.lang.base import ImportRef, LanguageAnalyzer, ParseResult
|
|
8
|
+
|
|
9
|
+
class GenericLanguageAnalyzer(LanguageAnalyzer):
|
|
10
|
+
"""Fallback analyzer for configuration, markup, and generic source files.
|
|
11
|
+
Example:
|
|
12
|
+
>>> analyzer = GenericLanguageAnalyzer()
|
|
13
|
+
>>> res = analyzer.parse(Path("config.json"), '{"key": "value"}')
|
|
14
|
+
>>> syms = list(analyzer.extract_symbols(res))
|
|
15
|
+
>>> len(syms) == 1
|
|
16
|
+
True
|
|
17
|
+
"""
|
|
18
|
+
language: ClassVar[str] = "generic"
|
|
19
|
+
has_ast_support: ClassVar[bool] = False
|
|
20
|
+
is_supported: ClassVar[bool] = False
|
|
21
|
+
extensions: ClassVar[tuple[str, ...]] = (
|
|
22
|
+
".java", ".kt", ".cs", ".rb", ".php", ".swift", ".vue", ".svelte",
|
|
23
|
+
".c", ".cpp", ".cc", ".cxx", ".h", ".hpp", ".m", ".mm", ".scala"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
def parse(self, path: Path, source: str) -> ParseResult:
|
|
27
|
+
"""Parse source into generic result.
|
|
28
|
+
Example:
|
|
29
|
+
>>> analyzer = GenericLanguageAnalyzer()
|
|
30
|
+
>>> res = analyzer.parse(Path("README.md"), "# Hello")
|
|
31
|
+
>>> res.errors
|
|
32
|
+
[]
|
|
33
|
+
"""
|
|
34
|
+
content_hash = hashlib.sha256(source.encode("utf-8")).hexdigest()
|
|
35
|
+
return ParseResult(
|
|
36
|
+
file_path=str(path),
|
|
37
|
+
source=source,
|
|
38
|
+
ast=source,
|
|
39
|
+
content_hash=content_hash,
|
|
40
|
+
errors=[],
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def extract_symbols(self, parse: ParseResult) -> Iterable[Symbol]:
|
|
44
|
+
"""Extract top-level FILE symbol for generic documents.
|
|
45
|
+
Example:
|
|
46
|
+
>>> analyzer = GenericLanguageAnalyzer()
|
|
47
|
+
>>> res = analyzer.parse(Path("a.txt"), "hello")
|
|
48
|
+
>>> syms = list(analyzer.extract_symbols(res))
|
|
49
|
+
>>> syms[0].kind == SymbolKind.FILE
|
|
50
|
+
True
|
|
51
|
+
"""
|
|
52
|
+
file_path = parse.file_path
|
|
53
|
+
lines = parse.source.splitlines()
|
|
54
|
+
max_line = len(lines) if lines else 1
|
|
55
|
+
return [
|
|
56
|
+
Symbol(
|
|
57
|
+
id=file_path,
|
|
58
|
+
name=Path(file_path).name,
|
|
59
|
+
qualified_name=file_path,
|
|
60
|
+
kind=SymbolKind.FILE,
|
|
61
|
+
language=self.language,
|
|
62
|
+
location=Location(file_path, 1, 0, max_line, 0),
|
|
63
|
+
parent_id=None,
|
|
64
|
+
signature=None,
|
|
65
|
+
visibility=Visibility.PUBLIC,
|
|
66
|
+
modifiers=(),
|
|
67
|
+
content_hash=parse.content_hash,
|
|
68
|
+
)
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
def extract_edges(self, parse: ParseResult, symbols: Sequence[Symbol]) -> Iterable[Edge]:
|
|
72
|
+
"""Extract edges for generic files (empty set).
|
|
73
|
+
Example:
|
|
74
|
+
>>> analyzer = GenericLanguageAnalyzer()
|
|
75
|
+
>>> res = analyzer.parse(Path("a.txt"), "hello")
|
|
76
|
+
>>> list(analyzer.extract_edges(res, []))
|
|
77
|
+
[]
|
|
78
|
+
"""
|
|
79
|
+
return []
|
|
80
|
+
|
|
81
|
+
def resolve_import(self, ref: ImportRef, ctx: ResolveContext) -> Iterable[str]:
|
|
82
|
+
"""Resolve import references for generic files.
|
|
83
|
+
Example:
|
|
84
|
+
>>> analyzer = GenericLanguageAnalyzer()
|
|
85
|
+
>>> ctx = ResolveContext(Path("."))
|
|
86
|
+
>>> ref = ImportRef("a.txt", "b", None, False)
|
|
87
|
+
>>> list(analyzer.resolve_import(ref, ctx))
|
|
88
|
+
[]
|
|
89
|
+
"""
|
|
90
|
+
return []
|