Navegador

navegador / navegador / ingestion / csharp.py
Blame History Raw 252 lines
1
"""
2
C# AST parser — extracts classes, interfaces, structs, methods, and
3
imports (using directives) from .cs files using tree-sitter.
4
"""
5
6
import logging
7
from pathlib import Path
8
9
from navegador.graph.schema import EdgeType, NodeLabel
10
from navegador.graph.store import GraphStore
11
from navegador.ingestion.parser import LanguageParser
12
13
logger = logging.getLogger(__name__)
14
15
16
def _get_csharp_language():
17
try:
18
import tree_sitter_c_sharp as tscsharp # type: ignore[import]
19
from tree_sitter import Language
20
21
return Language(tscsharp.language())
22
except ImportError as e:
23
raise ImportError("Install tree-sitter-c-sharp: pip install tree-sitter-c-sharp") from e
24
25
26
def _node_text(node, source: bytes) -> str:
27
return source[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
28
29
30
class CSharpParser(LanguageParser):
31
"""Parses C# source files into the navegador graph."""
32
33
def __init__(self) -> None:
34
from tree_sitter import Parser # type: ignore[import]
35
36
self._parser = Parser(_get_csharp_language())
37
38
def parse_file(self, path: Path, repo_root: Path, store: GraphStore) -> dict[str, int]:
39
source = path.read_bytes()
40
tree = self._parser.parse(source)
41
rel_path = str(path.relative_to(repo_root))
42
43
store.create_node(
44
NodeLabel.File,
45
{
46
"name": path.name,
47
"path": rel_path,
48
"language": "csharp",
49
"line_count": source.count(b"\n"),
50
},
51
)
52
53
stats = {"functions": 0, "classes": 0, "edges": 0}
54
self._walk(tree.root_node, source, rel_path, store, stats, class_name=None)
55
return stats
56
57
# ── AST walker ────────────────────────────────────────────────────────────
58
59
def _walk(
60
self,
61
node,
62
source: bytes,
63
file_path: str,
64
store: GraphStore,
65
stats: dict,
66
class_name: str | None,
67
) -> None:
68
if node.type in (
69
"class_declaration",
70
"interface_declaration",
71
"struct_declaration",
72
"record_declaration",
73
):
74
self._handle_class(node, source, file_path, store, stats)
75
return
76
if node.type in ("method_declaration", "constructor_declaration"):
77
self._handle_method(node, source, file_path, store, stats, class_name)
78
return
79
if node.type == "using_directive":
80
self._handle_using(node, source, file_path, store, stats)
81
return
82
for child in node.children:
83
self._walk(child, source, file_path, store, stats, class_name)
84
85
# ── Handlers ──────────────────────────────────────────────────────────────
86
87
def _handle_class(
88
self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
89
) -> None:
90
name_node = node.child_by_field_name("name")
91
if not name_node:
92
name_node = next((c for c in node.children if c.type == "identifier"), None)
93
if not name_node:
94
return
95
name = _node_text(name_node, source)
96
97
store.create_node(
98
NodeLabel.Class,
99
{
100
"name": name,
101
"file_path": file_path,
102
"line_start": node.start_point[0] + 1,
103
"line_end": node.end_point[0] + 1,
104
"docstring": "",
105
},
106
)
107
store.create_edge(
108
NodeLabel.File,
109
{"path": file_path},
110
EdgeType.CONTAINS,
111
NodeLabel.Class,
112
{"name": name, "file_path": file_path},
113
)
114
stats["classes"] += 1
115
stats["edges"] += 1
116
117
# Superclass / interfaces
118
bases = node.child_by_field_name("bases")
119
if bases:
120
for child in bases.children:
121
if child.type == "identifier":
122
parent_name = _node_text(child, source)
123
store.create_edge(
124
NodeLabel.Class,
125
{"name": name, "file_path": file_path},
126
EdgeType.INHERITS,
127
NodeLabel.Class,
128
{"name": parent_name, "file_path": file_path},
129
)
130
stats["edges"] += 1
131
132
# Walk class body for methods
133
body = node.child_by_field_name("body")
134
if not body:
135
body = next((c for c in node.children if c.type == "declaration_list"), None)
136
if body:
137
for child in body.children:
138
if child.type in ("method_declaration", "constructor_declaration"):
139
self._handle_method(child, source, file_path, store, stats, class_name=name)
140
141
def _handle_method(
142
self,
143
node,
144
source: bytes,
145
file_path: str,
146
store: GraphStore,
147
stats: dict,
148
class_name: str | None,
149
) -> None:
150
name_node = node.child_by_field_name("name")
151
if not name_node:
152
name_node = next((c for c in node.children if c.type == "identifier"), None)
153
if not name_node:
154
return
155
name = _node_text(name_node, source)
156
157
label = NodeLabel.Method if class_name else NodeLabel.Function
158
store.create_node(
159
label,
160
{
161
"name": name,
162
"file_path": file_path,
163
"line_start": node.start_point[0] + 1,
164
"line_end": node.end_point[0] + 1,
165
"docstring": "",
166
"class_name": class_name or "",
167
},
168
)
169
170
container_label = NodeLabel.Class if class_name else NodeLabel.File
171
container_key = (
172
{"name": class_name, "file_path": file_path} if class_name else {"path": file_path}
173
)
174
store.create_edge(
175
container_label,
176
container_key,
177
EdgeType.CONTAINS,
178
label,
179
{"name": name, "file_path": file_path},
180
)
181
stats["functions"] += 1
182
stats["edges"] += 1
183
184
self._extract_calls(node, source, file_path, name, label, store, stats)
185
186
def _handle_using(
187
self, node, source: bytes, file_path: str, store: GraphStore, stats: dict
188
) -> None:
189
raw = _node_text(node, source).strip()
190
# "using System.Collections.Generic;" or "using static ..."
191
module = raw.removeprefix("using").removeprefix(" static").removesuffix(";").strip()
192
if not module:
193
return
194
store.create_node(
195
NodeLabel.Import,
196
{
197
"name": module,
198
"file_path": file_path,
199
"line_start": node.start_point[0] + 1,
200
"module": module,
201
},
202
)
203
store.create_edge(
204
NodeLabel.File,
205
{"path": file_path},
206
EdgeType.IMPORTS,
207
NodeLabel.Import,
208
{"name": module, "file_path": file_path},
209
)
210
stats["edges"] += 1
211
212
def _extract_calls(
213
self,
214
fn_node,
215
source: bytes,
216
file_path: str,
217
fn_name: str,
218
fn_label: str,
219
store: GraphStore,
220
stats: dict,
221
) -> None:
222
def walk(node):
223
if node.type == "invocation_expression":
224
func = node.child_by_field_name("function")
225
if not func:
226
func = next(
227
(
228
c
229
for c in node.children
230
if c.type in ("identifier", "member_access_expression")
231
),
232
None,
233
)
234
if func:
235
callee = _node_text(func, source).split(".")[-1]
236
store.create_edge(
237
fn_label,
238
{"name": fn_name, "file_path": file_path},
239
EdgeType.CALLS,
240
NodeLabel.Function,
241
{"name": callee, "file_path": file_path},
242
)
243
stats["edges"] += 1
244
for child in node.children:
245
walk(child)
246
247
body = fn_node.child_by_field_name("body")
248
if not body:
249
body = next((c for c in fn_node.children if c.type == "block"), None)
250
if body:
251
walk(body)
252

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button