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/cpp.py
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""C and C++ 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 CppLanguageAnalyzer(LanguageAnalyzer):
|
|
14
|
+
"""C and C++ language analyzer.
|
|
15
|
+
Example:
|
|
16
|
+
>>> analyzer = CppLanguageAnalyzer()
|
|
17
|
+
>>> res = analyzer.parse(Path("main.cpp"), '#include "helper.h"\\nint main() { return 0; }')
|
|
18
|
+
>>> syms = list(analyzer.extract_symbols(res))
|
|
19
|
+
>>> any(s.name == "main" for s in syms)
|
|
20
|
+
True
|
|
21
|
+
"""
|
|
22
|
+
language: ClassVar[str] = "cpp"
|
|
23
|
+
extensions: ClassVar[tuple[str, ...]] = (".c", ".cpp", ".cc", ".cxx", ".h", ".hpp")
|
|
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 C or C++ source code into ParseResult.
|
|
29
|
+
Example:
|
|
30
|
+
>>> analyzer = CppLanguageAnalyzer()
|
|
31
|
+
>>> res = analyzer.parse(Path("engine.cpp"), 'class Engine { public: void run(); };')
|
|
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 functions, structs, classes, and include statements from C/C++ source.
|
|
46
|
+
Example:
|
|
47
|
+
>>> analyzer = CppLanguageAnalyzer()
|
|
48
|
+
>>> res = analyzer.parse(Path("main.c"), 'void compute(int a) {}')
|
|
49
|
+
>>> syms = list(analyzer.extract_symbols(res))
|
|
50
|
+
>>> any(s.name == "compute" for s in syms)
|
|
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
|
+
include_pattern = re.compile(r'#include\s+["<]([^">]+)[">]')
|
|
75
|
+
class_pattern = re.compile(r'(?:class|struct)\s+([A-Za-z0-9_]+)(?:\s*:\s*(?:public|protected|private)?\s*([A-Za-z0-9_]+))?')
|
|
76
|
+
func_pattern = re.compile(r'(?:[\w::<>*&]+\s+)+([A-Za-z0-9_]+)\s*\(([^)]*)\)\s*(?:const)?\s*[{;]')
|
|
77
|
+
|
|
78
|
+
keywords = {"if", "for", "while", "switch", "catch", "return", "throw", "sizeof", "typedef", "struct", "class", "namespace"}
|
|
79
|
+
class_ranges: List[tuple[str, str, int, int, str]] = []
|
|
80
|
+
|
|
81
|
+
for idx, line in enumerate(lines, start=1):
|
|
82
|
+
line_str = line.strip()
|
|
83
|
+
if not line_str or line_str.startswith("//") or line_str.startswith("/*") or line_str.startswith("*"):
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
for match in include_pattern.finditer(line_str):
|
|
87
|
+
inc = match.group(1)
|
|
88
|
+
sym_id = f"import:{inc}"
|
|
89
|
+
symbols.append(Symbol(
|
|
90
|
+
id=sym_id,
|
|
91
|
+
name=inc,
|
|
92
|
+
qualified_name=f"import:{inc}",
|
|
93
|
+
kind=SymbolKind.IMPORT,
|
|
94
|
+
language=self.language,
|
|
95
|
+
location=Location(file_path, idx, line.find(inc), idx, line.find(inc) + len(inc)),
|
|
96
|
+
parent_id=file_path,
|
|
97
|
+
signature=f"#include <{inc}>",
|
|
98
|
+
visibility=Visibility.PUBLIC,
|
|
99
|
+
modifiers=(),
|
|
100
|
+
content_hash=self._extract_body_hash(lines, idx - 1),
|
|
101
|
+
))
|
|
102
|
+
|
|
103
|
+
for match in class_pattern.finditer(line_str):
|
|
104
|
+
c_name = match.group(1)
|
|
105
|
+
if c_name in keywords:
|
|
106
|
+
continue
|
|
107
|
+
base_name = match.group(2) or ""
|
|
108
|
+
sym_id = f"{file_path}::{c_name}"
|
|
109
|
+
end_line = self._find_block_end(lines, idx - 1)
|
|
110
|
+
class_ranges.append((c_name, sym_id, idx, end_line, base_name))
|
|
111
|
+
loc = Location(file_path, idx, line.find(c_name), end_line, line.find(c_name) + len(c_name))
|
|
112
|
+
symbols.append(Symbol(
|
|
113
|
+
id=sym_id,
|
|
114
|
+
name=c_name,
|
|
115
|
+
qualified_name=c_name,
|
|
116
|
+
kind=SymbolKind.CLASS,
|
|
117
|
+
language=self.language,
|
|
118
|
+
location=loc,
|
|
119
|
+
parent_id=file_path,
|
|
120
|
+
signature=f"class {c_name}",
|
|
121
|
+
visibility=Visibility.PUBLIC,
|
|
122
|
+
modifiers=(),
|
|
123
|
+
content_hash=self._extract_body_hash(lines, idx - 1),
|
|
124
|
+
))
|
|
125
|
+
|
|
126
|
+
for match in func_pattern.finditer(line_str):
|
|
127
|
+
f_name = match.group(1)
|
|
128
|
+
params = match.group(2)
|
|
129
|
+
if f_name in keywords:
|
|
130
|
+
continue
|
|
131
|
+
enclosing = next((c for c in reversed(class_ranges) if c[2] <= idx <= c[3]), None)
|
|
132
|
+
if enclosing:
|
|
133
|
+
cls_name, cls_sym_id, _, _, _ = enclosing
|
|
134
|
+
sym_id = f"{file_path}::{cls_name}::{f_name}"
|
|
135
|
+
qual_name = f"{cls_name}.{f_name}"
|
|
136
|
+
parent_sym_id = cls_sym_id
|
|
137
|
+
sym_kind = SymbolKind.METHOD
|
|
138
|
+
else:
|
|
139
|
+
sym_id = f"{file_path}::{f_name}"
|
|
140
|
+
qual_name = f_name
|
|
141
|
+
parent_sym_id = file_path
|
|
142
|
+
sym_kind = SymbolKind.FUNCTION
|
|
143
|
+
end_line = self._find_block_end(lines, idx - 1)
|
|
144
|
+
loc = Location(file_path, idx, line.find(f_name), end_line, line.find(f_name) + len(f_name))
|
|
145
|
+
symbols.append(Symbol(
|
|
146
|
+
id=sym_id,
|
|
147
|
+
name=f_name,
|
|
148
|
+
qualified_name=qual_name,
|
|
149
|
+
kind=sym_kind,
|
|
150
|
+
language=self.language,
|
|
151
|
+
location=loc,
|
|
152
|
+
parent_id=parent_sym_id,
|
|
153
|
+
signature=f"{f_name}({params})",
|
|
154
|
+
visibility=Visibility.PUBLIC,
|
|
155
|
+
modifiers=(),
|
|
156
|
+
content_hash=self._extract_body_hash(lines, idx - 1),
|
|
157
|
+
))
|
|
158
|
+
|
|
159
|
+
return symbols
|
|
160
|
+
|
|
161
|
+
def extract_edges(self, parse: ParseResult, symbols: Sequence[Symbol]) -> Iterable[Edge]:
|
|
162
|
+
"""Extract relationship edges for C/C++ AST.
|
|
163
|
+
Example:
|
|
164
|
+
>>> analyzer = CppLanguageAnalyzer()
|
|
165
|
+
>>> res = analyzer.parse(Path("main.cpp"), '#include "helper.h"')
|
|
166
|
+
>>> syms = list(analyzer.extract_symbols(res))
|
|
167
|
+
>>> edges = list(analyzer.extract_edges(res, syms))
|
|
168
|
+
>>> len(edges) >= 1
|
|
169
|
+
True
|
|
170
|
+
"""
|
|
171
|
+
edges: List[Edge] = []
|
|
172
|
+
file_path = parse.file_path
|
|
173
|
+
sym_map = {s.id: s for s in symbols}
|
|
174
|
+
class_pattern = re.compile(r'(?:class|struct)\s+([A-Za-z0-9_]+)(?:\s*:\s*(?:public|protected|private)?\s*([A-Za-z0-9_]+))?')
|
|
175
|
+
call_pattern = re.compile(r'(?<![A-Za-z0-9_$-])\b([A-Za-z_][A-Za-z0-9_]*)\s*\(')
|
|
176
|
+
keywords = {"if", "for", "while", "switch", "catch", "return", "throw", "sizeof", "sizeof", "static_cast", "reinterpret_cast", "dynamic_cast"}
|
|
177
|
+
|
|
178
|
+
for s in symbols:
|
|
179
|
+
if s.parent_id and s.parent_id in sym_map:
|
|
180
|
+
edges.append(Edge(
|
|
181
|
+
source_id=s.parent_id,
|
|
182
|
+
target_id=s.id,
|
|
183
|
+
kind=EdgeKind.CONTAINS,
|
|
184
|
+
weight=1.0,
|
|
185
|
+
confidence=1.0,
|
|
186
|
+
location=s.location,
|
|
187
|
+
))
|
|
188
|
+
if s.kind == SymbolKind.IMPORT:
|
|
189
|
+
target_mod = s.qualified_name.replace("import:", "")
|
|
190
|
+
edges.append(Edge(
|
|
191
|
+
source_id=file_path,
|
|
192
|
+
target_id=f"module::{target_mod}",
|
|
193
|
+
kind=EdgeKind.IMPORTS,
|
|
194
|
+
weight=1.0,
|
|
195
|
+
confidence=1.0,
|
|
196
|
+
location=s.location,
|
|
197
|
+
))
|
|
198
|
+
|
|
199
|
+
lines = parse.source.splitlines()
|
|
200
|
+
for idx, line in enumerate(lines, start=1):
|
|
201
|
+
line_str = line.strip()
|
|
202
|
+
if not line_str or line_str.startswith("//") or line_str.startswith("/*") or line_str.startswith("*"):
|
|
203
|
+
continue
|
|
204
|
+
for match in class_pattern.finditer(line_str):
|
|
205
|
+
cls_name = match.group(1)
|
|
206
|
+
base_name = match.group(2)
|
|
207
|
+
if base_name and base_name not in keywords:
|
|
208
|
+
cls_sym_id = f"{file_path}::{cls_name}"
|
|
209
|
+
loc = Location(file_path, idx, line.find(base_name), idx, line.find(base_name) + len(base_name))
|
|
210
|
+
edges.append(Edge(
|
|
211
|
+
source_id=cls_sym_id,
|
|
212
|
+
target_id=f"module::{base_name}",
|
|
213
|
+
kind=EdgeKind.INHERITS,
|
|
214
|
+
weight=1.0,
|
|
215
|
+
confidence=0.9,
|
|
216
|
+
location=loc,
|
|
217
|
+
))
|
|
218
|
+
|
|
219
|
+
func_syms = [s for s in symbols if s.kind in (SymbolKind.FUNCTION, SymbolKind.METHOD)]
|
|
220
|
+
name_to_syms: dict[str, list[Symbol]] = {}
|
|
221
|
+
for s in func_syms:
|
|
222
|
+
name_to_syms.setdefault(s.name, []).append(s)
|
|
223
|
+
|
|
224
|
+
for f in func_syms:
|
|
225
|
+
if not f.location:
|
|
226
|
+
continue
|
|
227
|
+
f_lines = lines[f.location.start_line - 1 : f.location.end_line]
|
|
228
|
+
for f_line in f_lines:
|
|
229
|
+
for match in call_pattern.finditer(f_line):
|
|
230
|
+
callee_name = match.group(1)
|
|
231
|
+
if callee_name in keywords or callee_name == f.name:
|
|
232
|
+
continue
|
|
233
|
+
if callee_name in name_to_syms:
|
|
234
|
+
target = name_to_syms[callee_name][0]
|
|
235
|
+
edges.append(Edge(
|
|
236
|
+
source_id=f.id,
|
|
237
|
+
target_id=target.id,
|
|
238
|
+
kind=EdgeKind.CALLS,
|
|
239
|
+
weight=1.0,
|
|
240
|
+
confidence=0.85,
|
|
241
|
+
location=f.location,
|
|
242
|
+
))
|
|
243
|
+
|
|
244
|
+
return edges
|
|
245
|
+
|
|
246
|
+
def resolve_import(self, ref: ImportRef, ctx: ResolveContext) -> Iterable[str]:
|
|
247
|
+
"""Resolve C/C++ include references to project header files.
|
|
248
|
+
Example:
|
|
249
|
+
>>> analyzer = CppLanguageAnalyzer()
|
|
250
|
+
>>> ctx = ResolveContext(Path("."))
|
|
251
|
+
>>> list(analyzer.resolve_import(ImportRef("main.cpp", "helper.h", None, False), ctx))
|
|
252
|
+
[]
|
|
253
|
+
"""
|
|
254
|
+
return ctx.resolve_path(ref.file_path, ref.module_name, self.language)
|
|
255
|
+
|
|
256
|
+
def _find_block_end(self, lines: List[str], start_idx: int) -> int:
|
|
257
|
+
open_braces = 0
|
|
258
|
+
found_brace = False
|
|
259
|
+
for i in range(start_idx, len(lines)):
|
|
260
|
+
line = lines[i]
|
|
261
|
+
open_braces += line.count("{") - line.count("}")
|
|
262
|
+
if "{" in line:
|
|
263
|
+
found_brace = True
|
|
264
|
+
if found_brace and open_braces <= 0:
|
|
265
|
+
return i + 1
|
|
266
|
+
return min(start_idx + 30, len(lines))
|
|
267
|
+
|
|
268
|
+
def _extract_body_hash(self, lines: List[str], start_idx: int) -> str:
|
|
269
|
+
end_idx = self._find_block_end(lines, start_idx)
|
|
270
|
+
snippet = "\n".join(lines[start_idx:end_idx])
|
|
271
|
+
return hashlib.sha256(snippet.encode("utf-8")).hexdigest()
|
codeui/lang/csharp.py
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""C# 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 CSharpLanguageAnalyzer(LanguageAnalyzer):
|
|
14
|
+
"""C# language analyzer.
|
|
15
|
+
Example:
|
|
16
|
+
>>> analyzer = CSharpLanguageAnalyzer()
|
|
17
|
+
>>> res = analyzer.parse(Path("App.cs"), 'public class App { public void Run() {} }')
|
|
18
|
+
>>> syms = list(analyzer.extract_symbols(res))
|
|
19
|
+
>>> any(s.name == "App" for s in syms)
|
|
20
|
+
True
|
|
21
|
+
"""
|
|
22
|
+
language: ClassVar[str] = "csharp"
|
|
23
|
+
extensions: ClassVar[tuple[str, ...]] = (".cs",)
|
|
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 C# source code into ParseResult.
|
|
29
|
+
Example:
|
|
30
|
+
>>> analyzer = CSharpLanguageAnalyzer()
|
|
31
|
+
>>> res = analyzer.parse(Path("App.cs"), "using System;")
|
|
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, records, structs, and methods from C# source.
|
|
46
|
+
Example:
|
|
47
|
+
>>> analyzer = CSharpLanguageAnalyzer()
|
|
48
|
+
>>> res = analyzer.parse(Path("User.cs"), "public class User { public string GetName() { return name; } }")
|
|
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
|
+
using_pattern = re.compile(r'using\s+(?:static\s+)?([A-Za-z0-9_.]+)\s*;')
|
|
75
|
+
class_pattern = re.compile(r'(?:public|protected|private|internal|file)?\s*(?:static|abstract|sealed|partial)?\s*(?:class|interface|record|struct)\s+([A-Za-z0-9_]+)(?:<[^>]+>)?(?:\s*:\s*([^{]+))?')
|
|
76
|
+
method_pattern = re.compile(r'(?:public|protected|private|internal|static|async|override|virtual|abstract|partial|\s)+[\w<>\[\]?,.]+\s+([A-Za-z0-9_]+)\s*\(([^)]*)\)')
|
|
77
|
+
|
|
78
|
+
cs_keywords = {"if", "for", "while", "switch", "catch", "return", "throw", "new", "class", "interface", "namespace", "using", "else", "try", "finally"}
|
|
79
|
+
class_ranges: List[tuple[str, str, int, int, str]] = []
|
|
80
|
+
|
|
81
|
+
for idx, line in enumerate(lines, start=1):
|
|
82
|
+
line_str = line.strip()
|
|
83
|
+
if not line_str or line_str.startswith("//") or line_str.startswith("*"):
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
for match in using_pattern.finditer(line_str):
|
|
87
|
+
using_path = match.group(1)
|
|
88
|
+
sym_id = f"import:{using_path}"
|
|
89
|
+
symbols.append(Symbol(
|
|
90
|
+
id=sym_id,
|
|
91
|
+
name=using_path.split(".")[-1],
|
|
92
|
+
qualified_name=f"import:{using_path}",
|
|
93
|
+
kind=SymbolKind.IMPORT,
|
|
94
|
+
language=self.language,
|
|
95
|
+
location=Location(file_path, idx, line.find(using_path), idx, line.find(using_path) + len(using_path)),
|
|
96
|
+
parent_id=file_path,
|
|
97
|
+
signature=f"using {using_path}",
|
|
98
|
+
visibility=Visibility.PUBLIC,
|
|
99
|
+
modifiers=(),
|
|
100
|
+
content_hash=self._extract_body_hash(lines, idx - 1),
|
|
101
|
+
))
|
|
102
|
+
|
|
103
|
+
for match in class_pattern.finditer(line_str):
|
|
104
|
+
cls_name = match.group(1)
|
|
105
|
+
if cls_name in cs_keywords:
|
|
106
|
+
continue
|
|
107
|
+
bases = match.group(2) or ""
|
|
108
|
+
sym_id = f"{file_path}::{cls_name}"
|
|
109
|
+
end_line = self._find_block_end(lines, idx - 1)
|
|
110
|
+
class_ranges.append((cls_name, sym_id, idx, end_line, bases))
|
|
111
|
+
loc = Location(file_path, idx, line.find(cls_name), end_line, line.find(cls_name) + len(cls_name))
|
|
112
|
+
vis = Visibility.PUBLIC if "public" in line_str else (Visibility.PRIVATE if "private" in line_str else Visibility.INTERNAL)
|
|
113
|
+
symbols.append(Symbol(
|
|
114
|
+
id=sym_id,
|
|
115
|
+
name=cls_name,
|
|
116
|
+
qualified_name=cls_name,
|
|
117
|
+
kind=SymbolKind.CLASS,
|
|
118
|
+
language=self.language,
|
|
119
|
+
location=loc,
|
|
120
|
+
parent_id=file_path,
|
|
121
|
+
signature=f"class {cls_name}",
|
|
122
|
+
visibility=vis,
|
|
123
|
+
modifiers=("public",) if "public" in line_str else (),
|
|
124
|
+
content_hash=self._extract_body_hash(lines, idx - 1),
|
|
125
|
+
))
|
|
126
|
+
|
|
127
|
+
for match in method_pattern.finditer(line_str):
|
|
128
|
+
m_name = match.group(1)
|
|
129
|
+
params = match.group(2)
|
|
130
|
+
if m_name in cs_keywords:
|
|
131
|
+
continue
|
|
132
|
+
enclosing = next((c for c in reversed(class_ranges) if c[2] <= idx <= c[3]), None)
|
|
133
|
+
if enclosing:
|
|
134
|
+
cls_name, cls_sym_id, _, _, _ = enclosing
|
|
135
|
+
sym_id = f"{file_path}::{cls_name}::{m_name}"
|
|
136
|
+
qual_name = f"{cls_name}.{m_name}"
|
|
137
|
+
parent_sym_id = cls_sym_id
|
|
138
|
+
else:
|
|
139
|
+
sym_id = f"{file_path}::{m_name}"
|
|
140
|
+
qual_name = m_name
|
|
141
|
+
parent_sym_id = file_path
|
|
142
|
+
end_line = self._find_block_end(lines, idx - 1)
|
|
143
|
+
loc = Location(file_path, idx, line.find(m_name), end_line, line.find(m_name) + len(m_name))
|
|
144
|
+
vis = Visibility.PUBLIC if "public" in line_str else (Visibility.PRIVATE if "private" in line_str else Visibility.INTERNAL)
|
|
145
|
+
symbols.append(Symbol(
|
|
146
|
+
id=sym_id,
|
|
147
|
+
name=m_name,
|
|
148
|
+
qualified_name=qual_name,
|
|
149
|
+
kind=SymbolKind.METHOD,
|
|
150
|
+
language=self.language,
|
|
151
|
+
location=loc,
|
|
152
|
+
parent_id=parent_sym_id,
|
|
153
|
+
signature=f"{m_name}({params})",
|
|
154
|
+
visibility=vis,
|
|
155
|
+
modifiers=("public",) if "public" in line_str else (),
|
|
156
|
+
content_hash=self._extract_body_hash(lines, idx - 1),
|
|
157
|
+
))
|
|
158
|
+
|
|
159
|
+
return symbols
|
|
160
|
+
|
|
161
|
+
def extract_edges(self, parse: ParseResult, symbols: Sequence[Symbol]) -> Iterable[Edge]:
|
|
162
|
+
"""Extract relationship edges for C# AST.
|
|
163
|
+
Example:
|
|
164
|
+
>>> analyzer = CSharpLanguageAnalyzer()
|
|
165
|
+
>>> res = analyzer.parse(Path("App.cs"), "using System.Text;")
|
|
166
|
+
>>> syms = list(analyzer.extract_symbols(res))
|
|
167
|
+
>>> edges = list(analyzer.extract_edges(res, syms))
|
|
168
|
+
>>> len(edges) >= 1
|
|
169
|
+
True
|
|
170
|
+
"""
|
|
171
|
+
edges: List[Edge] = []
|
|
172
|
+
file_path = parse.file_path
|
|
173
|
+
sym_map = {s.id: s for s in symbols}
|
|
174
|
+
class_pattern = re.compile(r'(?:class|interface|record|struct)\s+([A-Za-z0-9_]+)(?:<[^>]+>)?(?:\s*:\s*([^{]+))?')
|
|
175
|
+
call_pattern = re.compile(r'(?<![A-Za-z0-9_$-])\b([A-Za-z_][A-Za-z0-9_]*)\s*\(')
|
|
176
|
+
cs_keywords = {"if", "for", "while", "switch", "catch", "return", "throw", "new", "typeof", "sizeof", "default"}
|
|
177
|
+
|
|
178
|
+
for s in symbols:
|
|
179
|
+
if s.parent_id and s.parent_id in sym_map:
|
|
180
|
+
edges.append(Edge(
|
|
181
|
+
source_id=s.parent_id,
|
|
182
|
+
target_id=s.id,
|
|
183
|
+
kind=EdgeKind.CONTAINS,
|
|
184
|
+
weight=1.0,
|
|
185
|
+
confidence=1.0,
|
|
186
|
+
location=s.location,
|
|
187
|
+
))
|
|
188
|
+
if s.kind == SymbolKind.IMPORT:
|
|
189
|
+
target_mod = s.qualified_name.replace("import:", "")
|
|
190
|
+
edges.append(Edge(
|
|
191
|
+
source_id=file_path,
|
|
192
|
+
target_id=f"module::{target_mod}",
|
|
193
|
+
kind=EdgeKind.IMPORTS,
|
|
194
|
+
weight=1.0,
|
|
195
|
+
confidence=1.0,
|
|
196
|
+
location=s.location,
|
|
197
|
+
))
|
|
198
|
+
|
|
199
|
+
lines = parse.source.splitlines()
|
|
200
|
+
for idx, line in enumerate(lines, start=1):
|
|
201
|
+
line_str = line.strip()
|
|
202
|
+
if not line_str or line_str.startswith("//") or line_str.startswith("*"):
|
|
203
|
+
continue
|
|
204
|
+
for match in class_pattern.finditer(line_str):
|
|
205
|
+
cls_name = match.group(1)
|
|
206
|
+
bases_raw = match.group(2)
|
|
207
|
+
if not bases_raw:
|
|
208
|
+
continue
|
|
209
|
+
cls_sym_id = f"{file_path}::{cls_name}"
|
|
210
|
+
for base_item in bases_raw.split(","):
|
|
211
|
+
base_clean = base_item.strip().split("<")[0].strip()
|
|
212
|
+
if not base_clean or base_clean in cs_keywords:
|
|
213
|
+
continue
|
|
214
|
+
is_iface = len(base_clean) > 1 and base_clean[0] == "I" and base_clean[1].isupper()
|
|
215
|
+
kind = EdgeKind.IMPLEMENTS if is_iface else EdgeKind.INHERITS
|
|
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=kind,
|
|
221
|
+
weight=1.0,
|
|
222
|
+
confidence=0.9,
|
|
223
|
+
location=loc,
|
|
224
|
+
))
|
|
225
|
+
|
|
226
|
+
method_syms = [s for s in symbols if s.kind in (SymbolKind.METHOD, SymbolKind.FUNCTION)]
|
|
227
|
+
name_to_syms: dict[str, list[Symbol]] = {}
|
|
228
|
+
for s in method_syms:
|
|
229
|
+
name_to_syms.setdefault(s.name, []).append(s)
|
|
230
|
+
|
|
231
|
+
for m in method_syms:
|
|
232
|
+
if not m.location:
|
|
233
|
+
continue
|
|
234
|
+
m_lines = lines[m.location.start_line - 1 : m.location.end_line]
|
|
235
|
+
for m_line in m_lines:
|
|
236
|
+
for match in call_pattern.finditer(m_line):
|
|
237
|
+
callee_name = match.group(1)
|
|
238
|
+
if callee_name in cs_keywords or callee_name == m.name:
|
|
239
|
+
continue
|
|
240
|
+
if callee_name in name_to_syms:
|
|
241
|
+
target = name_to_syms[callee_name][0]
|
|
242
|
+
edges.append(Edge(
|
|
243
|
+
source_id=m.id,
|
|
244
|
+
target_id=target.id,
|
|
245
|
+
kind=EdgeKind.CALLS,
|
|
246
|
+
weight=1.0,
|
|
247
|
+
confidence=0.85,
|
|
248
|
+
location=m.location,
|
|
249
|
+
))
|
|
250
|
+
|
|
251
|
+
return edges
|
|
252
|
+
|
|
253
|
+
def resolve_import(self, ref: ImportRef, ctx: ResolveContext) -> Iterable[str]:
|
|
254
|
+
"""Resolve C# using references to project files.
|
|
255
|
+
Example:
|
|
256
|
+
>>> analyzer = CSharpLanguageAnalyzer()
|
|
257
|
+
>>> ctx = ResolveContext(Path("."))
|
|
258
|
+
>>> list(analyzer.resolve_import(ImportRef("App.cs", "System.Text", None, False), ctx))
|
|
259
|
+
[]
|
|
260
|
+
"""
|
|
261
|
+
clean_path = ref.module_name.replace(".", "/")
|
|
262
|
+
return ctx.resolve_path(ref.file_path, clean_path, self.language)
|