codeui-python 0.1.0__py3-none-any.whl → 0.1.1__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 (49) hide show
  1. codeui/__init__.py +1 -1
  2. codeui/agent/context.py +19 -3
  3. codeui/analysis/dead_code.py +13 -2
  4. codeui/analysis/undefined.py +70 -0
  5. codeui/analysis/unresolved_import.py +34 -4
  6. codeui/cli/main.py +101 -7
  7. codeui/core/cache.py +54 -0
  8. codeui/core/graph.py +233 -3
  9. codeui/core/override.py +1 -0
  10. codeui/core/resolver.py +51 -37
  11. codeui/errors.py +10 -0
  12. codeui/lang/base.py +5 -2
  13. codeui/lang/cpp.py +271 -0
  14. codeui/lang/csharp.py +262 -0
  15. codeui/lang/dart.py +278 -0
  16. codeui/lang/generic.py +6 -4
  17. codeui/lang/go.py +5 -2
  18. codeui/lang/java.py +320 -0
  19. codeui/lang/kotlin.py +303 -0
  20. codeui/lang/php.py +275 -0
  21. codeui/lang/python.py +49 -7
  22. codeui/lang/registry.py +22 -2
  23. codeui/lang/ruby.py +270 -0
  24. codeui/lang/rust.py +5 -2
  25. codeui/lang/scala.py +290 -0
  26. codeui/lang/swift.py +276 -0
  27. codeui/lang/ts.py +99 -8
  28. codeui/report/sarif_emitter.py +2 -1
  29. codeui/server/server.py +156 -85
  30. codeui/server/static/css/styles.css +1609 -0
  31. codeui/server/static/index.html +199 -0
  32. codeui/server/static/js/api.js +485 -0
  33. codeui/server/static/js/app.js +1044 -0
  34. codeui/server/static/js/graph_canvas.js +1485 -0
  35. codeui/server/static/js/minimap.js +383 -0
  36. codeui/server/static/js/sidebar.js +268 -0
  37. codeui/server/static/js/state.js +83 -0
  38. codeui/server/static/js/subgraph_window.js +1208 -0
  39. codeui/server/static/js/theme.js +332 -0
  40. codeui/tracer/__init__.py +4 -1
  41. codeui/tracer/tracer.py +109 -118
  42. codeui_python-0.1.1.dist-info/METADATA +171 -0
  43. codeui_python-0.1.1.dist-info/RECORD +71 -0
  44. {codeui_python-0.1.0.dist-info → codeui_python-0.1.1.dist-info}/licenses/LICENSE +1 -1
  45. codeui_python-0.1.0.dist-info/METADATA +0 -191
  46. codeui_python-0.1.0.dist-info/RECORD +0 -52
  47. {codeui_python-0.1.0.dist-info → codeui_python-0.1.1.dist-info}/WHEEL +0 -0
  48. {codeui_python-0.1.0.dist-info → codeui_python-0.1.1.dist-info}/entry_points.txt +0 -0
  49. {codeui_python-0.1.0.dist-info → codeui_python-0.1.1.dist-info}/top_level.txt +0 -0
codeui/lang/kotlin.py ADDED
@@ -0,0 +1,303 @@
1
+ """Kotlin language analyzer for codeui."""
2
+ from __future__ import annotations
3
+ import hashlib
4
+ import re
5
+ from pathlib import Path
6
+ from typing import ClassVar, Iterable, List, Sequence, TYPE_CHECKING
7
+ from codeui.core.ir import Edge, EdgeKind, Location, Symbol, SymbolKind, Visibility
8
+ from codeui.lang.base import ImportRef, LanguageAnalyzer, ParseResult
9
+
10
+ if TYPE_CHECKING:
11
+ from codeui.core.resolver import ResolveContext
12
+
13
+ class KotlinLanguageAnalyzer(LanguageAnalyzer):
14
+ """Kotlin language adapter extracting classes, interfaces, objects, funs, and properties.
15
+ Example:
16
+ >>> analyzer = KotlinLanguageAnalyzer()
17
+ >>> res = analyzer.parse(Path("Main.kt"), "data class User(val id: Long, val name: String)")
18
+ >>> syms = list(analyzer.extract_symbols(res))
19
+ >>> any(s.kind == SymbolKind.CLASS for s in syms)
20
+ True
21
+ """
22
+ language: ClassVar[str] = "kotlin"
23
+ extensions: ClassVar[tuple[str, ...]] = (".kt", ".kts")
24
+
25
+ def parse(self, path: Path, source: str) -> ParseResult:
26
+ """Parse Kotlin source into AST-emulating representation.
27
+ Example:
28
+ >>> analyzer = KotlinLanguageAnalyzer()
29
+ >>> res = analyzer.parse(Path("App.kt"), "fun main() {}")
30
+ >>> res.errors
31
+ []
32
+ """
33
+ content_hash = hashlib.sha256(source.encode("utf-8")).hexdigest()
34
+ return ParseResult(
35
+ file_path=str(path),
36
+ source=source,
37
+ ast=source,
38
+ content_hash=content_hash,
39
+ errors=[],
40
+ )
41
+
42
+ def extract_symbols(self, parse: ParseResult) -> Iterable[Symbol]:
43
+ """Extract Kotlin packages, classes, interfaces, objects, functions, and properties.
44
+ Example:
45
+ >>> analyzer = KotlinLanguageAnalyzer()
46
+ >>> res = analyzer.parse(Path("Greeter.kt"), "class Greeter { fun greet(): String = \\"hi\\" }")
47
+ >>> syms = list(analyzer.extract_symbols(res))
48
+ >>> len(syms) >= 2
49
+ True
50
+ """
51
+ source = parse.source
52
+ file_path = parse.file_path
53
+ lines = source.splitlines()
54
+ max_line = len(lines) if lines else 1
55
+ symbols: List[Symbol] = []
56
+
57
+ file_sym = Symbol(
58
+ id=file_path,
59
+ name=Path(file_path).name,
60
+ qualified_name=file_path,
61
+ kind=SymbolKind.FILE,
62
+ language=self.language,
63
+ location=Location(file_path, 1, 0, max_line, 0),
64
+ parent_id=None,
65
+ signature=None,
66
+ visibility=Visibility.PUBLIC,
67
+ modifiers=(),
68
+ content_hash=parse.content_hash,
69
+ )
70
+ symbols.append(file_sym)
71
+
72
+ package_pattern = re.compile(r'^\s*package\s+([a-zA-Z0-9_.]+)')
73
+ import_pattern = re.compile(r'^\s*import\s+([a-zA-Z0-9_.*]+)')
74
+ class_pattern = re.compile(r'^\s*(?:(public|private|protected|internal)\s+)?(?:(data|sealed|open|abstract|enum|annotation|inner)\s+)?(class|interface|object)\s+([a-zA-Z0-9_]+)(?:<[^>]+>)?(?:\s*\([^)]*\))?(?:\s*:\s*([a-zA-Z0-9_.,\s<>()]+))?')
75
+ companion_pattern = re.compile(r'^\s*companion\s+object(?:\s+([a-zA-Z0-9_]+))?')
76
+ fun_pattern = re.compile(r'^\s*(?:(public|private|protected|internal)\s+)?(?:(override|open|abstract|suspend|inline|operator|infix)\s+)*fun\s+(?:<[^>]+>\s+)?([a-zA-Z0-9_]+)\s*\(([^)]*)\)(?:\s*:\s*([a-zA-Z0-9_?<>,\s]+))?')
77
+ property_pattern = re.compile(r'^\s*(?:(public|private|protected|internal)\s+)?(?:(override|open|abstract|const)\s+)*(val|var)\s+([a-zA-Z0-9_]+)(?:\s*:\s*([a-zA-Z0-9_?<>,\s]+))?')
78
+
79
+ current_package = ""
80
+ current_class: str | None = None
81
+ brace_depth = 0
82
+ class_depth = 0
83
+
84
+ for line_idx, line in enumerate(lines, start=1):
85
+ stripped = line.strip()
86
+ if not stripped or stripped.startswith("//") or stripped.startswith("/*") or stripped.startswith("*"):
87
+ continue
88
+
89
+ pkg_match = package_pattern.match(line)
90
+ if pkg_match:
91
+ current_package = pkg_match.group(1)
92
+ pkg_sym = Symbol(
93
+ id=f"{file_path}::package::{current_package}",
94
+ name=current_package,
95
+ qualified_name=current_package,
96
+ kind=SymbolKind.NAMESPACE,
97
+ language=self.language,
98
+ location=Location(file_path, line_idx, 0, line_idx, len(line)),
99
+ parent_id=file_path,
100
+ signature=f"package {current_package}",
101
+ visibility=Visibility.PUBLIC,
102
+ modifiers=(),
103
+ content_hash=hashlib.sha256(line.encode("utf-8")).hexdigest()[:16],
104
+ )
105
+ symbols.append(pkg_sym)
106
+ continue
107
+
108
+ imp_match = import_pattern.match(line)
109
+ if imp_match:
110
+ imp_name = imp_match.group(1)
111
+ symbols.append(Symbol(
112
+ id=f"{file_path}::import::{imp_name}",
113
+ name=imp_name.split(".")[-1],
114
+ qualified_name=imp_name,
115
+ kind=SymbolKind.IMPORT,
116
+ language=self.language,
117
+ location=Location(file_path, line_idx, 0, line_idx, len(line)),
118
+ parent_id=file_path,
119
+ signature=f"import {imp_name}",
120
+ visibility=Visibility.PUBLIC,
121
+ modifiers=(),
122
+ content_hash=hashlib.sha256(line.encode("utf-8")).hexdigest()[:16],
123
+ ))
124
+ continue
125
+
126
+ comp_match = companion_pattern.match(line)
127
+ if comp_match:
128
+ comp_name = comp_match.group(1) or "Companion"
129
+ parent_id = current_class or file_path
130
+ sym_id = f"{file_path}::{comp_name}" if not current_class else f"{current_class}::{comp_name}"
131
+ symbols.append(Symbol(
132
+ id=sym_id,
133
+ name=comp_name,
134
+ qualified_name=f"{current_package}.{comp_name}" if current_package else comp_name,
135
+ kind=SymbolKind.CLASS,
136
+ language=self.language,
137
+ location=Location(file_path, line_idx, 0, line_idx, len(line)),
138
+ parent_id=parent_id,
139
+ signature=f"companion object {comp_name}",
140
+ visibility=Visibility.PUBLIC,
141
+ modifiers=("companion",),
142
+ content_hash=hashlib.sha256(line.encode("utf-8")).hexdigest()[:16],
143
+ ))
144
+
145
+ class_match = class_pattern.match(line)
146
+ if class_match:
147
+ vis_str = class_match.group(1) or "public"
148
+ mod_str = class_match.group(2)
149
+ type_kind = class_match.group(3)
150
+ class_name = class_match.group(4)
151
+ vis = Visibility.PRIVATE if vis_str == "private" else (Visibility.PROTECTED if vis_str in ("protected", "internal") else Visibility.PUBLIC)
152
+ kind = SymbolKind.INTERFACE if type_kind == "interface" else SymbolKind.CLASS
153
+ modifiers = tuple(filter(None, [mod_str, type_kind]))
154
+ qual_prefix = f"{current_package}." if current_package else ""
155
+ sym_id = f"{file_path}::{class_name}"
156
+ current_class = sym_id
157
+ class_depth = brace_depth
158
+
159
+ symbols.append(Symbol(
160
+ id=sym_id,
161
+ name=class_name,
162
+ qualified_name=f"{qual_prefix}{class_name}",
163
+ kind=kind,
164
+ language=self.language,
165
+ location=Location(file_path, line_idx, 0, line_idx, len(line)),
166
+ parent_id=file_path,
167
+ signature=f"{vis_str} {type_kind} {class_name}",
168
+ visibility=vis,
169
+ modifiers=modifiers,
170
+ content_hash=hashlib.sha256(line.encode("utf-8")).hexdigest()[:16],
171
+ ))
172
+
173
+ fun_match = fun_pattern.match(line)
174
+ if fun_match:
175
+ vis_str = fun_match.group(1) or "public"
176
+ fun_mod = fun_match.group(2)
177
+ fun_name = fun_match.group(3)
178
+ fun_args = fun_match.group(4)
179
+ fun_ret = fun_match.group(5) or "Unit"
180
+ vis = Visibility.PRIVATE if vis_str == "private" else (Visibility.PROTECTED if vis_str in ("protected", "internal") else Visibility.PUBLIC)
181
+ parent_id = current_class if current_class and brace_depth > class_depth else file_path
182
+ sym_id = f"{parent_id}::{fun_name}"
183
+ symbols.append(Symbol(
184
+ id=sym_id,
185
+ name=fun_name,
186
+ qualified_name=f"{current_package}.{fun_name}" if current_package else fun_name,
187
+ kind=SymbolKind.FUNCTION,
188
+ language=self.language,
189
+ location=Location(file_path, line_idx, 0, line_idx, len(line)),
190
+ parent_id=parent_id,
191
+ signature=f"fun {fun_name}({fun_args}): {fun_ret}",
192
+ visibility=vis,
193
+ modifiers=tuple(filter(None, [fun_mod])),
194
+ content_hash=hashlib.sha256(line.encode("utf-8")).hexdigest()[:16],
195
+ ))
196
+
197
+ prop_match = property_pattern.match(line)
198
+ if prop_match:
199
+ vis_str = prop_match.group(1) or "public"
200
+ prop_mod = prop_match.group(2)
201
+ val_or_var = prop_match.group(3)
202
+ prop_name = prop_match.group(4)
203
+ prop_type = prop_match.group(5) or "Any"
204
+ vis = Visibility.PRIVATE if vis_str == "private" else (Visibility.PROTECTED if vis_str in ("protected", "internal") else Visibility.PUBLIC)
205
+ parent_id = current_class if current_class and brace_depth > class_depth else file_path
206
+ sym_id = f"{parent_id}::{prop_name}"
207
+ symbols.append(Symbol(
208
+ id=sym_id,
209
+ name=prop_name,
210
+ qualified_name=f"{current_package}.{prop_name}" if current_package else prop_name,
211
+ kind=SymbolKind.VARIABLE,
212
+ language=self.language,
213
+ location=Location(file_path, line_idx, 0, line_idx, len(line)),
214
+ parent_id=parent_id,
215
+ signature=f"{val_or_var} {prop_name}: {prop_type}",
216
+ visibility=vis,
217
+ modifiers=tuple(filter(None, [prop_mod, val_or_var])),
218
+ content_hash=hashlib.sha256(line.encode("utf-8")).hexdigest()[:16],
219
+ ))
220
+
221
+ brace_depth += line.count("{") - line.count("}")
222
+ if current_class and brace_depth <= class_depth:
223
+ current_class = None
224
+
225
+ return sorted(symbols, key=lambda s: (s.location.start_line if s.location else 0, s.name))
226
+
227
+ def extract_edges(self, parse: ParseResult, symbols: Sequence[Symbol]) -> Iterable[Edge]:
228
+ """Extract containment, inheritance, imports, and calls edges for Kotlin.
229
+ Example:
230
+ >>> analyzer = KotlinLanguageAnalyzer()
231
+ >>> res = analyzer.parse(Path("App.kt"), "class Child : Parent()")
232
+ >>> syms = list(analyzer.extract_symbols(res))
233
+ >>> edges = list(analyzer.extract_edges(res, syms))
234
+ >>> len(edges) >= 1
235
+ True
236
+ """
237
+ edges: List[Edge] = []
238
+ file_path = parse.file_path
239
+ symbol_map = {s.name: s for s in symbols}
240
+ class_pattern = re.compile(r'^\s*(?:[a-zA-Z0-9_\s<>]+)?(?:class|interface|object)\s+([a-zA-Z0-9_]+)(?:<[^>]+>)?(?:\s*\([^)]*\))?\s*:\s*([a-zA-Z0-9_.,\s<>()]+)')
241
+
242
+ for s in symbols:
243
+ if s.parent_id and s.parent_id != s.id:
244
+ edges.append(Edge(
245
+ source_id=s.parent_id,
246
+ target_id=s.id,
247
+ kind=EdgeKind.CONTAINS,
248
+ weight=1.0,
249
+ confidence=1.0,
250
+ location=s.location,
251
+ ))
252
+
253
+ for line_idx, line in enumerate(parse.source.splitlines(), start=1):
254
+ class_match = class_pattern.match(line)
255
+ if class_match:
256
+ cls_name = class_match.group(1)
257
+ super_types_raw = class_match.group(2)
258
+ cls_sym = symbol_map.get(cls_name)
259
+ if cls_sym:
260
+ for super_raw in super_types_raw.split(","):
261
+ clean_super = super_raw.strip().split("(")[0].split("<")[0].strip()
262
+ if clean_super and clean_super not in ("Any", "Serializable"):
263
+ target_id = symbol_map[clean_super].id if clean_super in symbol_map else clean_super
264
+ edges.append(Edge(
265
+ source_id=cls_sym.id,
266
+ target_id=target_id,
267
+ kind=EdgeKind.INHERITS,
268
+ weight=1.0,
269
+ confidence=0.9,
270
+ location=Location(file_path, line_idx, 0, line_idx, len(line)),
271
+ ))
272
+
273
+ if line.strip().startswith("import "):
274
+ imp_parts = line.strip().replace("import ", "").rstrip(";").split()
275
+ if imp_parts:
276
+ target_mod = imp_parts[0]
277
+ edges.append(Edge(
278
+ source_id=file_path,
279
+ target_id=target_mod,
280
+ kind=EdgeKind.IMPORTS,
281
+ weight=1.0,
282
+ confidence=1.0,
283
+ location=Location(file_path, line_idx, 0, line_idx, len(line)),
284
+ ))
285
+
286
+ return sorted(edges, key=lambda e: (e.source_id, e.target_id, e.kind.value))
287
+
288
+ def resolve_import(self, ref: ImportRef, ctx: ResolveContext) -> Iterable[str]:
289
+ """Resolve Kotlin package or class imports against workspace files.
290
+ Example:
291
+ >>> analyzer = KotlinLanguageAnalyzer()
292
+ >>> from codeui.core.resolver import ResolveContext
293
+ >>> ctx = ResolveContext(Path("."))
294
+ >>> ref = ImportRef("app/Main.kt", "com.example.User", None, False)
295
+ >>> list(analyzer.resolve_import(ref, ctx))
296
+ []
297
+ """
298
+ mod_name = ref.module_name.rstrip(".*")
299
+ candidates = ctx.resolve_path(ref.from_file, mod_name, "kotlin")
300
+ if not candidates and "." in mod_name:
301
+ leaf = mod_name.split(".")[-1]
302
+ candidates = ctx.resolve_path(ref.from_file, leaf, "kotlin")
303
+ return candidates
codeui/lang/php.py ADDED
@@ -0,0 +1,275 @@
1
+ """PHP language analyzer for codeui."""
2
+ from __future__ import annotations
3
+ import hashlib
4
+ import re
5
+ from pathlib import Path
6
+ from typing import ClassVar, Iterable, List, Sequence, TYPE_CHECKING
7
+ from codeui.core.ir import Edge, EdgeKind, Location, Symbol, SymbolKind, Visibility
8
+ from codeui.lang.base import ImportRef, LanguageAnalyzer, ParseResult
9
+
10
+ if TYPE_CHECKING:
11
+ from codeui.core.resolver import ResolveContext
12
+
13
+ class PHPLanguageAnalyzer(LanguageAnalyzer):
14
+ """PHP language analyzer.
15
+ Example:
16
+ >>> analyzer = PHPLanguageAnalyzer()
17
+ >>> res = analyzer.parse(Path("App.php"), '<?php namespace App; class Controller { public function index() {} }')
18
+ >>> syms = list(analyzer.extract_symbols(res))
19
+ >>> any(s.name == "Controller" for s in syms)
20
+ True
21
+ """
22
+ language: ClassVar[str] = "php"
23
+ extensions: ClassVar[tuple[str, ...]] = (".php",)
24
+ has_ast_support: ClassVar[bool] = True
25
+ is_supported: ClassVar[bool] = True
26
+
27
+ def parse(self, path: Path, source: str) -> ParseResult:
28
+ """Parse PHP source code into ParseResult.
29
+ Example:
30
+ >>> analyzer = PHPLanguageAnalyzer()
31
+ >>> res = analyzer.parse(Path("User.php"), "<?php namespace App\Models;")
32
+ >>> res.errors
33
+ []
34
+ """
35
+ content_hash = hashlib.sha256(source.encode("utf-8")).hexdigest()
36
+ return ParseResult(
37
+ file_path=str(path),
38
+ source=source,
39
+ ast=source,
40
+ content_hash=content_hash,
41
+ errors=[],
42
+ )
43
+
44
+ def extract_symbols(self, parse: ParseResult) -> Iterable[Symbol]:
45
+ """Extract namespaces, classes, interfaces, traits, enums, functions, and methods from PHP source.
46
+ Example:
47
+ >>> analyzer = PHPLanguageAnalyzer()
48
+ >>> res = analyzer.parse(Path("Post.php"), "<?php class Post { public function save() {} }")
49
+ >>> syms = list(analyzer.extract_symbols(res))
50
+ >>> len(syms) >= 2
51
+ True
52
+ """
53
+ source = parse.source
54
+ file_path = parse.file_path
55
+ lines = source.splitlines()
56
+ max_line = len(lines) if lines else 1
57
+ symbols: List[Symbol] = []
58
+
59
+ file_sym = Symbol(
60
+ id=file_path,
61
+ name=Path(file_path).name,
62
+ qualified_name=file_path,
63
+ kind=SymbolKind.FILE,
64
+ language=self.language,
65
+ location=Location(file_path, 1, 0, max_line, 0),
66
+ parent_id=None,
67
+ signature=None,
68
+ visibility=Visibility.PUBLIC,
69
+ modifiers=(),
70
+ content_hash=parse.content_hash,
71
+ )
72
+ symbols.append(file_sym)
73
+
74
+ use_pattern = re.compile(r'use\s+([A-Za-z0-9_\\]+)(?:\s+as\s+([A-Za-z0-9_]+))?\s*;')
75
+ class_pattern = re.compile(r'(?:abstract|final|readonly)?\s*(?:class|interface|trait|enum)\s+([A-Za-z0-9_]+)(?:\s+extends\s+([A-Za-z0-9_\\]+))?(?:\s+implements\s+([A-Za-z0-9_\\,\s]+))?')
76
+ method_pattern = re.compile(r'(?:public|protected|private|static|final|abstract|\s)+function\s+([A-Za-z0-9_]+)\s*\(([^)]*)\)')
77
+ func_pattern = re.compile(r'function\s+([A-Za-z0-9_]+)\s*\(([^)]*)\)')
78
+
79
+ php_keywords = {"if", "for", "while", "switch", "catch", "return", "throw", "new", "class", "interface", "namespace", "use", "else", "try", "finally", "echo", "print"}
80
+ class_ranges: List[tuple[str, str, int, int, str, str]] = []
81
+
82
+ for idx, line in enumerate(lines, start=1):
83
+ line_str = line.strip()
84
+ if not line_str or line_str.startswith("//") or line_str.startswith("#") or line_str.startswith("*"):
85
+ continue
86
+
87
+ for match in use_pattern.finditer(line_str):
88
+ use_path = match.group(1)
89
+ alias = match.group(2) or use_path.split("\\")[-1]
90
+ sym_id = f"import:{use_path}"
91
+ symbols.append(Symbol(
92
+ id=sym_id,
93
+ name=alias,
94
+ qualified_name=f"import:{use_path}",
95
+ kind=SymbolKind.IMPORT,
96
+ language=self.language,
97
+ location=Location(file_path, idx, line.find(use_path), idx, line.find(use_path) + len(use_path)),
98
+ parent_id=file_path,
99
+ signature=f"use {use_path}",
100
+ visibility=Visibility.PUBLIC,
101
+ modifiers=(),
102
+ content_hash=self._extract_body_hash(lines, idx - 1),
103
+ ))
104
+
105
+ for match in class_pattern.finditer(line_str):
106
+ cls_name = match.group(1)
107
+ if cls_name in php_keywords:
108
+ continue
109
+ ext_name = match.group(2) or ""
110
+ impl_names = match.group(3) or ""
111
+ sym_id = f"{file_path}::{cls_name}"
112
+ end_line = self._find_block_end(lines, idx - 1)
113
+ class_ranges.append((cls_name, sym_id, idx, end_line, ext_name, impl_names))
114
+ loc = Location(file_path, idx, line.find(cls_name), end_line, line.find(cls_name) + len(cls_name))
115
+ vis = Visibility.PUBLIC if "public" in line_str or "abstract" in line_str or "final" in line_str else Visibility.INTERNAL
116
+ symbols.append(Symbol(
117
+ id=sym_id,
118
+ name=cls_name,
119
+ qualified_name=cls_name,
120
+ kind=SymbolKind.CLASS,
121
+ language=self.language,
122
+ location=loc,
123
+ parent_id=file_path,
124
+ signature=f"class {cls_name}",
125
+ visibility=vis,
126
+ modifiers=(),
127
+ content_hash=self._extract_body_hash(lines, idx - 1),
128
+ ))
129
+
130
+ for match in method_pattern.finditer(line_str):
131
+ m_name = match.group(1)
132
+ params = match.group(2)
133
+ if m_name in php_keywords:
134
+ continue
135
+ enclosing = next((c for c in reversed(class_ranges) if c[2] <= idx <= c[3]), None)
136
+ if enclosing:
137
+ cls_name, cls_sym_id, _, _, _, _ = enclosing
138
+ sym_id = f"{file_path}::{cls_name}::{m_name}"
139
+ qual_name = f"{cls_name}::{m_name}"
140
+ parent_sym_id = cls_sym_id
141
+ sym_kind = SymbolKind.METHOD
142
+ else:
143
+ sym_id = f"{file_path}::{m_name}"
144
+ qual_name = m_name
145
+ parent_sym_id = file_path
146
+ sym_kind = SymbolKind.FUNCTION
147
+ end_line = self._find_block_end(lines, idx - 1)
148
+ loc = Location(file_path, idx, line.find(m_name), end_line, line.find(m_name) + len(m_name))
149
+ vis = Visibility.PRIVATE if "private" in line_str else (Visibility.PROTECTED if "protected" in line_str else Visibility.PUBLIC)
150
+ symbols.append(Symbol(
151
+ id=sym_id,
152
+ name=m_name,
153
+ qualified_name=qual_name,
154
+ kind=sym_kind,
155
+ language=self.language,
156
+ location=loc,
157
+ parent_id=parent_sym_id,
158
+ signature=f"function {m_name}({params})",
159
+ visibility=vis,
160
+ modifiers=(),
161
+ content_hash=self._extract_body_hash(lines, idx - 1),
162
+ ))
163
+
164
+ return symbols
165
+
166
+ def extract_edges(self, parse: ParseResult, symbols: Sequence[Symbol]) -> Iterable[Edge]:
167
+ """Extract relationship edges for PHP AST.
168
+ Example:
169
+ >>> analyzer = PHPLanguageAnalyzer()
170
+ >>> res = analyzer.parse(Path("App.php"), "<?php use App\\Helper;")
171
+ >>> syms = list(analyzer.extract_symbols(res))
172
+ >>> edges = list(analyzer.extract_edges(res, syms))
173
+ >>> len(edges) >= 1
174
+ True
175
+ """
176
+ edges: List[Edge] = []
177
+ file_path = parse.file_path
178
+ sym_map = {s.id: s for s in symbols}
179
+ class_pattern = re.compile(r'(?:class|interface|trait|enum)\s+([A-Za-z0-9_]+)(?:\s+extends\s+([A-Za-z0-9_\\]+))?(?:\s+implements\s+([A-Za-z0-9_\\,\s]+))?')
180
+ call_pattern = re.compile(r'(?<![A-Za-z0-9_$-])\b([A-Za-z_][A-Za-z0-9_]*)\s*\(')
181
+ php_keywords = {"if", "for", "while", "switch", "catch", "return", "throw", "new", "echo", "print", "isset", "empty", "unset"}
182
+
183
+ for s in symbols:
184
+ if s.parent_id and s.parent_id in sym_map:
185
+ edges.append(Edge(
186
+ source_id=s.parent_id,
187
+ target_id=s.id,
188
+ kind=EdgeKind.CONTAINS,
189
+ weight=1.0,
190
+ confidence=1.0,
191
+ location=s.location,
192
+ ))
193
+ if s.kind == SymbolKind.IMPORT:
194
+ target_mod = s.qualified_name.replace("import:", "").replace("\\", "/")
195
+ edges.append(Edge(
196
+ source_id=file_path,
197
+ target_id=f"module::{target_mod}",
198
+ kind=EdgeKind.IMPORTS,
199
+ weight=1.0,
200
+ confidence=1.0,
201
+ location=s.location,
202
+ ))
203
+
204
+ lines = parse.source.splitlines()
205
+ for idx, line in enumerate(lines, start=1):
206
+ line_str = line.strip()
207
+ if not line_str or line_str.startswith("//") or line_str.startswith("#") or line_str.startswith("*"):
208
+ continue
209
+ for match in class_pattern.finditer(line_str):
210
+ cls_name = match.group(1)
211
+ ext_name = match.group(2)
212
+ impl_names = match.group(3)
213
+ cls_sym_id = f"{file_path}::{cls_name}"
214
+ if ext_name:
215
+ base_clean = ext_name.split("\\")[-1]
216
+ loc = Location(file_path, idx, line.find(base_clean), idx, line.find(base_clean) + len(base_clean))
217
+ edges.append(Edge(
218
+ source_id=cls_sym_id,
219
+ target_id=f"module::{base_clean}",
220
+ kind=EdgeKind.INHERITS,
221
+ weight=1.0,
222
+ confidence=0.9,
223
+ location=loc,
224
+ ))
225
+ if impl_names:
226
+ for iface in impl_names.split(","):
227
+ iface_clean = iface.strip().split("\\")[-1]
228
+ if iface_clean and iface_clean not in php_keywords:
229
+ loc = Location(file_path, idx, line.find(iface_clean), idx, line.find(iface_clean) + len(iface_clean))
230
+ edges.append(Edge(
231
+ source_id=cls_sym_id,
232
+ target_id=f"module::{iface_clean}",
233
+ kind=EdgeKind.IMPLEMENTS,
234
+ weight=1.0,
235
+ confidence=0.9,
236
+ location=loc,
237
+ ))
238
+
239
+ method_syms = [s for s in symbols if s.kind in (SymbolKind.METHOD, SymbolKind.FUNCTION)]
240
+ name_to_syms: dict[str, list[Symbol]] = {}
241
+ for s in method_syms:
242
+ name_to_syms.setdefault(s.name, []).append(s)
243
+
244
+ for m in method_syms:
245
+ if not m.location:
246
+ continue
247
+ m_lines = lines[m.location.start_line - 1 : m.location.end_line]
248
+ for m_line in m_lines:
249
+ for match in call_pattern.finditer(m_line):
250
+ callee_name = match.group(1)
251
+ if callee_name in php_keywords or callee_name == m.name:
252
+ continue
253
+ if callee_name in name_to_syms:
254
+ target = name_to_syms[callee_name][0]
255
+ edges.append(Edge(
256
+ source_id=m.id,
257
+ target_id=target.id,
258
+ kind=EdgeKind.CALLS,
259
+ weight=1.0,
260
+ confidence=0.85,
261
+ location=m.location,
262
+ ))
263
+
264
+ return edges
265
+
266
+ def resolve_import(self, ref: ImportRef, ctx: ResolveContext) -> Iterable[str]:
267
+ """Resolve PHP use references to project files.
268
+ Example:
269
+ >>> analyzer = PHPLanguageAnalyzer()
270
+ >>> ctx = ResolveContext(Path("."))
271
+ >>> list(analyzer.resolve_import(ImportRef("App.php", "App\\Helper", None, False), ctx))
272
+ []
273
+ """
274
+ clean_path = ref.module_name.replace("\\", "/")
275
+ return ctx.resolve_path(ref.file_path, clean_path, self.language)
codeui/lang/python.py CHANGED
@@ -1,13 +1,16 @@
1
1
  """Python language adapter using standard library ast."""
2
+ from __future__ import annotations
2
3
  import ast
3
4
  import hashlib
4
5
  import re
5
6
  from pathlib import Path
6
- from typing import Any, ClassVar, Dict, Iterable, List, Optional, Sequence, Tuple
7
+ from typing import Any, ClassVar, Dict, Iterable, List, Optional, Sequence, Tuple, TYPE_CHECKING
7
8
  from codeui.core.ir import Edge, EdgeKind, Location, Symbol, SymbolKind, Visibility
8
- from codeui.core.resolver import ResolveContext
9
9
  from codeui.lang.base import ImportRef, LanguageAnalyzer, ParseResult
10
10
 
11
+ if TYPE_CHECKING:
12
+ from codeui.core.resolver import ResolveContext
13
+
11
14
  class PythonLanguageAnalyzer(LanguageAnalyzer):
12
15
  """Python language analyzer using standard library ast.
13
16
  Example:
@@ -192,6 +195,17 @@ class PythonLanguageAnalyzer(LanguageAnalyzer):
192
195
  location=Location(parse.file_path, node.lineno, node.col_offset, node.lineno, node.col_offset),
193
196
  ))
194
197
  class_names = {s.name for s in symbols if s.kind == SymbolKind.CLASS}
198
+ extracted_sym_ids = {s.id for s in symbols}
199
+
200
+ def _extract_names(target_node: ast.AST) -> set[str]:
201
+ names = set()
202
+ if isinstance(target_node, ast.Name):
203
+ names.add(target_node.id)
204
+ elif isinstance(target_node, (ast.Tuple, ast.List)):
205
+ for elt in target_node.elts:
206
+ names.update(_extract_names(elt))
207
+ return names
208
+
195
209
  def walk_calls(node: ast.AST, current_scope: str, class_scope: str, locals_in_scope: set[str]) -> None:
196
210
  for child in ast.iter_child_nodes(node):
197
211
  if isinstance(child, ast.ClassDef):
@@ -219,21 +233,49 @@ class PythonLanguageAnalyzer(LanguageAnalyzer):
219
233
  new_locals.add(child.args.vararg.arg)
220
234
  if getattr(child.args, "kwarg", None):
221
235
  new_locals.add(child.args.kwarg.arg)
236
+
222
237
  for subnode in ast.walk(child):
223
238
  if isinstance(subnode, ast.Assign):
224
239
  for target in subnode.targets:
225
- if isinstance(target, ast.Name):
226
- new_locals.add(target.id)
240
+ new_locals.update(_extract_names(target))
227
241
  elif isinstance(subnode, ast.AnnAssign):
228
- if isinstance(subnode.target, ast.Name):
229
- new_locals.add(subnode.target.id)
242
+ new_locals.update(_extract_names(subnode.target))
243
+ elif isinstance(subnode, (ast.For, ast.AsyncFor)):
244
+ new_locals.update(_extract_names(subnode.target))
245
+ elif isinstance(subnode, (ast.With, ast.AsyncWith)):
246
+ for item in subnode.items:
247
+ if item.optional_vars:
248
+ new_locals.update(_extract_names(item.optional_vars))
249
+ elif isinstance(subnode, ast.ExceptHandler):
250
+ if subnode.name:
251
+ new_locals.add(subnode.name)
252
+ elif isinstance(subnode, (ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp)):
253
+ for gen in subnode.generators:
254
+ new_locals.update(_extract_names(gen.target))
255
+
230
256
  walk_calls(child, func_sym_id, class_scope, new_locals)
231
257
  elif isinstance(child, ast.Call):
232
258
  caller_id = current_scope if current_scope else parse.file_path
233
259
  if isinstance(child.func, ast.Name):
234
260
  callee_name = child.func.id
235
261
  if callee_name not in locals_in_scope:
236
- target_id = import_map.get(callee_name, f"{parse.file_path}::{callee_name}")
262
+ target_id = None
263
+ if current_scope:
264
+ scope_parts = current_scope.split("::")[-1].split(".")
265
+ for i in range(len(scope_parts), 0, -1):
266
+ candidate_prefix = ".".join(scope_parts[:i])
267
+ candidate_id = f"{parse.file_path}::{candidate_prefix}.{callee_name}"
268
+ if candidate_id in extracted_sym_ids:
269
+ target_id = candidate_id
270
+ break
271
+
272
+ if not target_id:
273
+ candidate_id = f"{parse.file_path}::{callee_name}"
274
+ if candidate_id in extracted_sym_ids:
275
+ target_id = candidate_id
276
+ else:
277
+ target_id = import_map.get(callee_name, f"{parse.file_path}::{callee_name}")
278
+
237
279
  weight = float(self._count_symbol_usages(parse.source, callee_name))
238
280
  edges.append(Edge(
239
281
  source_id=caller_id,