1use super::{add_node_if_missing, make_file_node};
2use crate::error::{CodeSynapseError, Result};
3use crate::extract::{make_id, ImportNode, LanguageExtractor};
4use crate::types::{Edge, ExtractionFragment, Node};
5use std::collections::{HashMap, HashSet};
6use std::path::Path;
7use tree_sitter::{Node as TsNode, Parser};
8
9pub struct TsCSharpExtractor;
10
11fn cs_text(source: &[u8], node: &TsNode<'_>) -> String {
12 std::str::from_utf8(&source[node.start_byte()..node.end_byte()])
13 .unwrap_or("")
14 .trim()
15 .to_string()
16}
17
18fn cs_node(id: String, label: String, file_type: &str, path: &Path) -> Node {
19 Node {
20 id,
21 label,
22 file_type: file_type.to_string(),
23 source_file: path.to_string_lossy().to_string(),
24 source_location: None,
25 community: None,
26 rationale: None,
27 docstring: None,
28 metadata: HashMap::new(),
29 }
30}
31
32fn prescan_interfaces(root: TsNode<'_>, source: &[u8]) -> HashSet<String> {
34 let mut ifaces = HashSet::new();
35 prescan_walk(root, source, &mut ifaces);
36 ifaces
37}
38
39fn prescan_walk(node: TsNode<'_>, source: &[u8], ifaces: &mut HashSet<String>) {
40 if node.kind() == "interface_declaration" {
41 if let Some(name_node) = node.child_by_field_name("name") {
42 let name = cs_text(source, &name_node);
43 ifaces.insert(name);
44 }
45 }
46 for i in 0..node.child_count() {
47 if let Some(child) = node.child(i) {
48 prescan_walk(child, source, ifaces);
49 }
50 }
51}
52
53impl TsCSharpExtractor {
54 pub fn extract(source: &[u8], path: &Path) -> Result<ExtractionFragment> {
55 let (file_id, _, file_node) = make_file_node(path);
56 let mut fragment = ExtractionFragment {
57 nodes: vec![file_node],
58 edges: vec![],
59 };
60
61 let lang = tree_sitter_c_sharp::LANGUAGE.into();
62 let mut parser = Parser::new();
63 parser
64 .set_language(&lang)
65 .map_err(|e| CodeSynapseError::Parse(format!("csharp lang: {}", e)))?;
66
67 let tree = parser
68 .parse(source, None)
69 .ok_or_else(|| CodeSynapseError::Parse("csharp parse failed".to_string()))?;
70
71 let stem = file_id.clone();
72
73 let interface_names = prescan_interfaces(tree.root_node(), source);
74
75 Self::walk(
76 tree.root_node(),
77 source,
78 path,
79 &file_id,
80 &stem,
81 &interface_names,
82 None,
83 &mut fragment,
84 );
85
86 Ok(fragment)
87 }
88
89 #[allow(clippy::too_many_arguments)]
90 fn walk(
91 node: TsNode<'_>,
92 source: &[u8],
93 path: &Path,
94 file_id: &str,
95 stem: &str,
96 ifaces: &HashSet<String>,
97 class_id: Option<&str>,
98 fragment: &mut ExtractionFragment,
99 ) {
100 match node.kind() {
101 "using_directive" => {
102 for i in 0..node.child_count() {
105 if let Some(child) = node.child(i) {
106 match child.kind() {
107 "identifier" | "qualified_name" => {
108 let full = cs_text(source, &child);
109 let leaf = full.split('.').next_back().unwrap_or(&full).to_string();
110 let leaf_id = make_id(&[&leaf]);
111 add_node_if_missing(
112 fragment,
113 cs_node(leaf_id.clone(), leaf, "code", path),
114 );
115 fragment.edges.push(Edge {
116 source: file_id.to_string(),
117 target: leaf_id,
118 relation: "imports".to_string(),
119 confidence: "EXTRACTED".to_string(),
120 source_file: Some(path.to_string_lossy().to_string()),
121 weight: 1.0,
122 context: Some("import".to_string()),
123 });
124 }
125 _ => {}
126 }
127 }
128 }
129 }
130 "namespace_declaration" => {
131 if let Some(name_node) = node.child_by_field_name("name") {
132 let name = cs_text(source, &name_node);
133 let ns_id = make_id(&[file_id, &name, "ns"]);
134 add_node_if_missing(
135 fragment,
136 Node {
137 id: ns_id.clone(),
138 label: format!("{} namespace", name),
139 file_type: "namespace".to_string(),
140 source_file: path.to_string_lossy().to_string(),
141 source_location: None,
142 community: None,
143 rationale: None,
144 docstring: None,
145 metadata: {
146 let mut m = HashMap::new();
147 m.insert("kind".to_string(), "namespace".to_string());
148 m
149 },
150 },
151 );
152 fragment.edges.push(Edge {
153 source: file_id.to_string(),
154 target: ns_id.clone(),
155 relation: "contains".to_string(),
156 confidence: "EXTRACTED".to_string(),
157 source_file: Some(path.to_string_lossy().to_string()),
158 weight: 1.0,
159 context: None,
160 });
161 }
162 for i in 0..node.child_count() {
164 if let Some(child) = node.child(i) {
165 Self::walk(
166 child, source, path, file_id, stem, ifaces, class_id, fragment,
167 );
168 }
169 }
170 }
171 "class_declaration" => {
172 let name_node = match node.child_by_field_name("name") {
173 Some(n) => n,
174 None => return,
175 };
176 let class_name = cs_text(source, &name_node);
177 let new_class_id = make_id(&[stem, &class_name]);
178 add_node_if_missing(
179 fragment,
180 cs_node(new_class_id.clone(), class_name.clone(), "class", path),
181 );
182 fragment.edges.push(Edge {
183 source: file_id.to_string(),
184 target: new_class_id.clone(),
185 relation: "contains".to_string(),
186 confidence: "EXTRACTED".to_string(),
187 source_file: Some(path.to_string_lossy().to_string()),
188 weight: 1.0,
189 context: None,
190 });
191
192 for i in 0..node.child_count() {
194 if let Some(child) = node.child(i) {
195 if child.kind() == "base_list" {
196 Self::handle_base_list(
197 &child,
198 source,
199 path,
200 &new_class_id,
201 fragment,
202 ifaces,
203 stem,
204 );
205 break;
206 }
207 }
208 }
209
210 if let Some(body) = node.child_by_field_name("body") {
212 for i in 0..body.child_count() {
213 if let Some(child) = body.child(i) {
214 Self::walk(
215 child,
216 source,
217 path,
218 file_id,
219 stem,
220 ifaces,
221 Some(&new_class_id),
222 fragment,
223 );
224 }
225 }
226 }
227 }
228 "interface_declaration" => {
229 if let Some(name_node) = node.child_by_field_name("name") {
230 let name = cs_text(source, &name_node);
231 let id = make_id(&[stem, &name]);
232 add_node_if_missing(fragment, cs_node(id.clone(), name, "interface", path));
233 fragment.edges.push(Edge {
234 source: file_id.to_string(),
235 target: id,
236 relation: "contains".to_string(),
237 confidence: "EXTRACTED".to_string(),
238 source_file: Some(path.to_string_lossy().to_string()),
239 weight: 1.0,
240 context: None,
241 });
242 }
243 }
244 "method_declaration" => {
245 let owner = class_id.unwrap_or(file_id);
246 Self::extract_method(node, source, path, file_id, stem, owner, fragment);
247 }
248 "field_declaration" => {
249 if let Some(owner) = class_id {
250 Self::extract_field(node, source, path, owner, fragment, stem);
251 }
252 }
253 "invocation_expression" => {
254 if let Some(caller) = class_id {
255 Self::emit_call(node, source, path, caller, fragment, stem);
256 }
257 }
258 _ => {
259 for i in 0..node.child_count() {
260 if let Some(child) = node.child(i) {
261 Self::walk(
262 child, source, path, file_id, stem, ifaces, class_id, fragment,
263 );
264 }
265 }
266 }
267 }
268 }
269
270 fn handle_base_list(
271 bases: &TsNode<'_>,
272 source: &[u8],
273 path: &Path,
274 class_id: &str,
275 fragment: &mut ExtractionFragment,
276 ifaces: &HashSet<String>,
277 stem: &str,
278 ) {
279 for i in 0..bases.child_count() {
280 if let Some(child) = bases.child(i) {
281 let type_name = match child.kind() {
282 "identifier" => cs_text(source, &child),
283 "generic_name" => {
284 if let Some(name_n) = child.child_by_field_name("name") {
285 cs_text(source, &name_n)
286 } else {
287 continue;
288 }
289 }
290 _ => continue,
291 };
292 let base_id = make_id(&[stem, &type_name]);
293 add_node_if_missing(
294 fragment,
295 cs_node(base_id.clone(), type_name.clone(), "code", path),
296 );
297 let relation = if ifaces.contains(&type_name) {
298 "implements"
299 } else {
300 "inherits"
301 };
302 fragment.edges.push(Edge {
303 source: class_id.to_string(),
304 target: base_id,
305 relation: relation.to_string(),
306 confidence: "EXTRACTED".to_string(),
307 source_file: Some(path.to_string_lossy().to_string()),
308 weight: 1.0,
309 context: None,
310 });
311 }
312 }
313 }
314
315 fn extract_method(
316 node: TsNode<'_>,
317 source: &[u8],
318 path: &Path,
319 file_id: &str,
320 stem: &str,
321 _class_id: &str,
322 fragment: &mut ExtractionFragment,
323 ) {
324 let name_node = match node.child_by_field_name("name") {
325 Some(n) => n,
326 None => return,
327 };
328 let method_name = cs_text(source, &name_node);
329 let method_id = make_id(&[stem, &method_name, "()"]);
330 add_node_if_missing(
331 fragment,
332 cs_node(
333 method_id.clone(),
334 format!("{}()", method_name),
335 "method",
336 path,
337 ),
338 );
339 fragment.edges.push(Edge {
340 source: file_id.to_string(),
341 target: method_id.clone(),
342 relation: "contains".to_string(),
343 confidence: "EXTRACTED".to_string(),
344 source_file: Some(path.to_string_lossy().to_string()),
345 weight: 1.0,
346 context: None,
347 });
348
349 if let Some(ret_type) = node.child_by_field_name("returns") {
351 Self::emit_type_ref_cs(
352 &ret_type,
353 source,
354 path,
355 &method_id,
356 fragment,
357 "return_type",
358 true,
359 );
360 }
361
362 if let Some(params) = node.child_by_field_name("parameters") {
364 for i in 0..params.child_count() {
365 if let Some(param) = params.child(i) {
366 if param.kind() == "parameter" {
367 if let Some(ptype) = param.child_by_field_name("type") {
368 Self::emit_type_ref_cs(
369 &ptype,
370 source,
371 path,
372 &method_id,
373 fragment,
374 "parameter_type",
375 false,
376 );
377 }
378 }
379 }
380 }
381 }
382
383 if let Some(body) = node.child_by_field_name("body") {
385 Self::walk_for_calls(body, source, path, &method_id, fragment, stem);
386 }
387 }
388
389 fn extract_field(
390 node: TsNode<'_>,
391 source: &[u8],
392 path: &Path,
393 class_id: &str,
394 fragment: &mut ExtractionFragment,
395 _stem: &str,
396 ) {
397 if let Some(type_node) = node.child_by_field_name("type") {
399 Self::emit_type_ref_cs(&type_node, source, path, class_id, fragment, "field", false);
400 }
401 }
402
403 fn walk_for_calls(
404 node: TsNode<'_>,
405 source: &[u8],
406 path: &Path,
407 caller_id: &str,
408 fragment: &mut ExtractionFragment,
409 stem: &str,
410 ) {
411 if node.kind() == "invocation_expression" {
412 Self::emit_call(node, source, path, caller_id, fragment, stem);
413 }
414 for i in 0..node.child_count() {
415 if let Some(child) = node.child(i) {
416 Self::walk_for_calls(child, source, path, caller_id, fragment, stem);
417 }
418 }
419 }
420
421 fn emit_call(
422 node: TsNode<'_>,
423 source: &[u8],
424 path: &Path,
425 caller_id: &str,
426 fragment: &mut ExtractionFragment,
427 stem: &str,
428 ) {
429 if let Some(func) = node.child_by_field_name("function") {
431 let callee_name = match func.kind() {
432 "member_access_expression" => {
433 func.child_by_field_name("name")
435 .map(|n| cs_text(source, &n))
436 .unwrap_or_default()
437 }
438 "identifier" => cs_text(source, &func),
439 _ => return,
440 };
441 if callee_name.is_empty() {
442 return;
443 }
444 let callee_id = make_id(&[stem, &callee_name, "()"]);
445 add_node_if_missing(
447 fragment,
448 cs_node(
449 callee_id.clone(),
450 format!("{}()", callee_name),
451 "code",
452 path,
453 ),
454 );
455 let already = fragment
456 .edges
457 .iter()
458 .any(|e| e.relation == "calls" && e.source == caller_id && e.target == callee_id);
459 if !already && caller_id != callee_id.as_str() {
460 fragment.edges.push(Edge {
461 source: caller_id.to_string(),
462 target: callee_id,
463 relation: "calls".to_string(),
464 confidence: "EXTRACTED".to_string(),
465 source_file: Some(path.to_string_lossy().to_string()),
466 weight: 1.0,
467 context: Some("call".to_string()),
468 });
469 }
470 }
471 }
472
473 fn emit_type_ref_cs(
474 type_node: &TsNode<'_>,
475 source: &[u8],
476 path: &Path,
477 owner_id: &str,
478 fragment: &mut ExtractionFragment,
479 context: &str,
480 emit_generics: bool,
481 ) {
482 match type_node.kind() {
483 "identifier" => {
484 let name = cs_text(source, type_node);
485 if !name.is_empty() && name != "void" && name != "var" {
486 let id = make_id(&[&name]);
487 add_node_if_missing(fragment, cs_node(id.clone(), name, "code", path));
488 fragment.edges.push(Edge {
489 source: owner_id.to_string(),
490 target: id,
491 relation: "references".to_string(),
492 confidence: "EXTRACTED".to_string(),
493 source_file: Some(path.to_string_lossy().to_string()),
494 weight: 1.0,
495 context: Some(context.to_string()),
496 });
497 }
498 }
499 "generic_name" => {
500 let mut type_name = String::new();
502 let mut args_node: Option<TsNode> = None;
503 for i in 0..type_node.child_count() {
504 if let Some(child) = type_node.child(i) {
505 match child.kind() {
506 "identifier" if type_name.is_empty() => {
507 type_name = cs_text(source, &child);
508 }
509 "type_argument_list" => {
510 args_node = Some(child);
511 }
512 _ => {}
513 }
514 }
515 }
516 if !type_name.is_empty() {
517 let id = make_id(&[&type_name]);
518 add_node_if_missing(fragment, cs_node(id.clone(), type_name, "code", path));
519 fragment.edges.push(Edge {
520 source: owner_id.to_string(),
521 target: id,
522 relation: "references".to_string(),
523 confidence: "EXTRACTED".to_string(),
524 source_file: Some(path.to_string_lossy().to_string()),
525 weight: 1.0,
526 context: Some(context.to_string()),
527 });
528 }
529 if emit_generics {
530 if let Some(args) = args_node {
531 for i in 0..args.child_count() {
532 if let Some(arg) = args.child(i) {
533 Self::emit_type_ref_cs(
534 &arg,
535 source,
536 path,
537 owner_id,
538 fragment,
539 "generic_arg",
540 false,
541 );
542 }
543 }
544 }
545 }
546 }
547 "nullable_type" | "array_type" | "pointer_type" => {
548 for i in 0..type_node.child_count() {
549 if let Some(child) = type_node.child(i) {
550 Self::emit_type_ref_cs(
551 &child,
552 source,
553 path,
554 owner_id,
555 fragment,
556 context,
557 emit_generics,
558 );
559 }
560 }
561 }
562 _ => {}
563 }
564 }
565}
566
567impl LanguageExtractor for TsCSharpExtractor {
568 fn file_extensions(&self) -> Vec<&'static str> {
569 vec!["cs"]
570 }
571 fn extract(&self, source: &[u8], path: &Path) -> Result<ExtractionFragment> {
572 Self::extract(source, path)
573 }
574 fn resolve_imports(&self, _imports: &[ImportNode]) -> Vec<Edge> {
575 vec![]
576 }
577 fn collect_type_refs(&self, _fragment: &mut ExtractionFragment) {}
578}