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 process_type_parameter_declarations(node, content, &qualified_class, helper);
520
521 if should_export(node, content)
523 && let Some(class_id) = node_map.get(&qualified_class)
524 {
525 export_from_file_module(helper, *class_id);
526 }
527
528 if let Some(body) = node.child_by_field_name("body") {
530 process_class_member_exports(body, content, &qualified_class, helper, node_map);
531
532 let mut cursor = body.walk();
533 for child in body.children(&mut cursor) {
534 walk_tree_for_edges(
535 child,
536 content,
537 ast_graph,
538 helper,
539 node_map,
540 namespace_stack,
541 class_stack,
542 scope_tree,
543 )?;
544 }
545 }
546
547 class_stack.pop();
548 return Ok(());
549 }
550 }
551 "interface_declaration" => {
552 if let Some(name_node) = node.child_by_field_name("name")
554 && let Ok(interface_name) = name_node.utf8_text(content)
555 {
556 let qualified_interface =
558 build_qualified_name(namespace_stack, class_stack, interface_name);
559
560 process_interface_declaration(
562 node,
563 content,
564 helper,
565 node_map,
566 &qualified_interface,
567 namespace_stack,
568 );
569
570 process_type_parameter_declarations(node, content, &qualified_interface, helper);
574
575 if should_export(node, content)
577 && let Some(interface_id) = node_map.get(&qualified_interface)
578 {
579 export_from_file_module(helper, *interface_id);
580 }
581
582 if let Some(body) = node.child_by_field_name("body") {
592 process_interface_member_exports(
593 body,
594 content,
595 &qualified_interface,
596 helper,
597 node_map,
598 );
599
600 class_stack.push(interface_name.to_string());
601 let mut cursor = body.walk();
602 for child in body.children(&mut cursor) {
603 walk_tree_for_edges(
604 child,
605 content,
606 ast_graph,
607 helper,
608 node_map,
609 namespace_stack,
610 class_stack,
611 scope_tree,
612 )?;
613 }
614 class_stack.pop();
615 }
616
617 return Ok(());
618 }
619 }
620 "invocation_expression" => {
621 process_invocation(node, content, ast_graph, helper, node_map);
622 }
623 "object_creation_expression" => {
624 process_object_creation(node, content, ast_graph, helper, node_map);
625 }
626 "using_directive" => {
627 process_using_directive(node, content, helper);
628 }
629 "method_declaration" => {
630 process_pinvoke_method(node, content, helper, node_map, namespace_stack);
632
633 if let Some(name_node) = node.child_by_field_name("name")
635 && let Ok(method_name) = name_node.utf8_text(content)
636 {
637 let mut scope_parts = namespace_stack.clone();
640 scope_parts.extend(class_stack.iter().cloned());
641
642 let qualified_name = if scope_parts.is_empty() {
643 method_name.to_string()
644 } else {
645 format!("{}.{}", scope_parts.join("."), method_name)
646 };
647
648 process_method_parameters(node, &qualified_name, content, helper);
649 process_method_return_type(node, &qualified_name, content, helper);
650
651 process_type_parameter_declarations(node, content, &qualified_name, helper);
655 }
656 }
657 "local_declaration_statement" => {
658 process_local_variables(node, content, helper, class_stack);
660 }
661 "field_declaration" => {
662 process_field_declaration(node, content, helper, class_stack);
664 }
665 "property_declaration" => {
666 process_property_declaration(node, content, helper, class_stack);
668 }
669 "identifier" => {
670 local_scopes::handle_identifier_for_reference(node, content, scope_tree, helper);
671 }
672 _ => {}
673 }
674
675 let mut cursor = node.walk();
677 for child in node.children(&mut cursor) {
678 walk_tree_for_edges(
679 child,
680 content,
681 ast_graph,
682 helper,
683 node_map,
684 namespace_stack,
685 class_stack,
686 scope_tree,
687 )?;
688 }
689
690 Ok(())
691}
692
693fn build_qualified_name(namespace_stack: &[String], class_stack: &[String], name: &str) -> String {
695 let mut parts = Vec::new();
696 parts.extend(namespace_stack.iter().cloned());
697 parts.extend(class_stack.iter().cloned());
698 parts.push(name.to_string());
699 parts.join(".")
700}
701
702fn qualify_type_name(type_name: &str, namespace_stack: &[String]) -> String {
709 if type_name.contains('.') {
711 return type_name.to_string();
712 }
713
714 if namespace_stack.is_empty() {
716 return type_name.to_string();
717 }
718
719 format!("{}.{}", namespace_stack.join("."), type_name)
721}
722
723fn process_invocation(
724 node: Node,
725 content: &[u8],
726 ast_graph: &ASTGraph,
727 helper: &mut GraphBuildHelper,
728 node_map: &mut HashMap<String, NodeId>,
729) {
730 let Some(function_node) = node.child_by_field_name("function") else {
731 return;
732 };
733
734 let Ok(callee_text) = function_node.utf8_text(content) else {
735 return;
736 };
737
738 let Some(call_context) = ast_graph.get_callable_context(node.id()) else {
740 return;
741 };
742
743 let callee_qualified = if callee_text.contains('.') {
748 callee_text.to_string()
750 } else if let Some(class_name) = &call_context.class_name {
751 format!("{class_name}.{callee_text}")
753 } else {
754 callee_text.to_string()
755 };
756
757 let caller_function_id = *node_map
759 .entry(call_context.qualified_name.clone())
760 .or_insert_with(|| helper.add_function(&call_context.qualified_name, None, false, false));
761
762 let target_function_id = *node_map
764 .entry(callee_qualified.clone())
765 .or_insert_with(|| helper.add_function(&callee_qualified, None, false, false));
766
767 let argument_count = count_call_arguments(node);
768 let call_span = Span::from_bytes(node.start_byte(), node.end_byte());
769 helper.add_call_edge_full_with_span(
770 caller_function_id,
771 target_function_id,
772 argument_count,
773 false,
774 vec![call_span],
775 );
776}
777
778fn process_object_creation(
779 node: Node,
780 content: &[u8],
781 ast_graph: &ASTGraph,
782 helper: &mut GraphBuildHelper,
783 node_map: &mut HashMap<String, NodeId>,
784) {
785 let Some(type_node) = node.child_by_field_name("type") else {
786 return;
787 };
788
789 let Ok(type_name) = type_node.utf8_text(content) else {
790 return;
791 };
792
793 let Some(call_context) = ast_graph.get_callable_context(node.id()) else {
795 return;
796 };
797
798 let callee_qualified = format!("{type_name}.ctor");
800
801 let caller_function_id = *node_map
803 .entry(call_context.qualified_name.clone())
804 .or_insert_with(|| helper.add_function(&call_context.qualified_name, None, false, false));
805
806 let target_function_id = *node_map
808 .entry(callee_qualified.clone())
809 .or_insert_with(|| helper.add_method(&callee_qualified, None, false, false));
810
811 let argument_count = count_call_arguments(node);
812 let call_span = Span::from_bytes(node.start_byte(), node.end_byte());
813 helper.add_call_edge_full_with_span(
814 caller_function_id,
815 target_function_id,
816 argument_count,
817 false,
818 vec![call_span],
819 );
820}
821
822fn count_call_arguments(call_node: Node<'_>) -> u8 {
823 let args_node = call_node
824 .child_by_field_name("arguments")
825 .or_else(|| call_node.child_by_field_name("argument_list"))
826 .or_else(|| {
827 let mut cursor = call_node.walk();
828 call_node
829 .children(&mut cursor)
830 .find(|child| child.kind() == "argument_list")
831 });
832
833 let Some(args_node) = args_node else {
834 return 255;
835 };
836
837 let count = args_node.named_child_count();
838 if count <= 254 {
839 u8::try_from(count).unwrap_or(u8::MAX)
840 } else {
841 u8::MAX
842 }
843}
844
845fn process_using_directive(node: Node, content: &[u8], helper: &mut GraphBuildHelper) {
899 let is_static = node
901 .children(&mut node.walk())
902 .any(|child| child.kind() == "static");
903
904 let has_equals = node
907 .children(&mut node.walk())
908 .any(|child| child.kind() == "=");
909
910 let (alias, imported_name) = if has_equals {
912 extract_aliased_using(node, content)
914 } else {
915 (None, extract_simple_using_target(node, content))
917 };
918
919 let Some(imported_name) = imported_name else {
920 return;
921 };
922
923 let module_id = helper.add_module("<file>", None);
925
926 let span = Span::from_bytes(node.start_byte(), node.end_byte());
928 let import_name = if is_static {
929 format!("static {imported_name}")
930 } else {
931 imported_name.clone()
932 };
933 let imported_id = helper.add_import(&import_name, Some(span));
934
935 match (alias.as_deref(), is_static) {
939 (Some(alias_str), _) => {
940 helper.add_import_edge_full(module_id, imported_id, Some(alias_str), false);
942 }
943 (None, true) => {
944 helper.add_import_edge_full(module_id, imported_id, None, true);
947 }
948 (None, false) => {
949 helper.add_import_edge(module_id, imported_id);
951 }
952 }
953}
954
955fn extract_aliased_using(node: Node, content: &[u8]) -> (Option<String>, Option<String>) {
961 let mut alias: Option<String> = None;
962 let mut target: Option<String> = None;
963 let mut past_equals = false;
964
965 let mut cursor = node.walk();
966 for child in node.children(&mut cursor) {
967 let kind = child.kind();
968
969 if kind == "=" {
970 past_equals = true;
971 continue;
972 }
973
974 if kind == "using" || kind == "static" || kind == ";" {
976 continue;
977 }
978
979 if past_equals {
980 if matches!(kind, "identifier" | "qualified_name") && target.is_none() {
982 target = child
983 .utf8_text(content)
984 .ok()
985 .map(std::string::ToString::to_string);
986 }
987 } else if kind == "identifier" && alias.is_none() {
988 alias = child
990 .utf8_text(content)
991 .ok()
992 .map(std::string::ToString::to_string);
993 }
994 }
995
996 (alias, target)
997}
998
999fn extract_simple_using_target(node: Node, content: &[u8]) -> Option<String> {
1004 let mut cursor = node.walk();
1005
1006 for child in node.children(&mut cursor) {
1007 let kind = child.kind();
1008
1009 if kind == "using" || kind == "static" || kind == ";" || kind == "=" {
1011 continue;
1012 }
1013
1014 if matches!(kind, "identifier" | "identifier_name" | "qualified_name") {
1016 return child
1017 .utf8_text(content)
1018 .ok()
1019 .map(std::string::ToString::to_string);
1020 }
1021 }
1022
1023 node.child_by_field_name("name")
1025 .and_then(|n| n.utf8_text(content).ok())
1026 .map(std::string::ToString::to_string)
1027}
1028
1029fn process_class_declaration(
1051 node: Node,
1052 content: &[u8],
1053 helper: &mut GraphBuildHelper,
1054 node_map: &mut HashMap<String, NodeId>,
1055 qualified_class_name: &str,
1056 namespace_stack: &[String],
1057) {
1058 let class_id = *node_map
1060 .entry(qualified_class_name.to_string())
1061 .or_insert_with(|| helper.add_class(qualified_class_name, None));
1062
1063 let mut cursor = node.walk();
1065 let base_list = node
1066 .children(&mut cursor)
1067 .find(|child| child.kind() == "base_list");
1068
1069 let Some(base_list) = base_list else {
1070 return;
1071 };
1072
1073 let mut first_base_class = true;
1075
1076 let mut base_cursor = base_list.walk();
1078 for base_child in base_list.children(&mut base_cursor) {
1079 let base_type_name = match base_child.kind() {
1080 "identifier" | "identifier_name" | "type_identifier" | "qualified_name" => base_child
1081 .utf8_text(content)
1082 .ok()
1083 .map(std::string::ToString::to_string),
1084 "generic_name" => {
1085 base_child
1087 .child_by_field_name("name")
1088 .or_else(|| base_child.child(0))
1089 .and_then(|n| n.utf8_text(content).ok())
1090 .map(std::string::ToString::to_string)
1091 }
1092 _ => None,
1093 };
1094
1095 let Some(base_name) = base_type_name else {
1096 continue;
1097 };
1098
1099 let qualified_base_name = qualify_type_name(&base_name, namespace_stack);
1102
1103 let is_interface = is_interface_name(&base_name);
1107
1108 if is_interface {
1109 let interface_id = *node_map
1111 .entry(qualified_base_name.clone())
1112 .or_insert_with(|| helper.add_interface(&qualified_base_name, None));
1113 helper.add_implements_edge(class_id, interface_id);
1114 } else if first_base_class {
1115 let parent_id = *node_map
1117 .entry(qualified_base_name.clone())
1118 .or_insert_with(|| helper.add_class(&qualified_base_name, None));
1119 helper.add_inherits_edge(class_id, parent_id);
1120 first_base_class = false;
1121 }
1122 }
1124}
1125
1126fn process_interface_declaration(
1143 node: Node,
1144 content: &[u8],
1145 helper: &mut GraphBuildHelper,
1146 node_map: &mut HashMap<String, NodeId>,
1147 qualified_interface_name: &str,
1148 namespace_stack: &[String],
1149) {
1150 let interface_id = *node_map
1152 .entry(qualified_interface_name.to_string())
1153 .or_insert_with(|| helper.add_interface(qualified_interface_name, None));
1154
1155 let mut cursor = node.walk();
1157 let base_list = node
1158 .children(&mut cursor)
1159 .find(|child| child.kind() == "base_list");
1160
1161 let Some(base_list) = base_list else {
1162 return;
1163 };
1164
1165 let mut base_cursor = base_list.walk();
1167 for base_child in base_list.children(&mut base_cursor) {
1168 let parent_name = match base_child.kind() {
1169 "identifier" | "identifier_name" | "type_identifier" | "qualified_name" => base_child
1170 .utf8_text(content)
1171 .ok()
1172 .map(std::string::ToString::to_string),
1173 "generic_name" => base_child
1174 .child_by_field_name("name")
1175 .or_else(|| base_child.child(0))
1176 .and_then(|n| n.utf8_text(content).ok())
1177 .map(std::string::ToString::to_string),
1178 _ => None,
1179 };
1180
1181 let Some(parent_name) = parent_name else {
1182 continue;
1183 };
1184
1185 let qualified_parent_name = qualify_type_name(&parent_name, namespace_stack);
1187
1188 let parent_id = *node_map
1190 .entry(qualified_parent_name.clone())
1191 .or_insert_with(|| helper.add_interface(&qualified_parent_name, None));
1192 helper.add_inherits_edge(interface_id, parent_id);
1193 }
1194}
1195
1196fn is_interface_name(name: &str) -> bool {
1202 let chars: Vec<char> = name.chars().collect();
1203 if chars.len() >= 2 {
1204 chars[0] == 'I' && chars[1].is_ascii_uppercase()
1206 } else {
1207 false
1208 }
1209}
1210
1211fn is_public(node: Node, content: &[u8]) -> bool {
1217 has_visibility_modifier(node, content, "public")
1218}
1219
1220fn is_internal(node: Node, content: &[u8]) -> bool {
1223 has_visibility_modifier(node, content, "internal")
1224}
1225
1226fn is_private(node: Node, content: &[u8]) -> bool {
1228 has_visibility_modifier(node, content, "private")
1229}
1230
1231#[allow(dead_code)] fn is_protected(node: Node, content: &[u8]) -> bool {
1234 has_visibility_modifier(node, content, "protected")
1235}
1236
1237fn has_visibility_modifier(node: Node, content: &[u8], modifier: &str) -> bool {
1239 node.children(&mut node.walk())
1240 .any(|child| child.kind() == modifier || child.utf8_text(content).unwrap_or("") == modifier)
1241}
1242
1243fn has_modifier(node: Node, content: &[u8], modifier: &str) -> bool {
1244 node.children(&mut node.walk())
1245 .any(|child| child.kind() == modifier || child.utf8_text(content).unwrap_or("") == modifier)
1246}
1247
1248fn should_export(node: Node, content: &[u8]) -> bool {
1254 is_public(node, content) || is_internal(node, content)
1255}
1256
1257fn export_from_file_module(helper: &mut GraphBuildHelper, exported: NodeId) {
1259 let module_id = helper.add_module(FILE_MODULE_NAME, None);
1260 helper.add_export_edge(module_id, exported);
1261}
1262
1263fn process_class_member_exports(
1265 body_node: Node,
1266 content: &[u8],
1267 class_qualified_name: &str,
1268 helper: &mut GraphBuildHelper,
1269 node_map: &mut HashMap<String, NodeId>,
1270) {
1271 let mut cursor = body_node.walk();
1272 for child in body_node.children(&mut cursor) {
1273 match child.kind() {
1274 "method_declaration" | "constructor_declaration" => {
1275 if should_export(child, content)
1277 && let Some(name_node) = child.child_by_field_name("name")
1278 && let Ok(method_name) = name_node.utf8_text(content)
1279 {
1280 let qualified_name = format!("{class_qualified_name}.{method_name}");
1281 if let Some(method_id) = node_map.get(&qualified_name) {
1283 export_from_file_module(helper, *method_id);
1284 }
1285 } else if should_export(child, content) && child.kind() == "constructor_declaration"
1286 {
1287 let class_name = class_qualified_name
1289 .rsplit('.')
1290 .next()
1291 .unwrap_or(class_qualified_name);
1292 let qualified_name = format!("{class_qualified_name}.{class_name}");
1293 if let Some(method_id) = node_map.get(&qualified_name) {
1294 export_from_file_module(helper, *method_id);
1295 }
1296 }
1297 }
1298 "field_declaration" | "property_declaration" => {
1299 if should_export(child, content) {
1305 let is_property = child.kind() == "property_declaration";
1306 let is_const = !is_property && has_modifier(child, content, "const");
1307 let is_readonly = !is_property && has_modifier(child, content, "readonly");
1308 let is_static = is_const || has_modifier(child, content, "static");
1309 let visibility = extract_field_visibility(child, content);
1310 let get_only = is_property && is_get_only_property(child);
1311 let emit_constant = is_const || is_readonly || get_only;
1312
1313 let mut field_cursor = child.walk();
1315 for field_child in child.children(&mut field_cursor) {
1316 if field_child.kind() == "variable_declarator"
1317 && let Some(name_node) = field_child.child_by_field_name("name")
1318 && let Ok(field_name) = name_node.utf8_text(content)
1319 {
1320 let qualified_name = format!("{class_qualified_name}.{field_name}");
1321 let span =
1322 Span::from_bytes(field_child.start_byte(), field_child.end_byte());
1323
1324 let field_id = if emit_constant {
1325 helper.add_constant_with_static_and_visibility(
1326 &qualified_name,
1327 Some(span),
1328 is_static,
1329 Some(visibility),
1330 )
1331 } else {
1332 helper.add_property_with_static_and_visibility(
1333 &qualified_name,
1334 Some(span),
1335 is_static,
1336 Some(visibility),
1337 )
1338 };
1339 export_from_file_module(helper, field_id);
1340 } else if field_child.kind() == "identifier"
1341 && let Ok(prop_name) = field_child.utf8_text(content)
1342 {
1343 let qualified_name = format!("{class_qualified_name}.{prop_name}");
1345 let span = Span::from_bytes(child.start_byte(), child.end_byte());
1346
1347 let prop_id = if emit_constant {
1348 helper.add_constant_with_static_and_visibility(
1349 &qualified_name,
1350 Some(span),
1351 is_static,
1352 Some(visibility),
1353 )
1354 } else {
1355 helper.add_property_with_static_and_visibility(
1356 &qualified_name,
1357 Some(span),
1358 is_static,
1359 Some(visibility),
1360 )
1361 };
1362 export_from_file_module(helper, prop_id);
1363 }
1364 }
1365 }
1366 }
1367 _ => {}
1368 }
1369 }
1370}
1371
1372fn process_interface_member_exports(
1375 body_node: Node,
1376 content: &[u8],
1377 interface_qualified_name: &str,
1378 helper: &mut GraphBuildHelper,
1379 node_map: &mut HashMap<String, NodeId>,
1380) {
1381 let mut cursor = body_node.walk();
1382 for child in body_node.children(&mut cursor) {
1383 if child.kind() == "method_declaration"
1384 && !is_private(child, content)
1385 && let Some(name_node) = child.child_by_field_name("name")
1386 && let Ok(method_name) = name_node.utf8_text(content)
1387 {
1388 let qualified_name = format!("{interface_qualified_name}.{method_name}");
1390 if let Some(method_id) = node_map.get(&qualified_name) {
1392 export_from_file_module(helper, *method_id);
1393 }
1394 }
1395 }
1396}
1397
1398fn process_pinvoke_method(
1429 node: Node,
1430 content: &[u8],
1431 helper: &mut GraphBuildHelper,
1432 node_map: &mut HashMap<String, NodeId>,
1433 namespace_stack: &[String],
1434) {
1435 let has_extern = node
1437 .children(&mut node.walk())
1438 .any(|child| child.kind() == "extern");
1439
1440 if !has_extern {
1441 return;
1442 }
1443
1444 let mut cursor = node.walk();
1446 let attribute_list = node
1447 .children(&mut cursor)
1448 .find(|child| child.kind() == "attribute_list");
1449
1450 let Some(attribute_list) = attribute_list else {
1451 return;
1452 };
1453
1454 let (dll_name, calling_convention) = extract_dllimport_info(attribute_list, content);
1456
1457 let Some(dll_name) = dll_name else {
1458 return;
1459 };
1460
1461 let method_name = node
1463 .child_by_field_name("name")
1464 .and_then(|n| n.utf8_text(content).ok())
1465 .map(std::string::ToString::to_string);
1466
1467 let Some(method_name) = method_name else {
1468 return;
1469 };
1470
1471 let qualified_method = if namespace_stack.is_empty() {
1473 method_name.clone()
1474 } else {
1475 format!("{}.{}", namespace_stack.join("."), method_name)
1476 };
1477
1478 let method_span = Span::from_bytes(node.start_byte(), node.end_byte());
1480 let method_id = *node_map
1481 .entry(qualified_method.clone())
1482 .or_insert_with(|| helper.add_method(&qualified_method, Some(method_span), false, true));
1483
1484 let ffi_func_name = format!("ffi::{dll_name}::{method_name}");
1486 let ffi_func_id = *node_map
1487 .entry(ffi_func_name.clone())
1488 .or_insert_with(|| helper.add_function(&ffi_func_name, None, false, false));
1489
1490 let convention = match calling_convention.as_deref() {
1492 Some("CallingConvention.Cdecl" | "Cdecl") => FfiConvention::Cdecl,
1493 Some("CallingConvention.FastCall" | "FastCall") => FfiConvention::Fastcall,
1494 _ => FfiConvention::Stdcall,
1496 };
1497
1498 helper.add_ffi_edge(method_id, ffi_func_id, convention);
1500}
1501
1502fn extract_dllimport_info(
1506 attribute_list: Node,
1507 content: &[u8],
1508) -> (Option<String>, Option<String>) {
1509 let mut dll_name = None;
1510 let mut calling_convention = None;
1511
1512 let mut list_cursor = attribute_list.walk();
1513 for attr_child in attribute_list.children(&mut list_cursor) {
1514 if attr_child.kind() != "attribute" {
1515 continue;
1516 }
1517
1518 let attr_name = attr_child
1520 .child_by_field_name("name")
1521 .and_then(|n| n.utf8_text(content).ok());
1522
1523 let is_dllimport = attr_name.is_some_and(|name| {
1524 name == "DllImport" || name == "System.Runtime.InteropServices.DllImport"
1525 });
1526
1527 if !is_dllimport {
1528 continue;
1529 }
1530
1531 let mut attr_cursor = attr_child.walk();
1533 let arg_list = attr_child
1534 .children(&mut attr_cursor)
1535 .find(|child| child.kind() == "attribute_argument_list");
1536
1537 let Some(arg_list) = arg_list else {
1538 continue;
1539 };
1540
1541 let mut arg_cursor = arg_list.walk();
1542 for arg in arg_list.children(&mut arg_cursor) {
1543 if arg.kind() != "attribute_argument" {
1544 continue;
1545 }
1546
1547 if let Some(name_node) = arg.child_by_field_name("name")
1549 && let Ok(name) = name_node.utf8_text(content)
1550 {
1551 if name == "CallingConvention"
1552 && let Some(expr) = arg.child_by_field_name("expression")
1553 && let Ok(value) = expr.utf8_text(content)
1554 {
1555 calling_convention = Some(value.to_string());
1556 }
1557 continue;
1558 }
1559
1560 if dll_name.is_none() {
1562 let expr = arg.child_by_field_name("expression").or_else(|| {
1564 let mut c = arg.walk();
1566 arg.children(&mut c)
1567 .find(|child| child.kind() == "string_literal")
1568 });
1569
1570 if let Some(expr) = expr
1571 && let Ok(text) = expr.utf8_text(content)
1572 {
1573 let trimmed = text.trim();
1575 if (trimmed.starts_with('"') && trimmed.ends_with('"'))
1576 || (trimmed.starts_with('@') && trimmed.len() > 2)
1577 {
1578 let start = if trimmed.starts_with('@') { 2 } else { 1 };
1579 dll_name = Some(trimmed[start..trimmed.len() - 1].to_string());
1580 } else {
1581 dll_name = Some(trimmed.to_string());
1582 }
1583 }
1584 }
1585 }
1586 }
1587
1588 (dll_name, calling_convention)
1589}
1590
1591fn process_local_variables(
1615 node: Node,
1616 content: &[u8],
1617 helper: &mut GraphBuildHelper,
1618 _class_stack: &[String],
1619) {
1620 let mut cursor = node.walk();
1622 let var_decl = node
1623 .children(&mut cursor)
1624 .find(|child| child.kind() == "variable_declaration");
1625
1626 let Some(var_decl) = var_decl else {
1627 return;
1628 };
1629
1630 let type_node = var_decl.child_by_field_name("type");
1632 let Some(type_node) = type_node else {
1633 return;
1634 };
1635
1636 let type_text = extract_type_string(type_node, content);
1638 let Some(type_text) = type_text else {
1639 return;
1640 };
1641
1642 let all_type_names = extract_all_type_names_from_annotation(type_node, content);
1644
1645 let mut var_cursor = var_decl.walk();
1647 for child in var_decl.children(&mut var_cursor) {
1648 if child.kind() == "variable_declarator"
1649 && let Some(name_node) = child.child_by_field_name("name")
1650 && let Ok(var_name) = name_node.utf8_text(content)
1651 {
1652 let span = Span::from_bytes(child.start_byte(), child.end_byte());
1653
1654 let var_id = helper.add_variable(var_name, Some(span));
1656
1657 let type_id = helper.add_type(&type_text, None);
1659 helper.add_typeof_edge_with_context(
1660 var_id,
1661 type_id,
1662 Some(TypeOfContext::Variable),
1663 None,
1664 Some(var_name),
1665 );
1666
1667 for type_name in &all_type_names {
1669 let ref_type_id = helper.add_type(type_name, None);
1670 helper.add_reference_edge(var_id, ref_type_id);
1671 }
1672 }
1673 }
1674}
1675
1676fn process_field_declaration(
1694 node: Node,
1695 content: &[u8],
1696 helper: &mut GraphBuildHelper,
1697 class_stack: &[String],
1698) {
1699 let decl_node = node
1701 .children(&mut node.walk())
1702 .find(|child| child.kind() == "variable_declaration");
1703
1704 let Some(decl_node) = decl_node else {
1705 return;
1706 };
1707
1708 let type_node = decl_node.child_by_field_name("type");
1710 let Some(type_node) = type_node else {
1711 return;
1712 };
1713
1714 let type_text = extract_type_string(type_node, content);
1716 let Some(type_text) = type_text else {
1717 return;
1718 };
1719
1720 let all_type_names = extract_all_type_names_from_annotation(type_node, content);
1722
1723 let class_name = class_stack.last().map_or("", String::as_str);
1725
1726 let mut var_cursor = decl_node.walk();
1728 for child in decl_node.children(&mut var_cursor) {
1729 if child.kind() == "variable_declarator"
1730 && let Some(name_node) = child.child_by_field_name("name")
1731 && let Ok(field_name) = name_node.utf8_text(content)
1732 {
1733 let qualified_name = if class_name.is_empty() {
1735 field_name.to_string()
1736 } else {
1737 format!("{class_name}.{field_name}")
1738 };
1739
1740 let span = Span::from_bytes(child.start_byte(), child.end_byte());
1741
1742 let is_const = has_modifier(node, content, "const");
1751 let is_readonly = has_modifier(node, content, "readonly");
1752 let is_static = is_const || has_modifier(node, content, "static");
1753 let visibility = extract_field_visibility(node, content);
1754
1755 let field_id = if is_const || is_readonly {
1756 helper.add_constant_with_static_and_visibility(
1757 &qualified_name,
1758 Some(span),
1759 is_static,
1760 Some(visibility),
1761 )
1762 } else {
1763 helper.add_property_with_static_and_visibility(
1764 &qualified_name,
1765 Some(span),
1766 is_static,
1767 Some(visibility),
1768 )
1769 };
1770
1771 let type_id = helper.add_type(&type_text, None);
1775 helper.add_typeof_edge_with_context(
1776 field_id,
1777 type_id,
1778 Some(TypeOfContext::Field),
1779 None,
1780 Some(field_name),
1781 );
1782
1783 for type_name in &all_type_names {
1785 let ref_type_id = helper.add_type(type_name, None);
1786 helper.add_reference_edge(field_id, ref_type_id);
1787 }
1788 }
1789 }
1790}
1791
1792fn process_property_declaration(
1809 node: Node,
1810 content: &[u8],
1811 helper: &mut GraphBuildHelper,
1812 class_stack: &[String],
1813) {
1814 let type_node = node.child_by_field_name("type");
1816 let Some(type_node) = type_node else {
1817 return;
1818 };
1819
1820 let type_text = extract_type_string(type_node, content);
1822 let Some(type_text) = type_text else {
1823 return;
1824 };
1825
1826 let all_type_names = extract_all_type_names_from_annotation(type_node, content);
1828
1829 let name_node = node.child_by_field_name("name");
1831 let Some(name_node) = name_node else {
1832 return;
1833 };
1834
1835 let Ok(prop_name) = name_node.utf8_text(content) else {
1836 return;
1837 };
1838
1839 let class_name = class_stack.last().map_or("", String::as_str);
1841
1842 let qualified_name = if class_name.is_empty() {
1844 prop_name.to_string()
1845 } else {
1846 format!("{class_name}.{prop_name}")
1847 };
1848
1849 let span = Span::from_bytes(node.start_byte(), node.end_byte());
1850
1851 let is_static = has_modifier(node, content, "static");
1865 let visibility = extract_field_visibility(node, content);
1866 let get_only = is_get_only_property(node);
1867
1868 let prop_id = if get_only {
1869 helper.add_constant_with_static_and_visibility(
1870 &qualified_name,
1871 Some(span),
1872 is_static,
1873 Some(visibility),
1874 )
1875 } else {
1876 helper.add_property_with_static_and_visibility(
1877 &qualified_name,
1878 Some(span),
1879 is_static,
1880 Some(visibility),
1881 )
1882 };
1883
1884 let type_id = helper.add_type(&type_text, None);
1889 helper.add_typeof_edge_with_context(
1890 prop_id,
1891 type_id,
1892 Some(TypeOfContext::Field),
1893 None,
1894 Some(prop_name),
1895 );
1896
1897 for type_name in &all_type_names {
1899 let ref_type_id = helper.add_type(type_name, None);
1900 helper.add_reference_edge(prop_id, ref_type_id);
1901 }
1902}
1903
1904fn extract_field_visibility(node: Node, content: &[u8]) -> &'static str {
1911 let has_protected = has_modifier(node, content, "protected");
1912 let has_internal = has_modifier(node, content, "internal");
1913 if has_protected && has_internal {
1914 "protected internal"
1915 } else if has_modifier(node, content, "public") {
1916 "public"
1917 } else if has_protected {
1918 "protected"
1919 } else if has_internal {
1920 "internal"
1921 } else if has_modifier(node, content, "private") {
1922 "private"
1923 } else {
1924 "private"
1926 }
1927}
1928
1929fn is_get_only_property(node: Node) -> bool {
1948 if node
1952 .children(&mut node.walk())
1953 .any(|child| child.kind() == "arrow_expression_clause")
1954 {
1955 return false;
1956 }
1957
1958 let Some(accessor_list) = node
1959 .children(&mut node.walk())
1960 .find(|child| child.kind() == "accessor_list")
1961 else {
1962 return false;
1965 };
1966
1967 let mut has_auto_get = false;
1968 let mut has_set_like = false;
1969 for accessor in accessor_list.children(&mut accessor_list.walk()) {
1970 if accessor.kind() != "accessor_declaration" {
1971 continue;
1972 }
1973
1974 let mut keyword: Option<&str> = None;
1980 let mut has_body = false;
1981 for tok in accessor.children(&mut accessor.walk()) {
1982 match tok.kind() {
1983 "get" | "set" | "init" => keyword = Some(tok.kind()),
1984 "block" | "arrow_expression_clause" => has_body = true,
1985 _ => {}
1986 }
1987 }
1988
1989 match keyword {
1990 Some("get") => {
1991 if has_body {
1992 return false;
1995 }
1996 has_auto_get = true;
1997 }
1998 Some("set" | "init") => has_set_like = true,
1999 _ => {}
2000 }
2001 }
2002
2003 has_auto_get && !has_set_like
2004}
2005fn process_method_parameters(
2022 node: Node,
2023 _method_name: &str,
2024 content: &[u8],
2025 helper: &mut GraphBuildHelper,
2026) {
2027 let Some(param_list) = node.child_by_field_name("parameter_list") else {
2029 return;
2030 };
2031
2032 let mut cursor = param_list.walk();
2034 let mut param_index: u16 = 0;
2035
2036 for child in param_list.children(&mut cursor) {
2038 if !child.is_named() {
2040 continue;
2041 }
2042 {
2044 let Some(name_node) = child.child_by_field_name("name") else {
2046 continue;
2047 };
2048 let Ok(param_name) = name_node.utf8_text(content) else {
2049 continue;
2050 };
2051
2052 let Some(type_node) = child.child_by_field_name("type") else {
2054 continue;
2055 };
2056
2057 let Some(type_text) = extract_type_string(type_node, content) else {
2059 continue;
2060 };
2061
2062 let all_type_names = extract_all_type_names_from_annotation(type_node, content);
2064
2065 let param_span = Span::from_bytes(child.start_byte(), child.end_byte());
2067 let param_id = helper.add_variable(param_name, Some(param_span));
2068
2069 let type_id = helper.add_type(&type_text, None);
2071 helper.add_typeof_edge_with_context(
2072 param_id,
2073 type_id,
2074 Some(TypeOfContext::Parameter),
2075 Some(param_index),
2076 Some(param_name),
2077 );
2078
2079 for type_name in &all_type_names {
2081 let ref_type_id = helper.add_type(type_name, None);
2082 helper.add_reference_edge(param_id, ref_type_id);
2083 }
2084
2085 param_index += 1;
2086 }
2087 }
2088}
2089
2090fn process_method_return_type(
2105 node: Node,
2106 method_name: &str,
2107 content: &[u8],
2108 helper: &mut GraphBuildHelper,
2109) {
2110 let return_type_node = node
2112 .child_by_field_name("return_type")
2113 .or_else(|| node.child_by_field_name("returns"))
2114 .or_else(|| node.child_by_field_name("type"));
2115
2116 let Some(return_type_node) = return_type_node else {
2117 return;
2118 };
2119
2120 if let Ok(type_text) = return_type_node.utf8_text(content)
2122 && type_text.trim() == "void"
2123 {
2124 return;
2125 }
2126
2127 let Some(type_text) = extract_type_string(return_type_node, content) else {
2129 return;
2130 };
2131
2132 let all_type_names = extract_all_type_names_from_annotation(return_type_node, content);
2134
2135 let method_span = Span::from_bytes(node.start_byte(), node.end_byte());
2137 let method_id = helper.add_method(method_name, Some(method_span), false, false);
2138
2139 let type_id = helper.add_type(&type_text, None);
2141 helper.add_typeof_edge_with_context(
2142 method_id,
2143 type_id,
2144 Some(TypeOfContext::Return),
2145 Some(0), Some(method_name),
2147 );
2148
2149 for type_name in &all_type_names {
2151 let ref_type_id = helper.add_type(type_name, None);
2152 helper.add_reference_edge(method_id, ref_type_id);
2153 }
2154}
2155
2156fn process_type_parameter_declarations(
2202 decl_node: Node,
2203 content: &[u8],
2204 parent_qualified_name: &str,
2205 helper: &mut GraphBuildHelper,
2206) {
2207 let mut decl_cursor = decl_node.walk();
2210 let Some(params_node) = decl_node
2211 .children(&mut decl_cursor)
2212 .find(|c| c.kind() == "type_parameter_list")
2213 else {
2214 return;
2215 };
2216
2217 let mut param_ids: HashMap<String, sqry_core::graph::unified::node::NodeId> = HashMap::new();
2220
2221 let mut params_cursor = params_node.walk();
2222 for param_node in params_node.children(&mut params_cursor) {
2223 if param_node.kind() != "type_parameter" {
2224 continue;
2225 }
2226
2227 let Some(name_node) = param_node.child_by_field_name("name") else {
2229 continue;
2230 };
2231 let Ok(param_name) = name_node.utf8_text(content) else {
2232 continue;
2233 };
2234
2235 let qualified_param = format!("{parent_qualified_name}.{param_name}");
2236 let span = Span::from_bytes(name_node.start_byte(), name_node.end_byte());
2237 let param_id = helper.add_type(&qualified_param, Some(span));
2241 param_ids.insert(param_name.to_string(), param_id);
2242 }
2243
2244 let mut clause_cursor = decl_node.walk();
2248 for clause_node in decl_node.children(&mut clause_cursor) {
2249 if clause_node.kind() != "type_parameter_constraints_clause" {
2250 continue;
2251 }
2252 emit_type_parameter_constraint_clause(clause_node, content, ¶m_ids, helper);
2253 }
2254}
2255
2256fn emit_type_parameter_constraint_clause(
2271 clause_node: Node,
2272 content: &[u8],
2273 param_ids: &HashMap<String, sqry_core::graph::unified::node::NodeId>,
2274 helper: &mut GraphBuildHelper,
2275) {
2276 let mut cursor = clause_node.walk();
2280 let mut named_children = clause_node
2281 .children(&mut cursor)
2282 .filter(tree_sitter::Node::is_named);
2283
2284 let Some(param_id_node) = named_children.next() else {
2285 return;
2286 };
2287 if param_id_node.kind() != "identifier" {
2288 return;
2289 }
2290 let Ok(param_name) = param_id_node.utf8_text(content) else {
2291 return;
2292 };
2293 let Some(¶m_id) = param_ids.get(param_name) else {
2294 return;
2297 };
2298
2299 for constraint_node in named_children {
2300 if constraint_node.kind() != "type_parameter_constraint" {
2301 continue;
2302 }
2303 let Some(constraint_target_name) = extract_constraint_target_name(constraint_node, content)
2304 else {
2305 continue;
2306 };
2307 let constraint_id = helper.add_type(&constraint_target_name, None);
2308 helper.add_typeof_edge_with_context(
2309 param_id,
2310 constraint_id,
2311 Some(TypeOfContext::Constraint),
2312 None,
2313 None,
2314 );
2315 }
2316}
2317
2318fn extract_constraint_target_name(constraint_node: Node, content: &[u8]) -> Option<String> {
2336 let mut cursor = constraint_node.walk();
2339 let mut had_named_constructor = false;
2340 let mut keyword_token: Option<&'static str> = None;
2341
2342 for child in constraint_node.children(&mut cursor) {
2343 match child.kind() {
2344 "constructor_constraint" => {
2345 had_named_constructor = true;
2346 }
2347 "class" if !child.is_named() => {
2348 keyword_token = Some("class");
2349 }
2350 "struct" if !child.is_named() => {
2351 keyword_token = Some("struct");
2352 }
2353 "unmanaged" if !child.is_named() => {
2354 keyword_token = Some("unmanaged");
2355 }
2356 "notnull" if !child.is_named() => {
2357 keyword_token = Some("notnull");
2358 }
2359 _ => {}
2360 }
2361 }
2362
2363 if had_named_constructor {
2364 return Some("new()".to_string());
2365 }
2366 if let Some(kw) = keyword_token {
2367 return Some(kw.to_string());
2368 }
2369
2370 let type_node = constraint_node.child_by_field_name("type")?;
2373 Some(extract_constraint_base_type_name(type_node, content))
2374}
2375
2376fn extract_constraint_base_type_name(type_node: Node, content: &[u8]) -> String {
2385 match type_node.kind() {
2386 "generic_name" => {
2387 let mut cursor = type_node.walk();
2390 for child in type_node.children(&mut cursor) {
2391 if matches!(child.kind(), "identifier" | "qualified_name") {
2392 return extract_constraint_base_type_name(child, content);
2393 }
2394 }
2395 type_node.utf8_text(content).unwrap_or_default().to_string()
2396 }
2397 "qualified_name" => {
2398 let mut cursor = type_node.walk();
2403 let children: Vec<_> = type_node
2404 .children(&mut cursor)
2405 .filter(tree_sitter::Node::is_named)
2406 .collect();
2407 if let Some(last) = children.last()
2409 && last.kind() == "generic_name"
2410 {
2411 let prefix_segments: Vec<String> = children
2413 .iter()
2414 .take(children.len() - 1)
2415 .filter_map(|c| c.utf8_text(content).ok().map(str::to_string))
2416 .collect();
2417 let leaf_base = extract_constraint_base_type_name(*last, content);
2418 if prefix_segments.is_empty() {
2419 return leaf_base;
2420 }
2421 return format!("{}.{}", prefix_segments.join("."), leaf_base);
2422 }
2423 type_node.utf8_text(content).unwrap_or_default().to_string()
2424 }
2425 "nullable_type" => {
2426 let mut cursor = type_node.walk();
2429 for child in type_node.children(&mut cursor) {
2430 if child.is_named() {
2431 return extract_constraint_base_type_name(child, content);
2432 }
2433 }
2434 type_node.utf8_text(content).unwrap_or_default().to_string()
2435 }
2436 _ => type_node.utf8_text(content).unwrap_or_default().to_string(),
2437 }
2438}