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/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)