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
@@ -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
@@ -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 []