1use std::collections::HashMap;
22use std::path::Path;
23
24use sqry_core::graph::unified::edge::FfiConvention;
25use sqry_core::graph::unified::edge::kind::TypeOfContext;
26use sqry_core::graph::unified::{GraphBuildHelper, NodeId, StagingGraph};
27use sqry_core::graph::{
28 GraphBuilder, GraphBuilderError, GraphResult, GraphSnapshot, Language, Span,
29};
30use tree_sitter::{Node, Tree};
31
32use super::local_scopes;
33use super::type_extractor::{extract_all_type_names_from_annotation, extract_type_string};
34
35const DEFAULT_MAX_SCOPE_DEPTH: usize = 6;
36
37const FILE_MODULE_NAME: &str = "<file_module>";
40
41#[derive(Debug, Clone, Copy)]
43pub struct CSharpGraphBuilder {
44 max_scope_depth: usize,
45}
46
47impl Default for CSharpGraphBuilder {
48 fn default() -> Self {
49 Self {
50 max_scope_depth: DEFAULT_MAX_SCOPE_DEPTH,
51 }
52 }
53}
54
55impl CSharpGraphBuilder {
56 #[must_use]
57 pub fn new(max_scope_depth: usize) -> Self {
58 Self { max_scope_depth }
59 }
60}
61
62impl GraphBuilder for CSharpGraphBuilder {
63 fn build_graph(
64 &self,
65 tree: &Tree,
66 content: &[u8],
67 file: &Path,
68 staging: &mut StagingGraph,
69 ) -> GraphResult<()> {
70 let mut helper = GraphBuildHelper::new(staging, file, Language::CSharp);
71
72 let ast_graph = ASTGraph::from_tree(tree, content, self.max_scope_depth).map_err(|e| {
74 GraphBuilderError::ParseError {
75 span: Span::default(),
76 reason: e,
77 }
78 })?;
79
80 let mut node_map = HashMap::new();
82
83 for context in ast_graph.contexts() {
85 let qualified_name = &context.qualified_name;
86 let span = Span::from_bytes(context.span.0, context.span.1);
87
88 let node_id = match context.kind {
89 ContextKind::Function { is_async } => {
90 helper.add_function_with_signature(
92 qualified_name,
93 Some(span),
94 is_async,
95 false,
96 None, context.return_type.as_deref(),
98 )
99 }
100 ContextKind::Method {
101 is_async,
102 is_static,
103 } => {
104 helper.add_method_with_signature(
106 qualified_name,
107 Some(span),
108 is_async,
109 is_static,
110 None, context.return_type.as_deref(),
112 )
113 }
114 ContextKind::Class => helper.add_class(qualified_name, Some(span)),
115 ContextKind::Interface => helper.add_interface(qualified_name, Some(span)),
116 };
117 node_map.insert(qualified_name.clone(), node_id);
118 }
119
120 let mut scope_tree = local_scopes::build(tree.root_node(), content)?;
122
123 let mut namespace_stack = Vec::new();
126 let mut class_stack = Vec::new();
127 let root = tree.root_node();
128 walk_tree_for_edges(
129 root,
130 content,
131 &ast_graph,
132 &mut helper,
133 &mut node_map,
134 &mut namespace_stack,
135 &mut class_stack,
136 &mut scope_tree,
137 )?;
138
139 Ok(())
140 }
141
142 fn language(&self) -> Language {
143 Language::CSharp
144 }
145
146 fn detect_cross_language_edges(
147 &self,
148 _snapshot: &GraphSnapshot,
149 ) -> GraphResult<Vec<sqry_core::graph::CodeEdge>> {
150 Ok(vec![])
153 }
154}
155
156#[derive(Debug, Clone)]
161enum ContextKind {
162 Function { is_async: bool },
163 Method { is_async: bool, is_static: bool },
164 Class,
165 Interface,
166}
167
168#[derive(Debug, Clone)]
169struct CallContext {
170 qualified_name: String,
171 span: (usize, usize),
172 kind: ContextKind,
173 class_name: Option<String>,
174 return_type: Option<String>,
176}
177
178struct ASTGraph {
179 contexts: Vec<CallContext>,
180 node_to_context: HashMap<usize, usize>,
181}
182
183impl ASTGraph {
184 fn from_tree(tree: &Tree, content: &[u8], max_depth: usize) -> Result<Self, String> {
185 let mut contexts = Vec::new();
186 let mut node_to_context = HashMap::new();
187 let mut scope_stack: Vec<String> = Vec::new();
188 let mut class_stack: Vec<String> = Vec::new();
189
190 let recursion_limits = sqry_core::config::RecursionLimits::load_or_default()
192 .map_err(|e| format!("Failed to load recursion limits: {e}"))?;
193 let file_ops_depth = recursion_limits
194 .effective_file_ops_depth()
195 .map_err(|e| format!("Invalid file_ops_depth configuration: {e}"))?;
196 let mut guard = sqry_core::query::security::RecursionGuard::new(file_ops_depth)
197 .map_err(|e| format!("Failed to create recursion guard: {e}"))?;
198
199 let mut walk_context = WalkContext {
200 content,
201 contexts: &mut contexts,
202 node_to_context: &mut node_to_context,
203 scope_stack: &mut scope_stack,
204 class_stack: &mut class_stack,
205 max_depth,
206 guard: &mut guard,
207 };
208
209 walk_ast(tree.root_node(), &mut walk_context)?;
210
211 Ok(Self {
212 contexts,
213 node_to_context,
214 })
215 }
216
217 fn contexts(&self) -> &[CallContext] {
218 &self.contexts
219 }
220
221 fn get_callable_context(&self, node_id: usize) -> Option<&CallContext> {
222 self.node_to_context
223 .get(&node_id)
224 .and_then(|idx| self.contexts.get(*idx))
225 }
226}
227
228#[allow(clippy::too_many_lines)] struct WalkContext<'a> {
233 content: &'a [u8],
234 contexts: &'a mut Vec<CallContext>,
235 node_to_context: &'a mut HashMap<usize, usize>,
236 scope_stack: &'a mut Vec<String>,
237 class_stack: &'a mut Vec<String>,
238 max_depth: usize,
239 guard: &'a mut sqry_core::query::security::RecursionGuard,
240}
241
242#[allow(clippy::too_many_lines)]
243fn walk_ast(node: Node, context: &mut WalkContext<'_>) -> Result<(), String> {
244 context
245 .guard
246 .enter()
247 .map_err(|e| format!("Recursion limit exceeded: {e}"))?;
248
249 if context.scope_stack.len() > context.max_depth {
250 context.guard.exit();
251 return Ok(());
252 }
253
254 match node.kind() {
255 "class_declaration" => {
256 let name_node = node
257 .child_by_field_name("name")
258 .ok_or_else(|| "class_declaration missing name".to_string())?;
259 let class_name = name_node
260 .utf8_text(context.content)
261 .map_err(|_| "failed to read class name".to_string())?;
262
263 let qualified_class = if context.scope_stack.is_empty() {
265 class_name.to_string()
266 } else {
267 format!("{}.{}", context.scope_stack.join("."), class_name)
268 };
269
270 context.class_stack.push(qualified_class.clone());
271 context.scope_stack.push(class_name.to_string());
272
273 let _context_idx = context.contexts.len();
275 context.contexts.push(CallContext {
276 qualified_name: qualified_class.clone(),
277 span: (node.start_byte(), node.end_byte()),
278 kind: ContextKind::Class,
279 class_name: Some(qualified_class),
280 return_type: None,
281 });
282
283 if let Some(body) = node.child_by_field_name("body") {
285 let mut cursor = body.walk();
286 for child in body.children(&mut cursor) {
287 walk_ast(child, context)?;
288 }
289 }
290
291 context.class_stack.pop();
292 context.scope_stack.pop();
293 }
294 "interface_declaration" => {
295 let name_node = node
296 .child_by_field_name("name")
297 .ok_or_else(|| "interface_declaration missing name".to_string())?;
298 let interface_name = name_node
299 .utf8_text(context.content)
300 .map_err(|_| "failed to read interface name".to_string())?;
301
302 let qualified_interface = if context.scope_stack.is_empty() {
304 interface_name.to_string()
305 } else {
306 format!("{}.{}", context.scope_stack.join("."), interface_name)
307 };
308
309 context.class_stack.push(qualified_interface.clone());
310 context.scope_stack.push(interface_name.to_string());
311
312 let _context_idx = context.contexts.len();
314 context.contexts.push(CallContext {
315 qualified_name: qualified_interface.clone(),
316 span: (node.start_byte(), node.end_byte()),
317 kind: ContextKind::Interface,
318 class_name: Some(qualified_interface),
319 return_type: None,
320 });
321
322 if let Some(body) = node.child_by_field_name("body") {
324 let mut cursor = body.walk();
325 for child in body.children(&mut cursor) {
326 walk_ast(child, context)?;
327 }
328 }
329
330 context.class_stack.pop();
331 context.scope_stack.pop();
332 }
333 "method_declaration" | "constructor_declaration" | "local_function_statement" => {
334 let name_node = node
335 .child_by_field_name("name")
336 .ok_or_else(|| format!("{} missing name", node.kind()).to_string())?;
337 let func_name = name_node
338 .utf8_text(context.content)
339 .map_err(|_| "failed to read function name".to_string())?;
340
341 let is_async = has_modifier(node, context.content, "async");
343
344 let is_static = has_modifier(node, context.content, "static");
346
347 let return_type = node
349 .child_by_field_name("return_type")
350 .or_else(|| node.child_by_field_name("returns"))
351 .or_else(|| node.child_by_field_name("type"))
352 .and_then(|type_node| type_node.utf8_text(context.content).ok())
353 .map(std::string::ToString::to_string);
354
355 let qualified_func = if context.scope_stack.is_empty() {
357 func_name.to_string()
358 } else {
359 format!("{}.{}", context.scope_stack.join("."), func_name)
360 };
361
362 let is_method = !context.class_stack.is_empty();
364 let class_name = context.class_stack.last().cloned();
365
366 let kind = if is_method {
367 ContextKind::Method {
368 is_async,
369 is_static,
370 }
371 } else {
372 ContextKind::Function { is_async }
373 };
374
375 let context_idx = context.contexts.len();
376 context.contexts.push(CallContext {
377 qualified_name: qualified_func.clone(),
378 span: (node.start_byte(), node.end_byte()),
379 kind,
380 class_name,
381 return_type,
382 });
383
384 if let Some(body) = node.child_by_field_name("body") {
386 associate_descendants(body, context_idx, context.node_to_context);
387 }
388
389 context.scope_stack.push(func_name.to_string());
390
391 if let Some(body) = node.child_by_field_name("body") {
393 let mut cursor = body.walk();
394 for child in body.children(&mut cursor) {
395 walk_ast(child, context)?;
396 }
397 }
398
399 context.scope_stack.pop();
400 }
401 "namespace_declaration" => {
402 if let Some(name_node) = node.child_by_field_name("name")
404 && let Ok(namespace_name) = name_node.utf8_text(context.content)
405 {
406 context.scope_stack.push(namespace_name.to_string());
407
408 if let Some(body) = node.child_by_field_name("body") {
410 let mut cursor = body.walk();
411 for child in body.children(&mut cursor) {
412 walk_ast(child, context)?;
413 }
414 }
415
416 context.scope_stack.pop();
417 }
418 }
419 _ => {
420 let mut cursor = node.walk();
422 for child in node.children(&mut cursor) {
423 walk_ast(child, context)?;
424 }
425 }
426 }
427
428 context.guard.exit();
429 Ok(())
430}
431
432fn associate_descendants(
433 node: Node,
434 context_idx: usize,
435 node_to_context: &mut HashMap<usize, usize>,
436) {
437 node_to_context.insert(node.id(), context_idx);
438
439 let mut stack = vec![node];
440 while let Some(current) = stack.pop() {
441 node_to_context.insert(current.id(), context_idx);
442
443 let mut cursor = current.walk();
444 for child in current.children(&mut cursor) {
445 stack.push(child);
446 }
447 }
448}
449
450#[allow(clippy::too_many_lines)] fn walk_tree_for_edges(
458 node: Node,
459 content: &[u8],
460 ast_graph: &ASTGraph,
461 helper: &mut GraphBuildHelper,
462 node_map: &mut HashMap<String, NodeId>,
463 namespace_stack: &mut Vec<String>,
464 class_stack: &mut Vec<String>,
465 scope_tree: &mut local_scopes::CSharpScopeTree,
466) -> GraphResult<()> {
467 match node.kind() {
468 "namespace_declaration" => {
469 if let Some(name_node) = node.child_by_field_name("name")
471 && let Ok(namespace_name) = name_node.utf8_text(content)
472 {
473 namespace_stack.push(namespace_name.to_string());
474
475 if let Some(body) = node.child_by_field_name("body") {
477 let mut cursor = body.walk();
478 for child in body.children(&mut cursor) {
479 walk_tree_for_edges(
480 child,
481 content,
482 ast_graph,
483 helper,
484 node_map,
485 namespace_stack,
486 class_stack,
487 scope_tree,
488 )?;
489 }
490 }
491
492 namespace_stack.pop();
493 return Ok(());
494 }
495 }
496 "class_declaration" => {
497 if let Some(name_node) = node.child_by_field_name("name")
499 && let Ok(class_name) = name_node.utf8_text(content)
500 {
501 let qualified_class =
503 build_qualified_name(namespace_stack, class_stack, class_name);
504 class_stack.push(class_name.to_string());
505
506 process_class_declaration(
508 node,
509 content,
510 helper,
511 node_map,
512 &qualified_class,
513 namespace_stack,
514 );
515
516 if should_export(node, content)
518 && let Some(class_id) = node_map.get(&qualified_class)
519 {
520 export_from_file_module(helper, *class_id);
521 }
522
523 if let Some(body) = node.child_by_field_name("body") {
525 process_class_member_exports(body, content, &qualified_class, helper, node_map);
526
527 let mut cursor = body.walk();
528 for child in body.children(&mut cursor) {
529 walk_tree_for_edges(
530 child,
531 content,
532 ast_graph,
533 helper,
534 node_map,
535 namespace_stack,
536 class_stack,
537 scope_tree,
538 )?;
539 }
540 }
541
542 class_stack.pop();
543 return Ok(());
544 }
545 }
546 "interface_declaration" => {
547 if let Some(name_node) = node.child_by_field_name("name")
549 && let Ok(interface_name) = name_node.utf8_text(content)
550 {
551 let qualified_interface =
553 build_qualified_name(namespace_stack, class_stack, interface_name);
554
555 process_interface_declaration(
557 node,
558 content,
559 helper,
560 node_map,
561 &qualified_interface,
562 namespace_stack,
563 );
564
565 if should_export(node, content)
567 && let Some(interface_id) = node_map.get(&qualified_interface)
568 {
569 export_from_file_module(helper, *interface_id);
570 }
571
572 if let Some(body) = node.child_by_field_name("body") {
574 process_interface_member_exports(
575 body,
576 content,
577 &qualified_interface,
578 helper,
579 node_map,
580 );
581 }
582
583 return Ok(());
585 }
586 }
587 "invocation_expression" => {
588 process_invocation(node, content, ast_graph, helper, node_map);
589 }
590 "object_creation_expression" => {
591 process_object_creation(node, content, ast_graph, helper, node_map);
592 }
593 "using_directive" => {
594 process_using_directive(node, content, helper);
595 }
596 "method_declaration" => {
597 process_pinvoke_method(node, content, helper, node_map, namespace_stack);
599
600 if let Some(name_node) = node.child_by_field_name("name")
602 && let Ok(method_name) = name_node.utf8_text(content)
603 {
604 let mut scope_parts = namespace_stack.clone();
607 scope_parts.extend(class_stack.iter().cloned());
608
609 let qualified_name = if scope_parts.is_empty() {
610 method_name.to_string()
611 } else {
612 format!("{}.{}", scope_parts.join("."), method_name)
613 };
614
615 process_method_parameters(node, &qualified_name, content, helper);
616 process_method_return_type(node, &qualified_name, content, helper);
617 }
618 }
619 "local_declaration_statement" => {
620 process_local_variables(node, content, helper, class_stack);
622 }
623 "field_declaration" => {
624 process_field_declaration(node, content, helper, class_stack);
626 }
627 "property_declaration" => {
628 process_property_declaration(node, content, helper, class_stack);
630 }
631 "identifier" => {
632 local_scopes::handle_identifier_for_reference(node, content, scope_tree, helper);
633 }
634 _ => {}
635 }
636
637 let mut cursor = node.walk();
639 for child in node.children(&mut cursor) {
640 walk_tree_for_edges(
641 child,
642 content,
643 ast_graph,
644 helper,
645 node_map,
646 namespace_stack,
647 class_stack,
648 scope_tree,
649 )?;
650 }
651
652 Ok(())
653}
654
655fn build_qualified_name(namespace_stack: &[String], class_stack: &[String], name: &str) -> String {
657 let mut parts = Vec::new();
658 parts.extend(namespace_stack.iter().cloned());
659 parts.extend(class_stack.iter().cloned());
660 parts.push(name.to_string());
661 parts.join(".")
662}
663
664fn qualify_type_name(type_name: &str, namespace_stack: &[String]) -> String {
671 if type_name.contains('.') {
673 return type_name.to_string();
674 }
675
676 if namespace_stack.is_empty() {
678 return type_name.to_string();
679 }
680
681 format!("{}.{}", namespace_stack.join("."), type_name)
683}
684
685fn process_invocation(
686 node: Node,
687 content: &[u8],
688 ast_graph: &ASTGraph,
689 helper: &mut GraphBuildHelper,
690 node_map: &mut HashMap<String, NodeId>,
691) {
692 let Some(function_node) = node.child_by_field_name("function") else {
693 return;
694 };
695
696 let Ok(callee_text) = function_node.utf8_text(content) else {
697 return;
698 };
699
700 let Some(call_context) = ast_graph.get_callable_context(node.id()) else {
702 return;
703 };
704
705 let callee_qualified = if callee_text.contains('.') {
710 callee_text.to_string()
712 } else if let Some(class_name) = &call_context.class_name {
713 format!("{class_name}.{callee_text}")
715 } else {
716 callee_text.to_string()
717 };
718
719 let caller_function_id = *node_map
721 .entry(call_context.qualified_name.clone())
722 .or_insert_with(|| helper.add_function(&call_context.qualified_name, None, false, false));
723
724 let target_function_id = *node_map
726 .entry(callee_qualified.clone())
727 .or_insert_with(|| helper.add_function(&callee_qualified, None, false, false));
728
729 let argument_count = count_call_arguments(node);
730 let call_span = Span::from_bytes(node.start_byte(), node.end_byte());
731 helper.add_call_edge_full_with_span(
732 caller_function_id,
733 target_function_id,
734 argument_count,
735 false,
736 vec![call_span],
737 );
738}
739
740fn process_object_creation(
741 node: Node,
742 content: &[u8],
743 ast_graph: &ASTGraph,
744 helper: &mut GraphBuildHelper,
745 node_map: &mut HashMap<String, NodeId>,
746) {
747 let Some(type_node) = node.child_by_field_name("type") else {
748 return;
749 };
750
751 let Ok(type_name) = type_node.utf8_text(content) else {
752 return;
753 };
754
755 let Some(call_context) = ast_graph.get_callable_context(node.id()) else {
757 return;
758 };
759
760 let callee_qualified = format!("{type_name}.ctor");
762
763 let caller_function_id = *node_map
765 .entry(call_context.qualified_name.clone())
766 .or_insert_with(|| helper.add_function(&call_context.qualified_name, None, false, false));
767
768 let target_function_id = *node_map
770 .entry(callee_qualified.clone())
771 .or_insert_with(|| helper.add_method(&callee_qualified, None, false, false));
772
773 let argument_count = count_call_arguments(node);
774 let call_span = Span::from_bytes(node.start_byte(), node.end_byte());
775 helper.add_call_edge_full_with_span(
776 caller_function_id,
777 target_function_id,
778 argument_count,
779 false,
780 vec![call_span],
781 );
782}
783
784fn count_call_arguments(call_node: Node<'_>) -> u8 {
785 let args_node = call_node
786 .child_by_field_name("arguments")
787 .or_else(|| call_node.child_by_field_name("argument_list"))
788 .or_else(|| {
789 let mut cursor = call_node.walk();
790 call_node
791 .children(&mut cursor)
792 .find(|child| child.kind() == "argument_list")
793 });
794
795 let Some(args_node) = args_node else {
796 return 255;
797 };
798
799 let count = args_node.named_child_count();
800 if count <= 254 {
801 u8::try_from(count).unwrap_or(u8::MAX)
802 } else {
803 u8::MAX
804 }
805}
806
807fn process_using_directive(node: Node, content: &[u8], helper: &mut GraphBuildHelper) {
861 let is_static = node
863 .children(&mut node.walk())
864 .any(|child| child.kind() == "static");
865
866 let has_equals = node
869 .children(&mut node.walk())
870 .any(|child| child.kind() == "=");
871
872 let (alias, imported_name) = if has_equals {
874 extract_aliased_using(node, content)
876 } else {
877 (None, extract_simple_using_target(node, content))
879 };
880
881 let Some(imported_name) = imported_name else {
882 return;
883 };
884
885 let module_id = helper.add_module("<file>", None);
887
888 let span = Span::from_bytes(node.start_byte(), node.end_byte());
890 let import_name = if is_static {
891 format!("static {imported_name}")
892 } else {
893 imported_name.clone()
894 };
895 let imported_id = helper.add_import(&import_name, Some(span));
896
897 match (alias.as_deref(), is_static) {
901 (Some(alias_str), _) => {
902 helper.add_import_edge_full(module_id, imported_id, Some(alias_str), false);
904 }
905 (None, true) => {
906 helper.add_import_edge_full(module_id, imported_id, None, true);
909 }
910 (None, false) => {
911 helper.add_import_edge(module_id, imported_id);
913 }
914 }
915}
916
917fn extract_aliased_using(node: Node, content: &[u8]) -> (Option<String>, Option<String>) {
923 let mut alias: Option<String> = None;
924 let mut target: Option<String> = None;
925 let mut past_equals = false;
926
927 let mut cursor = node.walk();
928 for child in node.children(&mut cursor) {
929 let kind = child.kind();
930
931 if kind == "=" {
932 past_equals = true;
933 continue;
934 }
935
936 if kind == "using" || kind == "static" || kind == ";" {
938 continue;
939 }
940
941 if past_equals {
942 if matches!(kind, "identifier" | "qualified_name") && target.is_none() {
944 target = child
945 .utf8_text(content)
946 .ok()
947 .map(std::string::ToString::to_string);
948 }
949 } else if kind == "identifier" && alias.is_none() {
950 alias = child
952 .utf8_text(content)
953 .ok()
954 .map(std::string::ToString::to_string);
955 }
956 }
957
958 (alias, target)
959}
960
961fn extract_simple_using_target(node: Node, content: &[u8]) -> Option<String> {
966 let mut cursor = node.walk();
967
968 for child in node.children(&mut cursor) {
969 let kind = child.kind();
970
971 if kind == "using" || kind == "static" || kind == ";" || kind == "=" {
973 continue;
974 }
975
976 if matches!(kind, "identifier" | "identifier_name" | "qualified_name") {
978 return child
979 .utf8_text(content)
980 .ok()
981 .map(std::string::ToString::to_string);
982 }
983 }
984
985 node.child_by_field_name("name")
987 .and_then(|n| n.utf8_text(content).ok())
988 .map(std::string::ToString::to_string)
989}
990
991fn process_class_declaration(
1013 node: Node,
1014 content: &[u8],
1015 helper: &mut GraphBuildHelper,
1016 node_map: &mut HashMap<String, NodeId>,
1017 qualified_class_name: &str,
1018 namespace_stack: &[String],
1019) {
1020 let class_id = *node_map
1022 .entry(qualified_class_name.to_string())
1023 .or_insert_with(|| helper.add_class(qualified_class_name, None));
1024
1025 let mut cursor = node.walk();
1027 let base_list = node
1028 .children(&mut cursor)
1029 .find(|child| child.kind() == "base_list");
1030
1031 let Some(base_list) = base_list else {
1032 return;
1033 };
1034
1035 let mut first_base_class = true;
1037
1038 let mut base_cursor = base_list.walk();
1040 for base_child in base_list.children(&mut base_cursor) {
1041 let base_type_name = match base_child.kind() {
1042 "identifier" | "identifier_name" | "type_identifier" | "qualified_name" => base_child
1043 .utf8_text(content)
1044 .ok()
1045 .map(std::string::ToString::to_string),
1046 "generic_name" => {
1047 base_child
1049 .child_by_field_name("name")
1050 .or_else(|| base_child.child(0))
1051 .and_then(|n| n.utf8_text(content).ok())
1052 .map(std::string::ToString::to_string)
1053 }
1054 _ => None,
1055 };
1056
1057 let Some(base_name) = base_type_name else {
1058 continue;
1059 };
1060
1061 let qualified_base_name = qualify_type_name(&base_name, namespace_stack);
1064
1065 let is_interface = is_interface_name(&base_name);
1069
1070 if is_interface {
1071 let interface_id = *node_map
1073 .entry(qualified_base_name.clone())
1074 .or_insert_with(|| helper.add_interface(&qualified_base_name, None));
1075 helper.add_implements_edge(class_id, interface_id);
1076 } else if first_base_class {
1077 let parent_id = *node_map
1079 .entry(qualified_base_name.clone())
1080 .or_insert_with(|| helper.add_class(&qualified_base_name, None));
1081 helper.add_inherits_edge(class_id, parent_id);
1082 first_base_class = false;
1083 }
1084 }
1086}
1087
1088fn process_interface_declaration(
1105 node: Node,
1106 content: &[u8],
1107 helper: &mut GraphBuildHelper,
1108 node_map: &mut HashMap<String, NodeId>,
1109 qualified_interface_name: &str,
1110 namespace_stack: &[String],
1111) {
1112 let interface_id = *node_map
1114 .entry(qualified_interface_name.to_string())
1115 .or_insert_with(|| helper.add_interface(qualified_interface_name, None));
1116
1117 let mut cursor = node.walk();
1119 let base_list = node
1120 .children(&mut cursor)
1121 .find(|child| child.kind() == "base_list");
1122
1123 let Some(base_list) = base_list else {
1124 return;
1125 };
1126
1127 let mut base_cursor = base_list.walk();
1129 for base_child in base_list.children(&mut base_cursor) {
1130 let parent_name = match base_child.kind() {
1131 "identifier" | "identifier_name" | "type_identifier" | "qualified_name" => base_child
1132 .utf8_text(content)
1133 .ok()
1134 .map(std::string::ToString::to_string),
1135 "generic_name" => base_child
1136 .child_by_field_name("name")
1137 .or_else(|| base_child.child(0))
1138 .and_then(|n| n.utf8_text(content).ok())
1139 .map(std::string::ToString::to_string),
1140 _ => None,
1141 };
1142
1143 let Some(parent_name) = parent_name else {
1144 continue;
1145 };
1146
1147 let qualified_parent_name = qualify_type_name(&parent_name, namespace_stack);
1149
1150 let parent_id = *node_map
1152 .entry(qualified_parent_name.clone())
1153 .or_insert_with(|| helper.add_interface(&qualified_parent_name, None));
1154 helper.add_inherits_edge(interface_id, parent_id);
1155 }
1156}
1157
1158fn is_interface_name(name: &str) -> bool {
1164 let chars: Vec<char> = name.chars().collect();
1165 if chars.len() >= 2 {
1166 chars[0] == 'I' && chars[1].is_ascii_uppercase()
1168 } else {
1169 false
1170 }
1171}
1172
1173fn is_public(node: Node, content: &[u8]) -> bool {
1179 has_visibility_modifier(node, content, "public")
1180}
1181
1182fn is_internal(node: Node, content: &[u8]) -> bool {
1185 has_visibility_modifier(node, content, "internal")
1186}
1187
1188fn is_private(node: Node, content: &[u8]) -> bool {
1190 has_visibility_modifier(node, content, "private")
1191}
1192
1193#[allow(dead_code)] fn is_protected(node: Node, content: &[u8]) -> bool {
1196 has_visibility_modifier(node, content, "protected")
1197}
1198
1199fn has_visibility_modifier(node: Node, content: &[u8], modifier: &str) -> bool {
1201 node.children(&mut node.walk())
1202 .any(|child| child.kind() == modifier || child.utf8_text(content).unwrap_or("") == modifier)
1203}
1204
1205fn has_modifier(node: Node, content: &[u8], modifier: &str) -> bool {
1206 node.children(&mut node.walk())
1207 .any(|child| child.kind() == modifier || child.utf8_text(content).unwrap_or("") == modifier)
1208}
1209
1210fn should_export(node: Node, content: &[u8]) -> bool {
1216 is_public(node, content) || is_internal(node, content)
1217}
1218
1219fn export_from_file_module(helper: &mut GraphBuildHelper, exported: NodeId) {
1221 let module_id = helper.add_module(FILE_MODULE_NAME, None);
1222 helper.add_export_edge(module_id, exported);
1223}
1224
1225fn process_class_member_exports(
1227 body_node: Node,
1228 content: &[u8],
1229 class_qualified_name: &str,
1230 helper: &mut GraphBuildHelper,
1231 node_map: &mut HashMap<String, NodeId>,
1232) {
1233 let mut cursor = body_node.walk();
1234 for child in body_node.children(&mut cursor) {
1235 match child.kind() {
1236 "method_declaration" | "constructor_declaration" => {
1237 if should_export(child, content)
1239 && let Some(name_node) = child.child_by_field_name("name")
1240 && let Ok(method_name) = name_node.utf8_text(content)
1241 {
1242 let qualified_name = format!("{class_qualified_name}.{method_name}");
1243 if let Some(method_id) = node_map.get(&qualified_name) {
1245 export_from_file_module(helper, *method_id);
1246 }
1247 } else if should_export(child, content) && child.kind() == "constructor_declaration"
1248 {
1249 let class_name = class_qualified_name
1251 .rsplit('.')
1252 .next()
1253 .unwrap_or(class_qualified_name);
1254 let qualified_name = format!("{class_qualified_name}.{class_name}");
1255 if let Some(method_id) = node_map.get(&qualified_name) {
1256 export_from_file_module(helper, *method_id);
1257 }
1258 }
1259 }
1260 "field_declaration" | "property_declaration" => {
1261 if should_export(child, content) {
1263 let mut field_cursor = child.walk();
1265 for field_child in child.children(&mut field_cursor) {
1266 if field_child.kind() == "variable_declarator"
1267 && let Some(name_node) = field_child.child_by_field_name("name")
1268 && let Ok(field_name) = name_node.utf8_text(content)
1269 {
1270 let qualified_name = format!("{class_qualified_name}.{field_name}");
1271 let span =
1272 Span::from_bytes(field_child.start_byte(), field_child.end_byte());
1273
1274 let field_id = helper.add_variable(&qualified_name, Some(span));
1276 export_from_file_module(helper, field_id);
1277 } else if field_child.kind() == "identifier"
1278 && let Ok(prop_name) = field_child.utf8_text(content)
1279 {
1280 let qualified_name = format!("{class_qualified_name}.{prop_name}");
1282 let span = Span::from_bytes(child.start_byte(), child.end_byte());
1283
1284 let prop_id = helper.add_variable(&qualified_name, Some(span));
1286 export_from_file_module(helper, prop_id);
1287 }
1288 }
1289 }
1290 }
1291 _ => {}
1292 }
1293 }
1294}
1295
1296fn process_interface_member_exports(
1299 body_node: Node,
1300 content: &[u8],
1301 interface_qualified_name: &str,
1302 helper: &mut GraphBuildHelper,
1303 node_map: &mut HashMap<String, NodeId>,
1304) {
1305 let mut cursor = body_node.walk();
1306 for child in body_node.children(&mut cursor) {
1307 if child.kind() == "method_declaration"
1308 && !is_private(child, content)
1309 && let Some(name_node) = child.child_by_field_name("name")
1310 && let Ok(method_name) = name_node.utf8_text(content)
1311 {
1312 let qualified_name = format!("{interface_qualified_name}.{method_name}");
1314 if let Some(method_id) = node_map.get(&qualified_name) {
1316 export_from_file_module(helper, *method_id);
1317 }
1318 }
1319 }
1320}
1321
1322fn process_pinvoke_method(
1353 node: Node,
1354 content: &[u8],
1355 helper: &mut GraphBuildHelper,
1356 node_map: &mut HashMap<String, NodeId>,
1357 namespace_stack: &[String],
1358) {
1359 let has_extern = node
1361 .children(&mut node.walk())
1362 .any(|child| child.kind() == "extern");
1363
1364 if !has_extern {
1365 return;
1366 }
1367
1368 let mut cursor = node.walk();
1370 let attribute_list = node
1371 .children(&mut cursor)
1372 .find(|child| child.kind() == "attribute_list");
1373
1374 let Some(attribute_list) = attribute_list else {
1375 return;
1376 };
1377
1378 let (dll_name, calling_convention) = extract_dllimport_info(attribute_list, content);
1380
1381 let Some(dll_name) = dll_name else {
1382 return;
1383 };
1384
1385 let method_name = node
1387 .child_by_field_name("name")
1388 .and_then(|n| n.utf8_text(content).ok())
1389 .map(std::string::ToString::to_string);
1390
1391 let Some(method_name) = method_name else {
1392 return;
1393 };
1394
1395 let qualified_method = if namespace_stack.is_empty() {
1397 method_name.clone()
1398 } else {
1399 format!("{}.{}", namespace_stack.join("."), method_name)
1400 };
1401
1402 let method_span = Span::from_bytes(node.start_byte(), node.end_byte());
1404 let method_id = *node_map
1405 .entry(qualified_method.clone())
1406 .or_insert_with(|| helper.add_method(&qualified_method, Some(method_span), false, true));
1407
1408 let ffi_func_name = format!("ffi::{dll_name}::{method_name}");
1410 let ffi_func_id = *node_map
1411 .entry(ffi_func_name.clone())
1412 .or_insert_with(|| helper.add_function(&ffi_func_name, None, false, false));
1413
1414 let convention = match calling_convention.as_deref() {
1416 Some("CallingConvention.Cdecl" | "Cdecl") => FfiConvention::Cdecl,
1417 Some("CallingConvention.FastCall" | "FastCall") => FfiConvention::Fastcall,
1418 _ => FfiConvention::Stdcall,
1420 };
1421
1422 helper.add_ffi_edge(method_id, ffi_func_id, convention);
1424}
1425
1426fn extract_dllimport_info(
1430 attribute_list: Node,
1431 content: &[u8],
1432) -> (Option<String>, Option<String>) {
1433 let mut dll_name = None;
1434 let mut calling_convention = None;
1435
1436 let mut list_cursor = attribute_list.walk();
1437 for attr_child in attribute_list.children(&mut list_cursor) {
1438 if attr_child.kind() != "attribute" {
1439 continue;
1440 }
1441
1442 let attr_name = attr_child
1444 .child_by_field_name("name")
1445 .and_then(|n| n.utf8_text(content).ok());
1446
1447 let is_dllimport = attr_name.is_some_and(|name| {
1448 name == "DllImport" || name == "System.Runtime.InteropServices.DllImport"
1449 });
1450
1451 if !is_dllimport {
1452 continue;
1453 }
1454
1455 let mut attr_cursor = attr_child.walk();
1457 let arg_list = attr_child
1458 .children(&mut attr_cursor)
1459 .find(|child| child.kind() == "attribute_argument_list");
1460
1461 let Some(arg_list) = arg_list else {
1462 continue;
1463 };
1464
1465 let mut arg_cursor = arg_list.walk();
1466 for arg in arg_list.children(&mut arg_cursor) {
1467 if arg.kind() != "attribute_argument" {
1468 continue;
1469 }
1470
1471 if let Some(name_node) = arg.child_by_field_name("name")
1473 && let Ok(name) = name_node.utf8_text(content)
1474 {
1475 if name == "CallingConvention"
1476 && let Some(expr) = arg.child_by_field_name("expression")
1477 && let Ok(value) = expr.utf8_text(content)
1478 {
1479 calling_convention = Some(value.to_string());
1480 }
1481 continue;
1482 }
1483
1484 if dll_name.is_none() {
1486 let expr = arg.child_by_field_name("expression").or_else(|| {
1488 let mut c = arg.walk();
1490 arg.children(&mut c)
1491 .find(|child| child.kind() == "string_literal")
1492 });
1493
1494 if let Some(expr) = expr
1495 && let Ok(text) = expr.utf8_text(content)
1496 {
1497 let trimmed = text.trim();
1499 if (trimmed.starts_with('"') && trimmed.ends_with('"'))
1500 || (trimmed.starts_with('@') && trimmed.len() > 2)
1501 {
1502 let start = if trimmed.starts_with('@') { 2 } else { 1 };
1503 dll_name = Some(trimmed[start..trimmed.len() - 1].to_string());
1504 } else {
1505 dll_name = Some(trimmed.to_string());
1506 }
1507 }
1508 }
1509 }
1510 }
1511
1512 (dll_name, calling_convention)
1513}
1514
1515fn process_local_variables(
1539 node: Node,
1540 content: &[u8],
1541 helper: &mut GraphBuildHelper,
1542 _class_stack: &[String],
1543) {
1544 let mut cursor = node.walk();
1546 let var_decl = node
1547 .children(&mut cursor)
1548 .find(|child| child.kind() == "variable_declaration");
1549
1550 let Some(var_decl) = var_decl else {
1551 return;
1552 };
1553
1554 let type_node = var_decl.child_by_field_name("type");
1556 let Some(type_node) = type_node else {
1557 return;
1558 };
1559
1560 let type_text = extract_type_string(type_node, content);
1562 let Some(type_text) = type_text else {
1563 return;
1564 };
1565
1566 let all_type_names = extract_all_type_names_from_annotation(type_node, content);
1568
1569 let mut var_cursor = var_decl.walk();
1571 for child in var_decl.children(&mut var_cursor) {
1572 if child.kind() == "variable_declarator"
1573 && let Some(name_node) = child.child_by_field_name("name")
1574 && let Ok(var_name) = name_node.utf8_text(content)
1575 {
1576 let span = Span::from_bytes(child.start_byte(), child.end_byte());
1577
1578 let var_id = helper.add_variable(var_name, Some(span));
1580
1581 let type_id = helper.add_type(&type_text, None);
1583 helper.add_typeof_edge_with_context(
1584 var_id,
1585 type_id,
1586 Some(TypeOfContext::Variable),
1587 None,
1588 Some(var_name),
1589 );
1590
1591 for type_name in &all_type_names {
1593 let ref_type_id = helper.add_type(type_name, None);
1594 helper.add_reference_edge(var_id, ref_type_id);
1595 }
1596 }
1597 }
1598}
1599
1600fn process_field_declaration(
1618 node: Node,
1619 content: &[u8],
1620 helper: &mut GraphBuildHelper,
1621 class_stack: &[String],
1622) {
1623 let decl_node = node
1625 .children(&mut node.walk())
1626 .find(|child| child.kind() == "variable_declaration");
1627
1628 let Some(decl_node) = decl_node else {
1629 return;
1630 };
1631
1632 let type_node = decl_node.child_by_field_name("type");
1634 let Some(type_node) = type_node else {
1635 return;
1636 };
1637
1638 let type_text = extract_type_string(type_node, content);
1640 let Some(type_text) = type_text else {
1641 return;
1642 };
1643
1644 let all_type_names = extract_all_type_names_from_annotation(type_node, content);
1646
1647 let class_name = class_stack.last().map_or("", String::as_str);
1649
1650 let mut var_cursor = decl_node.walk();
1652 for child in decl_node.children(&mut var_cursor) {
1653 if child.kind() == "variable_declarator"
1654 && let Some(name_node) = child.child_by_field_name("name")
1655 && let Ok(field_name) = name_node.utf8_text(content)
1656 {
1657 let qualified_name = if class_name.is_empty() {
1659 field_name.to_string()
1660 } else {
1661 format!("{class_name}.{field_name}")
1662 };
1663
1664 let span = Span::from_bytes(child.start_byte(), child.end_byte());
1665
1666 let field_id = helper.add_variable(&qualified_name, Some(span));
1668
1669 let type_id = helper.add_type(&type_text, None);
1671 helper.add_typeof_edge_with_context(
1672 field_id,
1673 type_id,
1674 Some(TypeOfContext::Field),
1675 None,
1676 Some(&qualified_name),
1677 );
1678
1679 for type_name in &all_type_names {
1681 let ref_type_id = helper.add_type(type_name, None);
1682 helper.add_reference_edge(field_id, ref_type_id);
1683 }
1684 }
1685 }
1686}
1687
1688fn process_property_declaration(
1705 node: Node,
1706 content: &[u8],
1707 helper: &mut GraphBuildHelper,
1708 class_stack: &[String],
1709) {
1710 let type_node = node.child_by_field_name("type");
1712 let Some(type_node) = type_node else {
1713 return;
1714 };
1715
1716 let type_text = extract_type_string(type_node, content);
1718 let Some(type_text) = type_text else {
1719 return;
1720 };
1721
1722 let all_type_names = extract_all_type_names_from_annotation(type_node, content);
1724
1725 let name_node = node.child_by_field_name("name");
1727 let Some(name_node) = name_node else {
1728 return;
1729 };
1730
1731 let Ok(prop_name) = name_node.utf8_text(content) else {
1732 return;
1733 };
1734
1735 let class_name = class_stack.last().map_or("", String::as_str);
1737
1738 let qualified_name = if class_name.is_empty() {
1740 prop_name.to_string()
1741 } else {
1742 format!("{class_name}.{prop_name}")
1743 };
1744
1745 let span = Span::from_bytes(node.start_byte(), node.end_byte());
1746
1747 let prop_id = helper.add_variable(&qualified_name, Some(span));
1749
1750 let type_id = helper.add_type(&type_text, None);
1753 helper.add_typeof_edge_with_context(
1754 prop_id,
1755 type_id,
1756 Some(TypeOfContext::Field),
1757 None,
1758 Some(&qualified_name),
1759 );
1760
1761 for type_name in &all_type_names {
1763 let ref_type_id = helper.add_type(type_name, None);
1764 helper.add_reference_edge(prop_id, ref_type_id);
1765 }
1766}
1767fn process_method_parameters(
1784 node: Node,
1785 _method_name: &str,
1786 content: &[u8],
1787 helper: &mut GraphBuildHelper,
1788) {
1789 let Some(param_list) = node.child_by_field_name("parameter_list") else {
1791 return;
1792 };
1793
1794 let mut cursor = param_list.walk();
1796 let mut param_index: u16 = 0;
1797
1798 for child in param_list.children(&mut cursor) {
1800 if !child.is_named() {
1802 continue;
1803 }
1804 {
1806 let Some(name_node) = child.child_by_field_name("name") else {
1808 continue;
1809 };
1810 let Ok(param_name) = name_node.utf8_text(content) else {
1811 continue;
1812 };
1813
1814 let Some(type_node) = child.child_by_field_name("type") else {
1816 continue;
1817 };
1818
1819 let Some(type_text) = extract_type_string(type_node, content) else {
1821 continue;
1822 };
1823
1824 let all_type_names = extract_all_type_names_from_annotation(type_node, content);
1826
1827 let param_span = Span::from_bytes(child.start_byte(), child.end_byte());
1829 let param_id = helper.add_variable(param_name, Some(param_span));
1830
1831 let type_id = helper.add_type(&type_text, None);
1833 helper.add_typeof_edge_with_context(
1834 param_id,
1835 type_id,
1836 Some(TypeOfContext::Parameter),
1837 Some(param_index),
1838 Some(param_name),
1839 );
1840
1841 for type_name in &all_type_names {
1843 let ref_type_id = helper.add_type(type_name, None);
1844 helper.add_reference_edge(param_id, ref_type_id);
1845 }
1846
1847 param_index += 1;
1848 }
1849 }
1850}
1851
1852fn process_method_return_type(
1867 node: Node,
1868 method_name: &str,
1869 content: &[u8],
1870 helper: &mut GraphBuildHelper,
1871) {
1872 let return_type_node = node
1874 .child_by_field_name("return_type")
1875 .or_else(|| node.child_by_field_name("returns"))
1876 .or_else(|| node.child_by_field_name("type"));
1877
1878 let Some(return_type_node) = return_type_node else {
1879 return;
1880 };
1881
1882 if let Ok(type_text) = return_type_node.utf8_text(content)
1884 && type_text.trim() == "void"
1885 {
1886 return;
1887 }
1888
1889 let Some(type_text) = extract_type_string(return_type_node, content) else {
1891 return;
1892 };
1893
1894 let all_type_names = extract_all_type_names_from_annotation(return_type_node, content);
1896
1897 let method_span = Span::from_bytes(node.start_byte(), node.end_byte());
1899 let method_id = helper.add_method(method_name, Some(method_span), false, false);
1900
1901 let type_id = helper.add_type(&type_text, None);
1903 helper.add_typeof_edge_with_context(
1904 method_id,
1905 type_id,
1906 Some(TypeOfContext::Return),
1907 Some(0), Some(method_name),
1909 );
1910
1911 for type_name in &all_type_names {
1913 let ref_type_id = helper.add_type(type_name, None);
1914 helper.add_reference_edge(method_id, ref_type_id);
1915 }
1916}