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.
- codeui/__init__.py +1 -1
- codeui/agent/context.py +19 -3
- codeui/analysis/dead_code.py +13 -2
- codeui/analysis/undefined.py +70 -0
- codeui/analysis/unresolved_import.py +34 -4
- codeui/cli/main.py +101 -7
- codeui/core/cache.py +54 -0
- codeui/core/graph.py +233 -3
- codeui/core/override.py +1 -0
- codeui/core/resolver.py +51 -37
- codeui/errors.py +10 -0
- codeui/lang/base.py +5 -2
- codeui/lang/cpp.py +271 -0
- codeui/lang/csharp.py +262 -0
- codeui/lang/dart.py +278 -0
- codeui/lang/generic.py +6 -4
- codeui/lang/go.py +5 -2
- codeui/lang/java.py +320 -0
- codeui/lang/kotlin.py +303 -0
- codeui/lang/php.py +275 -0
- codeui/lang/python.py +49 -7
- codeui/lang/registry.py +22 -2
- codeui/lang/ruby.py +270 -0
- codeui/lang/rust.py +5 -2
- codeui/lang/scala.py +290 -0
- codeui/lang/swift.py +276 -0
- codeui/lang/ts.py +99 -8
- codeui/report/sarif_emitter.py +2 -1
- codeui/server/server.py +156 -85
- codeui/server/static/css/styles.css +1609 -0
- codeui/server/static/index.html +199 -0
- codeui/server/static/js/api.js +485 -0
- codeui/server/static/js/app.js +1044 -0
- codeui/server/static/js/graph_canvas.js +1485 -0
- codeui/server/static/js/minimap.js +383 -0
- codeui/server/static/js/sidebar.js +268 -0
- codeui/server/static/js/state.js +83 -0
- codeui/server/static/js/subgraph_window.js +1208 -0
- codeui/server/static/js/theme.js +332 -0
- codeui/tracer/__init__.py +4 -1
- codeui/tracer/tracer.py +109 -118
- codeui_python-0.1.1.dist-info/METADATA +171 -0
- codeui_python-0.1.1.dist-info/RECORD +71 -0
- {codeui_python-0.1.0.dist-info → codeui_python-0.1.1.dist-info}/licenses/LICENSE +1 -1
- codeui_python-0.1.0.dist-info/METADATA +0 -191
- codeui_python-0.1.0.dist-info/RECORD +0 -52
- {codeui_python-0.1.0.dist-info → codeui_python-0.1.1.dist-info}/WHEEL +0 -0
- {codeui_python-0.1.0.dist-info → codeui_python-0.1.1.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
226
|
-
new_locals.add(target.id)
|
|
240
|
+
new_locals.update(_extract_names(target))
|
|
227
241
|
elif isinstance(subnode, ast.AnnAssign):
|
|
228
|
-
|
|
229
|
-
|
|
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 =
|
|
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,
|