1use std::collections::HashMap;
22use std::path::Path;
23use std::sync::OnceLock;
24
25use sqry_core::graph::unified::build::shape::{CfBucket, ShapeMapping};
26use sqry_core::graph::unified::edge::FfiConvention;
27use sqry_core::graph::unified::edge::kind::TypeOfContext;
28use sqry_core::graph::unified::storage::shape::SignatureShape;
29use sqry_core::graph::unified::{GraphBuildHelper, NodeId, StagingGraph};
30use sqry_core::graph::{
31 GraphBuilder, GraphBuilderError, GraphResult, GraphSnapshot, Language, Span,
32};
33use tree_sitter::{Node, Tree};
34
35use super::local_scopes;
36use super::type_extractor::{extract_all_type_names_from_annotation, extract_type_string};
37
38const DEFAULT_MAX_SCOPE_DEPTH: usize = 6;
39
40const FILE_MODULE_NAME: &str = "<file_module>";
43
44#[derive(Debug, Clone, Copy)]
46pub struct CSharpGraphBuilder {
47 max_scope_depth: usize,
48}
49
50impl Default for CSharpGraphBuilder {
51 fn default() -> Self {
52 Self {
53 max_scope_depth: DEFAULT_MAX_SCOPE_DEPTH,
54 }
55 }
56}
57
58impl CSharpGraphBuilder {
59 #[must_use]
60 pub fn new(max_scope_depth: usize) -> Self {
61 Self { max_scope_depth }
62 }
63}
64
65impl GraphBuilder for CSharpGraphBuilder {
66 fn build_graph(
67 &self,
68 tree: &Tree,
69 content: &[u8],
70 file: &Path,
71 staging: &mut StagingGraph,
72 ) -> GraphResult<()> {
73 let mut helper = GraphBuildHelper::new(staging, file, Language::CSharp);
74
75 let ast_graph = ASTGraph::from_tree(tree, content, self.max_scope_depth).map_err(|e| {
77 GraphBuilderError::ParseError {
78 span: Span::default(),
79 reason: e,
80 }
81 })?;
82
83 let mut node_map = HashMap::new();
85
86 for context in ast_graph.contexts() {
88 let qualified_name = &context.qualified_name;
89 let span = Span::from_bytes(context.span.0, context.span.1);
90
91 let node_id = match context.kind {
92 ContextKind::Function { is_async } => {
93 helper.add_function_with_signature(
95 qualified_name,
96 Some(span),
97 is_async,
98 false,
99 None, context.return_type.as_deref(),
101 )
102 }
103 ContextKind::Method {
104 is_async,
105 is_static,
106 } => {
107 helper.add_method_with_signature(
109 qualified_name,
110 Some(span),
111 is_async,
112 is_static,
113 None, context.return_type.as_deref(),
115 )
116 }
117 ContextKind::Class => helper.add_class(qualified_name, Some(span)),
118 ContextKind::Interface => helper.add_interface(qualified_name, Some(span)),
119 };
120 helper.mark_definition(node_id);
125 node_map.insert(qualified_name.clone(), node_id);
126 }
127
128 let mut scope_tree = local_scopes::build(tree.root_node(), content)?;
130
131 let mut namespace_stack = Vec::new();
134 let mut class_stack = Vec::new();
135 let root = tree.root_node();
136 walk_tree_for_edges(
137 root,
138 content,
139 &ast_graph,
140 &mut helper,
141 &mut node_map,
142 &mut namespace_stack,
143 &mut class_stack,
144 &mut scope_tree,
145 )?;
146
147 Ok(())
148 }
149
150 fn language(&self) -> Language {
151 Language::CSharp
152 }
153
154 fn shape_mapping(&self) -> Option<&dyn ShapeMapping> {
155 Some(csharp_shape_mapping())
156 }
157
158 fn detect_cross_language_edges(
159 &self,
160 _snapshot: &GraphSnapshot,
161 ) -> GraphResult<Vec<sqry_core::graph::CodeEdge>> {
162 Ok(vec![])
165 }
166}
167
168pub struct CSharpShapeMapping {
173 cf_by_kind_id: Vec<Option<CfBucket>>,
174}
175
176impl CSharpShapeMapping {
177 fn build() -> Self {
178 let lang: tree_sitter::Language = tree_sitter_c_sharp::LANGUAGE.into();
179 let count = lang.node_kind_count();
180 let mut cf_by_kind_id = vec![None; count];
181 for (id, slot) in cf_by_kind_id.iter_mut().enumerate() {
182 let Ok(kind_id) = u16::try_from(id) else {
183 break;
184 };
185 if !lang.node_kind_is_named(kind_id) {
186 continue;
187 }
188 if let Some(name) = lang.node_kind_for_id(kind_id) {
189 *slot = cf_bucket_for_csharp_kind(name);
190 }
191 }
192 Self { cf_by_kind_id }
193 }
194}
195
196impl ShapeMapping for CSharpShapeMapping {
197 fn cf_bucket(&self, ts_node_kind_id: u16) -> Option<CfBucket> {
198 self.cf_by_kind_id
199 .get(ts_node_kind_id as usize)
200 .copied()
201 .flatten()
202 }
203
204 fn signature_shape(&self, fn_node: Node, src: &[u8]) -> SignatureShape {
205 let mut shape = SignatureShape::default();
206 if let Some(params) = fn_node.child_by_field_name("parameters") {
212 let mut cursor = params.walk();
213 for child in params.named_children(&mut cursor) {
214 if child.kind() != "parameter" {
215 continue;
216 }
217 shape.arity_positional = shape.arity_positional.saturating_add(1);
218 let type_node = child.child_by_field_name("type");
219 let name_node = child.child_by_field_name("name");
220 let mut pc = child.walk();
221 for part in child.named_children(&mut pc) {
222 if Some(part) == type_node || Some(part) == name_node {
223 continue;
224 }
225 match part.kind() {
226 "attribute_list" => {}
227 "modifier" => {
228 if part.utf8_text(src).map(str::trim) == Ok("params") {
229 shape.has_varargs = true;
230 }
231 }
232 _ => shape.has_defaults = true,
234 }
235 }
236 }
237 }
238 shape.has_return_annotation = fn_node.child_by_field_name("returns").is_some();
240 shape
241 }
242}
243
244fn cf_bucket_for_csharp_kind(name: &str) -> Option<CfBucket> {
247 let bucket = match name {
248 "if_statement"
249 | "conditional_expression"
250 | "when_clause"
251 | "preproc_if"
252 | "preproc_elif" => CfBucket::Branch,
253 "for_statement" | "foreach_statement" | "while_statement" | "do_statement" => {
254 CfBucket::Loop
255 }
256 "switch_statement" | "switch_expression" | "switch_section" | "switch_expression_arm" => {
257 CfBucket::Match
258 }
259 "try_statement" => CfBucket::Try,
260 "catch_clause" => CfBucket::Catch,
261 "throw_statement" | "throw_expression" => CfBucket::Throw,
262 "using_statement" | "finally_clause" => CfBucket::Resource,
263 "return_statement" => CfBucket::Return,
264 "yield_statement" => CfBucket::Yield,
265 "await_expression" => CfBucket::Await,
266 "break_statement" | "continue_statement" | "goto_statement" => CfBucket::BreakContinue,
267 "invocation_expression" | "object_creation_expression" => CfBucket::Call,
268 "assignment_expression" | "variable_declaration" | "local_declaration_statement" => {
269 CfBucket::Assign
270 }
271 "lambda_expression" | "anonymous_method_expression" => CfBucket::Closure,
272 _ => return None,
273 };
274 Some(bucket)
275}
276
277#[must_use]
279pub fn csharp_shape_mapping() -> &'static CSharpShapeMapping {
280 static MAPPING: OnceLock<CSharpShapeMapping> = OnceLock::new();
281 MAPPING.get_or_init(CSharpShapeMapping::build)
282}
283
284#[derive(Debug, Clone)]
289enum ContextKind {
290 Function { is_async: bool },
291 Method { is_async: bool, is_static: bool },
292 Class,
293 Interface,
294}
295
296#[derive(Debug, Clone)]
297struct CallContext {
298 qualified_name: String,
299 span: (usize, usize),
300 kind: ContextKind,
301 class_name: Option<String>,
302 return_type: Option<String>,
304}
305
306struct ASTGraph {
307 contexts: Vec<CallContext>,
308 node_to_context: HashMap<usize, usize>,
309}
310
311impl ASTGraph {
312 fn from_tree(tree: &Tree, content: &[u8], max_depth: usize) -> Result<Self, String> {
313 let mut contexts = Vec::new();
314 let mut node_to_context = HashMap::new();
315 let mut scope_stack: Vec<String> = Vec::new();
316 let mut class_stack: Vec<String> = Vec::new();
317
318 let recursion_limits = sqry_core::config::RecursionLimits::load_or_default()
320 .map_err(|e| format!("Failed to load recursion limits: {e}"))?;
321 let file_ops_depth = recursion_limits
322 .effective_file_ops_depth()
323 .map_err(|e| format!("Invalid file_ops_depth configuration: {e}"))?;
324 let mut guard = sqry_core::query::security::RecursionGuard::new(file_ops_depth)
325 .map_err(|e| format!("Failed to create recursion guard: {e}"))?;
326
327 let mut walk_context = WalkContext {
328 content,
329 contexts: &mut contexts,
330 node_to_context: &mut node_to_context,
331 scope_stack: &mut scope_stack,
332 class_stack: &mut class_stack,
333 max_depth,
334 guard: &mut guard,
335 };
336
337 walk_ast(tree.root_node(), &mut walk_context)?;
338
339 Ok(Self {
340 contexts,
341 node_to_context,
342 })
343 }
344
345 fn contexts(&self) -> &[CallContext] {
346 &self.contexts
347 }
348
349 fn get_callable_context(&self, node_id: usize) -> Option<&CallContext> {
350 self.node_to_context
351 .get(&node_id)
352 .and_then(|idx| self.contexts.get(*idx))
353 }
354}
355
356#[allow(clippy::too_many_lines)] struct WalkContext<'a> {
361 content: &'a [u8],
362 contexts: &'a mut Vec<CallContext>,
363 node_to_context: &'a mut HashMap<usize, usize>,
364 scope_stack: &'a mut Vec<String>,
365 class_stack: &'a mut Vec<String>,
366 max_depth: usize,
367 guard: &'a mut sqry_core::query::security::RecursionGuard,
368}
369
370#[allow(clippy::too_many_lines)]
371fn walk_ast(node: Node, context: &mut WalkContext<'_>) -> Result<(), String> {
372 context
373 .guard
374 .enter()
375 .map_err(|e| format!("Recursion limit exceeded: {e}"))?;
376
377 if context.scope_stack.len() > context.max_depth {
378 context.guard.exit();
379 return Ok(());
380 }
381
382 match node.kind() {
383 "class_declaration" => {
384 let name_node = node
385 .child_by_field_name("name")
386 .ok_or_else(|| "class_declaration missing name".to_string())?;
387 let class_name = name_node
388 .utf8_text(context.content)
389 .map_err(|_| "failed to read class name".to_string())?;
390
391 let qualified_class = if context.scope_stack.is_empty() {
393 class_name.to_string()
394 } else {
395 format!("{}.{}", context.scope_stack.join("."), class_name)
396 };
397
398 context.class_stack.push(qualified_class.clone());
399 context.scope_stack.push(class_name.to_string());
400
401 let _context_idx = context.contexts.len();
403 context.contexts.push(CallContext {
404 qualified_name: qualified_class.clone(),
405 span: (node.start_byte(), node.end_byte()),
406 kind: ContextKind::Class,
407 class_name: Some(qualified_class),
408 return_type: None,
409 });
410
411 if let Some(body) = node.child_by_field_name("body") {
413 let mut cursor = body.walk();
414 for child in body.children(&mut cursor) {
415 walk_ast(child, context)?;
416 }
417 }
418
419 context.class_stack.pop();
420 context.scope_stack.pop();
421 }
422 "interface_declaration" => {
423 let name_node = node
424 .child_by_field_name("name")
425 .ok_or_else(|| "interface_declaration missing name".to_string())?;
426 let interface_name = name_node
427 .utf8_text(context.content)
428 .map_err(|_| "failed to read interface name".to_string())?;
429
430 let qualified_interface = if context.scope_stack.is_empty() {
432 interface_name.to_string()
433 } else {
434 format!("{}.{}", context.scope_stack.join("."), interface_name)
435 };
436
437 context.class_stack.push(qualified_interface.clone());
438 context.scope_stack.push(interface_name.to_string());
439
440 let _context_idx = context.contexts.len();
442 context.contexts.push(CallContext {
443 qualified_name: qualified_interface.clone(),
444 span: (node.start_byte(), node.end_byte()),
445 kind: ContextKind::Interface,
446 class_name: Some(qualified_interface),
447 return_type: None,
448 });
449
450 if let Some(body) = node.child_by_field_name("body") {
452 let mut cursor = body.walk();
453 for child in body.children(&mut cursor) {
454 walk_ast(child, context)?;
455 }
456 }
457
458 context.class_stack.pop();
459 context.scope_stack.pop();
460 }
461 "method_declaration" | "constructor_declaration" | "local_function_statement" => {
462 let name_node = node
463 .child_by_field_name("name")
464 .ok_or_else(|| format!("{} missing name", node.kind()).to_string())?;
465 let func_name = name_node
466 .utf8_text(context.content)
467 .map_err(|_| "failed to read function name".to_string())?;
468
469 let is_async = has_modifier(node, context.content, "async");
471
472 let is_static = has_modifier(node, context.content, "static");
474
475 let return_type = node
477 .child_by_field_name("return_type")
478 .or_else(|| node.child_by_field_name("returns"))
479 .or_else(|| node.child_by_field_name("type"))
480 .and_then(|type_node| type_node.utf8_text(context.content).ok())
481 .map(std::string::ToString::to_string);
482
483 let qualified_func = if context.scope_stack.is_empty() {
485 func_name.to_string()
486 } else {
487 format!("{}.{}", context.scope_stack.join("."), func_name)
488 };
489
490 let is_method = !context.class_stack.is_empty();
492 let class_name = context.class_stack.last().cloned();
493
494 let kind = if is_method {
495 ContextKind::Method {
496 is_async,
497 is_static,
498 }
499 } else {
500 ContextKind::Function { is_async }
501 };
502
503 let context_idx = context.contexts.len();
504 context.contexts.push(CallContext {
505 qualified_name: qualified_func.clone(),
506 span: (node.start_byte(), node.end_byte()),
507 kind,
508 class_name,
509 return_type,
510 });
511
512 if let Some(body) = node.child_by_field_name("body") {
514 associate_descendants(body, context_idx, context.node_to_context);
515 }
516
517 context.scope_stack.push(func_name.to_string());
518
519 if let Some(body) = node.child_by_field_name("body") {
521 let mut cursor = body.walk();
522 for child in body.children(&mut cursor) {
523 walk_ast(child, context)?;
524 }
525 }
526
527 context.scope_stack.pop();
528 }
529 "namespace_declaration" => {
530 if let Some(name_node) = node.child_by_field_name("name")
532 && let Ok(namespace_name) = name_node.utf8_text(context.content)
533 {
534 context.scope_stack.push(namespace_name.to_string());
535
536 if let Some(body) = node.child_by_field_name("body") {
538 let mut cursor = body.walk();
539 for child in body.children(&mut cursor) {
540 walk_ast(child, context)?;
541 }
542 }
543
544 context.scope_stack.pop();
545 }
546 }
547 _ => {
548 let mut cursor = node.walk();
550 for child in node.children(&mut cursor) {
551 walk_ast(child, context)?;
552 }
553 }
554 }
555
556 context.guard.exit();
557 Ok(())
558}
559
560fn associate_descendants(
561 node: Node,
562 context_idx: usize,
563 node_to_context: &mut HashMap<usize, usize>,
564) {
565 node_to_context.insert(node.id(), context_idx);
566
567 let mut stack = vec![node];
568 while let Some(current) = stack.pop() {
569 node_to_context.insert(current.id(), context_idx);
570
571 let mut cursor = current.walk();
572 for child in current.children(&mut cursor) {
573 stack.push(child);
574 }
575 }
576}
577
578#[allow(clippy::too_many_lines)] fn walk_tree_for_edges(
586 node: Node,
587 content: &[u8],
588 ast_graph: &ASTGraph,
589 helper: &mut GraphBuildHelper,
590 node_map: &mut HashMap<String, NodeId>,
591 namespace_stack: &mut Vec<String>,
592 class_stack: &mut Vec<String>,
593 scope_tree: &mut local_scopes::CSharpScopeTree,
594) -> GraphResult<()> {
595 match node.kind() {
596 "namespace_declaration" => {
597 if let Some(name_node) = node.child_by_field_name("name")
599 && let Ok(namespace_name) = name_node.utf8_text(content)
600 {
601 namespace_stack.push(namespace_name.to_string());
602
603 if let Some(body) = node.child_by_field_name("body") {
605 let mut cursor = body.walk();
606 for child in body.children(&mut cursor) {
607 walk_tree_for_edges(
608 child,
609 content,
610 ast_graph,
611 helper,
612 node_map,
613 namespace_stack,
614 class_stack,
615 scope_tree,
616 )?;
617 }
618 }
619
620 namespace_stack.pop();
621 return Ok(());
622 }
623 }
624 "class_declaration" => {
625 if let Some(name_node) = node.child_by_field_name("name")
627 && let Ok(class_name) = name_node.utf8_text(content)
628 {
629 let qualified_class =
631 build_qualified_name(namespace_stack, class_stack, class_name);
632 class_stack.push(class_name.to_string());
633
634 process_class_declaration(
636 node,
637 content,
638 helper,
639 node_map,
640 &qualified_class,
641 namespace_stack,
642 );
643
644 process_type_parameter_declarations(node, content, &qualified_class, helper);
648
649 if should_export(node, content)
651 && let Some(class_id) = node_map.get(&qualified_class)
652 {
653 export_from_file_module(helper, *class_id);
654 }
655
656 if let Some(body) = node.child_by_field_name("body") {
658 process_class_member_exports(body, content, &qualified_class, helper, node_map);
659
660 let mut cursor = body.walk();
661 for child in body.children(&mut cursor) {
662 walk_tree_for_edges(
663 child,
664 content,
665 ast_graph,
666 helper,
667 node_map,
668 namespace_stack,
669 class_stack,
670 scope_tree,
671 )?;
672 }
673 }
674
675 class_stack.pop();
676 return Ok(());
677 }
678 }
679 "interface_declaration" => {
680 if let Some(name_node) = node.child_by_field_name("name")
682 && let Ok(interface_name) = name_node.utf8_text(content)
683 {
684 let qualified_interface =
686 build_qualified_name(namespace_stack, class_stack, interface_name);
687
688 process_interface_declaration(
690 node,
691 content,
692 helper,
693 node_map,
694 &qualified_interface,
695 namespace_stack,
696 );
697
698 process_type_parameter_declarations(node, content, &qualified_interface, helper);
702
703 if should_export(node, content)
705 && let Some(interface_id) = node_map.get(&qualified_interface)
706 {
707 export_from_file_module(helper, *interface_id);
708 }
709
710 if let Some(body) = node.child_by_field_name("body") {
720 process_interface_member_exports(
721 body,
722 content,
723 &qualified_interface,
724 helper,
725 node_map,
726 );
727
728 class_stack.push(interface_name.to_string());
729 let mut cursor = body.walk();
730 for child in body.children(&mut cursor) {
731 walk_tree_for_edges(
732 child,
733 content,
734 ast_graph,
735 helper,
736 node_map,
737 namespace_stack,
738 class_stack,
739 scope_tree,
740 )?;
741 }
742 class_stack.pop();
743 }
744
745 return Ok(());
746 }
747 }
748 "invocation_expression" => {
749 process_invocation(node, content, ast_graph, helper, node_map);
750 }
751 "object_creation_expression" => {
752 process_object_creation(node, content, ast_graph, helper, node_map);
753 }
754 "using_directive" => {
755 process_using_directive(node, content, helper);
756 }
757 "method_declaration" => {
758 process_pinvoke_method(node, content, helper, node_map, namespace_stack);
760
761 if let Some(name_node) = node.child_by_field_name("name")
763 && let Ok(method_name) = name_node.utf8_text(content)
764 {
765 let mut scope_parts = namespace_stack.clone();
768 scope_parts.extend(class_stack.iter().cloned());
769
770 let qualified_name = if scope_parts.is_empty() {
771 method_name.to_string()
772 } else {
773 format!("{}.{}", scope_parts.join("."), method_name)
774 };
775
776 process_method_parameters(node, &qualified_name, content, helper);
777 process_method_return_type(node, &qualified_name, content, helper);
778
779 process_type_parameter_declarations(node, content, &qualified_name, helper);
783 }
784 }
785 "local_declaration_statement" => {
786 process_local_variables(node, content, helper, class_stack);
788 }
789 "field_declaration" => {
790 process_field_declaration(node, content, helper, class_stack);
792 }
793 "property_declaration" => {
794 process_property_declaration(node, content, helper, class_stack);
796 }
797 "identifier" => {
798 local_scopes::handle_identifier_for_reference(node, content, scope_tree, helper);
799 }
800 _ => {}
801 }
802
803 let mut cursor = node.walk();
805 for child in node.children(&mut cursor) {
806 walk_tree_for_edges(
807 child,
808 content,
809 ast_graph,
810 helper,
811 node_map,
812 namespace_stack,
813 class_stack,
814 scope_tree,
815 )?;
816 }
817
818 Ok(())
819}
820
821fn build_qualified_name(namespace_stack: &[String], class_stack: &[String], name: &str) -> String {
823 let mut parts = Vec::new();
824 parts.extend(namespace_stack.iter().cloned());
825 parts.extend(class_stack.iter().cloned());
826 parts.push(name.to_string());
827 parts.join(".")
828}
829
830fn qualify_type_name(type_name: &str, namespace_stack: &[String]) -> String {
837 if type_name.contains('.') {
839 return type_name.to_string();
840 }
841
842 if namespace_stack.is_empty() {
844 return type_name.to_string();
845 }
846
847 format!("{}.{}", namespace_stack.join("."), type_name)
849}
850
851fn process_invocation(
852 node: Node,
853 content: &[u8],
854 ast_graph: &ASTGraph,
855 helper: &mut GraphBuildHelper,
856 node_map: &mut HashMap<String, NodeId>,
857) {
858 let Some(function_node) = node.child_by_field_name("function") else {
859 return;
860 };
861
862 let Ok(callee_text) = function_node.utf8_text(content) else {
863 return;
864 };
865
866 let Some(call_context) = ast_graph.get_callable_context(node.id()) else {
868 return;
869 };
870
871 let callee_qualified = if callee_text.contains('.') {
876 callee_text.to_string()
878 } else if let Some(class_name) = &call_context.class_name {
879 format!("{class_name}.{callee_text}")
881 } else {
882 callee_text.to_string()
883 };
884
885 let caller_function_id = *node_map
887 .entry(call_context.qualified_name.clone())
888 .or_insert_with(|| helper.add_function(&call_context.qualified_name, None, false, false));
889
890 let target_function_id = *node_map
892 .entry(callee_qualified.clone())
893 .or_insert_with(|| helper.add_function(&callee_qualified, None, false, false));
894
895 let argument_count = count_call_arguments(node);
896 let call_span = Span::from_bytes(node.start_byte(), node.end_byte());
897 helper.add_call_edge_full_with_span(
898 caller_function_id,
899 target_function_id,
900 argument_count,
901 false,
902 vec![call_span],
903 );
904}
905
906fn process_object_creation(
907 node: Node,
908 content: &[u8],
909 ast_graph: &ASTGraph,
910 helper: &mut GraphBuildHelper,
911 node_map: &mut HashMap<String, NodeId>,
912) {
913 let Some(type_node) = node.child_by_field_name("type") else {
914 return;
915 };
916
917 let Ok(type_name) = type_node.utf8_text(content) else {
918 return;
919 };
920
921 let Some(call_context) = ast_graph.get_callable_context(node.id()) else {
923 return;
924 };
925
926 let callee_qualified = format!("{type_name}.ctor");
928
929 let caller_function_id = *node_map
931 .entry(call_context.qualified_name.clone())
932 .or_insert_with(|| helper.add_function(&call_context.qualified_name, None, false, false));
933
934 let target_function_id = *node_map
936 .entry(callee_qualified.clone())
937 .or_insert_with(|| helper.add_method(&callee_qualified, None, false, false));
938
939 let argument_count = count_call_arguments(node);
940 let call_span = Span::from_bytes(node.start_byte(), node.end_byte());
941 helper.add_call_edge_full_with_span(
942 caller_function_id,
943 target_function_id,
944 argument_count,
945 false,
946 vec![call_span],
947 );
948}
949
950fn count_call_arguments(call_node: Node<'_>) -> u8 {
951 let args_node = call_node
952 .child_by_field_name("arguments")
953 .or_else(|| call_node.child_by_field_name("argument_list"))
954 .or_else(|| {
955 let mut cursor = call_node.walk();
956 call_node
957 .children(&mut cursor)
958 .find(|child| child.kind() == "argument_list")
959 });
960
961 let Some(args_node) = args_node else {
962 return 255;
963 };
964
965 let count = args_node.named_child_count();
966 if count <= 254 {
967 u8::try_from(count).unwrap_or(u8::MAX)
968 } else {
969 u8::MAX
970 }
971}
972
973fn process_using_directive(node: Node, content: &[u8], helper: &mut GraphBuildHelper) {
1027 let is_static = node
1029 .children(&mut node.walk())
1030 .any(|child| child.kind() == "static");
1031
1032 let has_equals = node
1035 .children(&mut node.walk())
1036 .any(|child| child.kind() == "=");
1037
1038 let (alias, imported_name) = if has_equals {
1040 extract_aliased_using(node, content)
1042 } else {
1043 (None, extract_simple_using_target(node, content))
1045 };
1046
1047 let Some(imported_name) = imported_name else {
1048 return;
1049 };
1050
1051 let module_id = helper.add_module("<file>", None);
1053
1054 let span = Span::from_bytes(node.start_byte(), node.end_byte());
1056 let import_name = if is_static {
1057 format!("static {imported_name}")
1058 } else {
1059 imported_name.clone()
1060 };
1061 let imported_id = helper.add_import(&import_name, Some(span));
1062
1063 match (alias.as_deref(), is_static) {
1067 (Some(alias_str), _) => {
1068 helper.add_import_edge_full(module_id, imported_id, Some(alias_str), false);
1070 }
1071 (None, true) => {
1072 helper.add_import_edge_full(module_id, imported_id, None, true);
1075 }
1076 (None, false) => {
1077 helper.add_import_edge(module_id, imported_id);
1079 }
1080 }
1081}
1082
1083fn extract_aliased_using(node: Node, content: &[u8]) -> (Option<String>, Option<String>) {
1089 let mut alias: Option<String> = None;
1090 let mut target: Option<String> = None;
1091 let mut past_equals = false;
1092
1093 let mut cursor = node.walk();
1094 for child in node.children(&mut cursor) {
1095 let kind = child.kind();
1096
1097 if kind == "=" {
1098 past_equals = true;
1099 continue;
1100 }
1101
1102 if kind == "using" || kind == "static" || kind == ";" {
1104 continue;
1105 }
1106
1107 if past_equals {
1108 if matches!(kind, "identifier" | "qualified_name") && target.is_none() {
1110 target = child
1111 .utf8_text(content)
1112 .ok()
1113 .map(std::string::ToString::to_string);
1114 }
1115 } else if kind == "identifier" && alias.is_none() {
1116 alias = child
1118 .utf8_text(content)
1119 .ok()
1120 .map(std::string::ToString::to_string);
1121 }
1122 }
1123
1124 (alias, target)
1125}
1126
1127fn extract_simple_using_target(node: Node, content: &[u8]) -> Option<String> {
1132 let mut cursor = node.walk();
1133
1134 for child in node.children(&mut cursor) {
1135 let kind = child.kind();
1136
1137 if kind == "using" || kind == "static" || kind == ";" || kind == "=" {
1139 continue;
1140 }
1141
1142 if matches!(kind, "identifier" | "identifier_name" | "qualified_name") {
1144 return child
1145 .utf8_text(content)
1146 .ok()
1147 .map(std::string::ToString::to_string);
1148 }
1149 }
1150
1151 node.child_by_field_name("name")
1153 .and_then(|n| n.utf8_text(content).ok())
1154 .map(std::string::ToString::to_string)
1155}
1156
1157fn process_class_declaration(
1179 node: Node,
1180 content: &[u8],
1181 helper: &mut GraphBuildHelper,
1182 node_map: &mut HashMap<String, NodeId>,
1183 qualified_class_name: &str,
1184 namespace_stack: &[String],
1185) {
1186 let class_id = *node_map
1188 .entry(qualified_class_name.to_string())
1189 .or_insert_with(|| helper.add_class(qualified_class_name, None));
1190
1191 let mut cursor = node.walk();
1193 let base_list = node
1194 .children(&mut cursor)
1195 .find(|child| child.kind() == "base_list");
1196
1197 let Some(base_list) = base_list else {
1198 return;
1199 };
1200
1201 let mut first_base_class = true;
1203
1204 let mut base_cursor = base_list.walk();
1206 for base_child in base_list.children(&mut base_cursor) {
1207 let base_type_name = match base_child.kind() {
1208 "identifier" | "identifier_name" | "type_identifier" | "qualified_name" => base_child
1209 .utf8_text(content)
1210 .ok()
1211 .map(std::string::ToString::to_string),
1212 "generic_name" => {
1213 base_child
1215 .child_by_field_name("name")
1216 .or_else(|| base_child.child(0))
1217 .and_then(|n| n.utf8_text(content).ok())
1218 .map(std::string::ToString::to_string)
1219 }
1220 _ => None,
1221 };
1222
1223 let Some(base_name) = base_type_name else {
1224 continue;
1225 };
1226
1227 let qualified_base_name = qualify_type_name(&base_name, namespace_stack);
1230
1231 let is_interface = is_interface_name(&base_name);
1235
1236 if is_interface {
1237 let interface_id = *node_map
1239 .entry(qualified_base_name.clone())
1240 .or_insert_with(|| helper.add_interface(&qualified_base_name, None));
1241 helper.add_implements_edge(class_id, interface_id);
1242 } else if first_base_class {
1243 let parent_id = *node_map
1245 .entry(qualified_base_name.clone())
1246 .or_insert_with(|| helper.add_class(&qualified_base_name, None));
1247 helper.add_inherits_edge(class_id, parent_id);
1248 first_base_class = false;
1249 }
1250 }
1252}
1253
1254fn process_interface_declaration(
1271 node: Node,
1272 content: &[u8],
1273 helper: &mut GraphBuildHelper,
1274 node_map: &mut HashMap<String, NodeId>,
1275 qualified_interface_name: &str,
1276 namespace_stack: &[String],
1277) {
1278 let interface_id = *node_map
1280 .entry(qualified_interface_name.to_string())
1281 .or_insert_with(|| helper.add_interface(qualified_interface_name, None));
1282
1283 let mut cursor = node.walk();
1285 let base_list = node
1286 .children(&mut cursor)
1287 .find(|child| child.kind() == "base_list");
1288
1289 let Some(base_list) = base_list else {
1290 return;
1291 };
1292
1293 let mut base_cursor = base_list.walk();
1295 for base_child in base_list.children(&mut base_cursor) {
1296 let parent_name = match base_child.kind() {
1297 "identifier" | "identifier_name" | "type_identifier" | "qualified_name" => base_child
1298 .utf8_text(content)
1299 .ok()
1300 .map(std::string::ToString::to_string),
1301 "generic_name" => base_child
1302 .child_by_field_name("name")
1303 .or_else(|| base_child.child(0))
1304 .and_then(|n| n.utf8_text(content).ok())
1305 .map(std::string::ToString::to_string),
1306 _ => None,
1307 };
1308
1309 let Some(parent_name) = parent_name else {
1310 continue;
1311 };
1312
1313 let qualified_parent_name = qualify_type_name(&parent_name, namespace_stack);
1315
1316 let parent_id = *node_map
1318 .entry(qualified_parent_name.clone())
1319 .or_insert_with(|| helper.add_interface(&qualified_parent_name, None));
1320 helper.add_inherits_edge(interface_id, parent_id);
1321 }
1322}
1323
1324fn is_interface_name(name: &str) -> bool {
1330 let chars: Vec<char> = name.chars().collect();
1331 if chars.len() >= 2 {
1332 chars[0] == 'I' && chars[1].is_ascii_uppercase()
1334 } else {
1335 false
1336 }
1337}
1338
1339fn is_public(node: Node, content: &[u8]) -> bool {
1345 has_visibility_modifier(node, content, "public")
1346}
1347
1348fn is_internal(node: Node, content: &[u8]) -> bool {
1351 has_visibility_modifier(node, content, "internal")
1352}
1353
1354fn is_private(node: Node, content: &[u8]) -> bool {
1356 has_visibility_modifier(node, content, "private")
1357}
1358
1359#[allow(dead_code)] fn is_protected(node: Node, content: &[u8]) -> bool {
1362 has_visibility_modifier(node, content, "protected")
1363}
1364
1365fn has_visibility_modifier(node: Node, content: &[u8], modifier: &str) -> bool {
1367 node.children(&mut node.walk())
1368 .any(|child| child.kind() == modifier || child.utf8_text(content).unwrap_or("") == modifier)
1369}
1370
1371fn has_modifier(node: Node, content: &[u8], modifier: &str) -> bool {
1372 node.children(&mut node.walk())
1373 .any(|child| child.kind() == modifier || child.utf8_text(content).unwrap_or("") == modifier)
1374}
1375
1376fn should_export(node: Node, content: &[u8]) -> bool {
1382 is_public(node, content) || is_internal(node, content)
1383}
1384
1385fn export_from_file_module(helper: &mut GraphBuildHelper, exported: NodeId) {
1387 let module_id = helper.add_module(FILE_MODULE_NAME, None);
1388 helper.add_export_edge(module_id, exported);
1389}
1390
1391fn process_class_member_exports(
1393 body_node: Node,
1394 content: &[u8],
1395 class_qualified_name: &str,
1396 helper: &mut GraphBuildHelper,
1397 node_map: &mut HashMap<String, NodeId>,
1398) {
1399 let mut cursor = body_node.walk();
1400 for child in body_node.children(&mut cursor) {
1401 match child.kind() {
1402 "method_declaration" | "constructor_declaration" => {
1403 if should_export(child, content)
1405 && let Some(name_node) = child.child_by_field_name("name")
1406 && let Ok(method_name) = name_node.utf8_text(content)
1407 {
1408 let qualified_name = format!("{class_qualified_name}.{method_name}");
1409 if let Some(method_id) = node_map.get(&qualified_name) {
1411 export_from_file_module(helper, *method_id);
1412 }
1413 } else if should_export(child, content) && child.kind() == "constructor_declaration"
1414 {
1415 let class_name = class_qualified_name
1417 .rsplit('.')
1418 .next()
1419 .unwrap_or(class_qualified_name);
1420 let qualified_name = format!("{class_qualified_name}.{class_name}");
1421 if let Some(method_id) = node_map.get(&qualified_name) {
1422 export_from_file_module(helper, *method_id);
1423 }
1424 }
1425 }
1426 "field_declaration" | "property_declaration" => {
1427 if should_export(child, content) {
1433 let is_property = child.kind() == "property_declaration";
1434 let is_const = !is_property && has_modifier(child, content, "const");
1435 let is_readonly = !is_property && has_modifier(child, content, "readonly");
1436 let is_static = is_const || has_modifier(child, content, "static");
1437 let visibility = extract_field_visibility(child, content);
1438 let get_only = is_property && is_get_only_property(child);
1439 let emit_constant = is_const || is_readonly || get_only;
1440
1441 let mut field_cursor = child.walk();
1443 for field_child in child.children(&mut field_cursor) {
1444 if field_child.kind() == "variable_declarator"
1445 && let Some(name_node) = field_child.child_by_field_name("name")
1446 && let Ok(field_name) = name_node.utf8_text(content)
1447 {
1448 let qualified_name = format!("{class_qualified_name}.{field_name}");
1449 let span =
1450 Span::from_bytes(field_child.start_byte(), field_child.end_byte());
1451
1452 let field_id = if emit_constant {
1453 helper.add_constant_with_static_and_visibility(
1454 &qualified_name,
1455 Some(span),
1456 is_static,
1457 Some(visibility),
1458 )
1459 } else {
1460 helper.add_property_with_static_and_visibility(
1461 &qualified_name,
1462 Some(span),
1463 is_static,
1464 Some(visibility),
1465 )
1466 };
1467 export_from_file_module(helper, field_id);
1468 } else if field_child.kind() == "identifier"
1469 && let Ok(prop_name) = field_child.utf8_text(content)
1470 {
1471 let qualified_name = format!("{class_qualified_name}.{prop_name}");
1473 let span = Span::from_bytes(child.start_byte(), child.end_byte());
1474
1475 let prop_id = if emit_constant {
1476 helper.add_constant_with_static_and_visibility(
1477 &qualified_name,
1478 Some(span),
1479 is_static,
1480 Some(visibility),
1481 )
1482 } else {
1483 helper.add_property_with_static_and_visibility(
1484 &qualified_name,
1485 Some(span),
1486 is_static,
1487 Some(visibility),
1488 )
1489 };
1490 export_from_file_module(helper, prop_id);
1491 }
1492 }
1493 }
1494 }
1495 _ => {}
1496 }
1497 }
1498}
1499
1500fn process_interface_member_exports(
1503 body_node: Node,
1504 content: &[u8],
1505 interface_qualified_name: &str,
1506 helper: &mut GraphBuildHelper,
1507 node_map: &mut HashMap<String, NodeId>,
1508) {
1509 let mut cursor = body_node.walk();
1510 for child in body_node.children(&mut cursor) {
1511 if child.kind() == "method_declaration"
1512 && !is_private(child, content)
1513 && let Some(name_node) = child.child_by_field_name("name")
1514 && let Ok(method_name) = name_node.utf8_text(content)
1515 {
1516 let qualified_name = format!("{interface_qualified_name}.{method_name}");
1518 if let Some(method_id) = node_map.get(&qualified_name) {
1520 export_from_file_module(helper, *method_id);
1521 }
1522 }
1523 }
1524}
1525
1526fn process_pinvoke_method(
1557 node: Node,
1558 content: &[u8],
1559 helper: &mut GraphBuildHelper,
1560 node_map: &mut HashMap<String, NodeId>,
1561 namespace_stack: &[String],
1562) {
1563 let has_extern = node
1565 .children(&mut node.walk())
1566 .any(|child| child.kind() == "extern");
1567
1568 if !has_extern {
1569 return;
1570 }
1571
1572 let mut cursor = node.walk();
1574 let attribute_list = node
1575 .children(&mut cursor)
1576 .find(|child| child.kind() == "attribute_list");
1577
1578 let Some(attribute_list) = attribute_list else {
1579 return;
1580 };
1581
1582 let (dll_name, calling_convention) = extract_dllimport_info(attribute_list, content);
1584
1585 let Some(dll_name) = dll_name else {
1586 return;
1587 };
1588
1589 let method_name = node
1591 .child_by_field_name("name")
1592 .and_then(|n| n.utf8_text(content).ok())
1593 .map(std::string::ToString::to_string);
1594
1595 let Some(method_name) = method_name else {
1596 return;
1597 };
1598
1599 let qualified_method = if namespace_stack.is_empty() {
1601 method_name.clone()
1602 } else {
1603 format!("{}.{}", namespace_stack.join("."), method_name)
1604 };
1605
1606 let method_span = Span::from_bytes(node.start_byte(), node.end_byte());
1608 let method_id = *node_map
1609 .entry(qualified_method.clone())
1610 .or_insert_with(|| helper.add_method(&qualified_method, Some(method_span), false, true));
1611
1612 let ffi_func_name = format!("ffi::{dll_name}::{method_name}");
1614 let ffi_func_id = *node_map
1615 .entry(ffi_func_name.clone())
1616 .or_insert_with(|| helper.add_function(&ffi_func_name, None, false, false));
1617
1618 let convention = match calling_convention.as_deref() {
1620 Some("CallingConvention.Cdecl" | "Cdecl") => FfiConvention::Cdecl,
1621 Some("CallingConvention.FastCall" | "FastCall") => FfiConvention::Fastcall,
1622 _ => FfiConvention::Stdcall,
1624 };
1625
1626 helper.add_ffi_edge(method_id, ffi_func_id, convention);
1628}
1629
1630fn extract_dllimport_info(
1634 attribute_list: Node,
1635 content: &[u8],
1636) -> (Option<String>, Option<String>) {
1637 let mut dll_name = None;
1638 let mut calling_convention = None;
1639
1640 let mut list_cursor = attribute_list.walk();
1641 for attr_child in attribute_list.children(&mut list_cursor) {
1642 if attr_child.kind() != "attribute" {
1643 continue;
1644 }
1645
1646 let attr_name = attr_child
1648 .child_by_field_name("name")
1649 .and_then(|n| n.utf8_text(content).ok());
1650
1651 let is_dllimport = attr_name.is_some_and(|name| {
1652 name == "DllImport" || name == "System.Runtime.InteropServices.DllImport"
1653 });
1654
1655 if !is_dllimport {
1656 continue;
1657 }
1658
1659 let mut attr_cursor = attr_child.walk();
1661 let arg_list = attr_child
1662 .children(&mut attr_cursor)
1663 .find(|child| child.kind() == "attribute_argument_list");
1664
1665 let Some(arg_list) = arg_list else {
1666 continue;
1667 };
1668
1669 let mut arg_cursor = arg_list.walk();
1670 for arg in arg_list.children(&mut arg_cursor) {
1671 if arg.kind() != "attribute_argument" {
1672 continue;
1673 }
1674
1675 if let Some(name_node) = arg.child_by_field_name("name")
1677 && let Ok(name) = name_node.utf8_text(content)
1678 {
1679 if name == "CallingConvention"
1680 && let Some(expr) = arg.child_by_field_name("expression")
1681 && let Ok(value) = expr.utf8_text(content)
1682 {
1683 calling_convention = Some(value.to_string());
1684 }
1685 continue;
1686 }
1687
1688 if dll_name.is_none() {
1690 let expr = arg.child_by_field_name("expression").or_else(|| {
1692 let mut c = arg.walk();
1694 arg.children(&mut c)
1695 .find(|child| child.kind() == "string_literal")
1696 });
1697
1698 if let Some(expr) = expr
1699 && let Ok(text) = expr.utf8_text(content)
1700 {
1701 let trimmed = text.trim();
1703 if (trimmed.starts_with('"') && trimmed.ends_with('"'))
1704 || (trimmed.starts_with('@') && trimmed.len() > 2)
1705 {
1706 let start = if trimmed.starts_with('@') { 2 } else { 1 };
1707 dll_name = Some(trimmed[start..trimmed.len() - 1].to_string());
1708 } else {
1709 dll_name = Some(trimmed.to_string());
1710 }
1711 }
1712 }
1713 }
1714 }
1715
1716 (dll_name, calling_convention)
1717}
1718
1719fn process_local_variables(
1743 node: Node,
1744 content: &[u8],
1745 helper: &mut GraphBuildHelper,
1746 _class_stack: &[String],
1747) {
1748 let mut cursor = node.walk();
1750 let var_decl = node
1751 .children(&mut cursor)
1752 .find(|child| child.kind() == "variable_declaration");
1753
1754 let Some(var_decl) = var_decl else {
1755 return;
1756 };
1757
1758 let type_node = var_decl.child_by_field_name("type");
1760 let Some(type_node) = type_node else {
1761 return;
1762 };
1763
1764 let type_text = extract_type_string(type_node, content);
1766 let Some(type_text) = type_text else {
1767 return;
1768 };
1769
1770 let all_type_names = extract_all_type_names_from_annotation(type_node, content);
1772
1773 let mut var_cursor = var_decl.walk();
1775 for child in var_decl.children(&mut var_cursor) {
1776 if child.kind() == "variable_declarator"
1777 && let Some(name_node) = child.child_by_field_name("name")
1778 && let Ok(var_name) = name_node.utf8_text(content)
1779 {
1780 let span = Span::from_bytes(child.start_byte(), child.end_byte());
1781
1782 let var_id = helper.add_variable(var_name, Some(span));
1784 helper.mark_definition(var_id);
1785
1786 let type_id = helper.add_type(&type_text, None);
1788 helper.add_typeof_edge_with_context(
1789 var_id,
1790 type_id,
1791 Some(TypeOfContext::Variable),
1792 None,
1793 Some(var_name),
1794 );
1795
1796 for type_name in &all_type_names {
1798 let ref_type_id = helper.add_type(type_name, None);
1799 helper.add_reference_edge(var_id, ref_type_id);
1800 }
1801 }
1802 }
1803}
1804
1805fn process_field_declaration(
1823 node: Node,
1824 content: &[u8],
1825 helper: &mut GraphBuildHelper,
1826 class_stack: &[String],
1827) {
1828 let decl_node = node
1830 .children(&mut node.walk())
1831 .find(|child| child.kind() == "variable_declaration");
1832
1833 let Some(decl_node) = decl_node else {
1834 return;
1835 };
1836
1837 let type_node = decl_node.child_by_field_name("type");
1839 let Some(type_node) = type_node else {
1840 return;
1841 };
1842
1843 let type_text = extract_type_string(type_node, content);
1845 let Some(type_text) = type_text else {
1846 return;
1847 };
1848
1849 let all_type_names = extract_all_type_names_from_annotation(type_node, content);
1851
1852 let class_name = class_stack.last().map_or("", String::as_str);
1854
1855 let mut var_cursor = decl_node.walk();
1857 for child in decl_node.children(&mut var_cursor) {
1858 if child.kind() == "variable_declarator"
1859 && let Some(name_node) = child.child_by_field_name("name")
1860 && let Ok(field_name) = name_node.utf8_text(content)
1861 {
1862 let qualified_name = if class_name.is_empty() {
1864 field_name.to_string()
1865 } else {
1866 format!("{class_name}.{field_name}")
1867 };
1868
1869 let span = Span::from_bytes(child.start_byte(), child.end_byte());
1870
1871 let is_const = has_modifier(node, content, "const");
1880 let is_readonly = has_modifier(node, content, "readonly");
1881 let is_static = is_const || has_modifier(node, content, "static");
1882 let visibility = extract_field_visibility(node, content);
1883
1884 let field_id = if is_const || is_readonly {
1885 helper.add_constant_with_static_and_visibility(
1886 &qualified_name,
1887 Some(span),
1888 is_static,
1889 Some(visibility),
1890 )
1891 } else {
1892 helper.add_property_with_static_and_visibility(
1893 &qualified_name,
1894 Some(span),
1895 is_static,
1896 Some(visibility),
1897 )
1898 };
1899
1900 let type_id = helper.add_type(&type_text, None);
1904 helper.add_typeof_edge_with_context(
1905 field_id,
1906 type_id,
1907 Some(TypeOfContext::Field),
1908 None,
1909 Some(field_name),
1910 );
1911
1912 for type_name in &all_type_names {
1914 let ref_type_id = helper.add_type(type_name, None);
1915 helper.add_reference_edge(field_id, ref_type_id);
1916 }
1917 }
1918 }
1919}
1920
1921fn process_property_declaration(
1938 node: Node,
1939 content: &[u8],
1940 helper: &mut GraphBuildHelper,
1941 class_stack: &[String],
1942) {
1943 let type_node = node.child_by_field_name("type");
1945 let Some(type_node) = type_node else {
1946 return;
1947 };
1948
1949 let type_text = extract_type_string(type_node, content);
1951 let Some(type_text) = type_text else {
1952 return;
1953 };
1954
1955 let all_type_names = extract_all_type_names_from_annotation(type_node, content);
1957
1958 let name_node = node.child_by_field_name("name");
1960 let Some(name_node) = name_node else {
1961 return;
1962 };
1963
1964 let Ok(prop_name) = name_node.utf8_text(content) else {
1965 return;
1966 };
1967
1968 let class_name = class_stack.last().map_or("", String::as_str);
1970
1971 let qualified_name = if class_name.is_empty() {
1973 prop_name.to_string()
1974 } else {
1975 format!("{class_name}.{prop_name}")
1976 };
1977
1978 let span = Span::from_bytes(node.start_byte(), node.end_byte());
1979
1980 let is_static = has_modifier(node, content, "static");
1994 let visibility = extract_field_visibility(node, content);
1995 let get_only = is_get_only_property(node);
1996
1997 let prop_id = if get_only {
1998 helper.add_constant_with_static_and_visibility(
1999 &qualified_name,
2000 Some(span),
2001 is_static,
2002 Some(visibility),
2003 )
2004 } else {
2005 helper.add_property_with_static_and_visibility(
2006 &qualified_name,
2007 Some(span),
2008 is_static,
2009 Some(visibility),
2010 )
2011 };
2012
2013 let type_id = helper.add_type(&type_text, None);
2018 helper.add_typeof_edge_with_context(
2019 prop_id,
2020 type_id,
2021 Some(TypeOfContext::Field),
2022 None,
2023 Some(prop_name),
2024 );
2025
2026 for type_name in &all_type_names {
2028 let ref_type_id = helper.add_type(type_name, None);
2029 helper.add_reference_edge(prop_id, ref_type_id);
2030 }
2031}
2032
2033fn extract_field_visibility(node: Node, content: &[u8]) -> &'static str {
2040 let has_protected = has_modifier(node, content, "protected");
2041 let has_internal = has_modifier(node, content, "internal");
2042 if has_protected && has_internal {
2043 "protected internal"
2044 } else if has_modifier(node, content, "public") {
2045 "public"
2046 } else if has_protected {
2047 "protected"
2048 } else if has_internal {
2049 "internal"
2050 } else if has_modifier(node, content, "private") {
2051 "private"
2052 } else {
2053 "private"
2055 }
2056}
2057
2058fn is_get_only_property(node: Node) -> bool {
2077 if node
2081 .children(&mut node.walk())
2082 .any(|child| child.kind() == "arrow_expression_clause")
2083 {
2084 return false;
2085 }
2086
2087 let Some(accessor_list) = node
2088 .children(&mut node.walk())
2089 .find(|child| child.kind() == "accessor_list")
2090 else {
2091 return false;
2094 };
2095
2096 let mut has_auto_get = false;
2097 let mut has_set_like = false;
2098 for accessor in accessor_list.children(&mut accessor_list.walk()) {
2099 if accessor.kind() != "accessor_declaration" {
2100 continue;
2101 }
2102
2103 let mut keyword: Option<&str> = None;
2109 let mut has_body = false;
2110 for tok in accessor.children(&mut accessor.walk()) {
2111 match tok.kind() {
2112 "get" | "set" | "init" => keyword = Some(tok.kind()),
2113 "block" | "arrow_expression_clause" => has_body = true,
2114 _ => {}
2115 }
2116 }
2117
2118 match keyword {
2119 Some("get") => {
2120 if has_body {
2121 return false;
2124 }
2125 has_auto_get = true;
2126 }
2127 Some("set" | "init") => has_set_like = true,
2128 _ => {}
2129 }
2130 }
2131
2132 has_auto_get && !has_set_like
2133}
2134fn process_method_parameters(
2151 node: Node,
2152 _method_name: &str,
2153 content: &[u8],
2154 helper: &mut GraphBuildHelper,
2155) {
2156 let Some(param_list) = node.child_by_field_name("parameter_list") else {
2158 return;
2159 };
2160
2161 let mut cursor = param_list.walk();
2163 let mut param_index: u16 = 0;
2164
2165 for child in param_list.children(&mut cursor) {
2167 if !child.is_named() {
2169 continue;
2170 }
2171 {
2173 let Some(name_node) = child.child_by_field_name("name") else {
2175 continue;
2176 };
2177 let Ok(param_name) = name_node.utf8_text(content) else {
2178 continue;
2179 };
2180
2181 let Some(type_node) = child.child_by_field_name("type") else {
2183 continue;
2184 };
2185
2186 let Some(type_text) = extract_type_string(type_node, content) else {
2188 continue;
2189 };
2190
2191 let all_type_names = extract_all_type_names_from_annotation(type_node, content);
2193
2194 let param_span = Span::from_bytes(child.start_byte(), child.end_byte());
2196 let param_id = helper.add_variable(param_name, Some(param_span));
2197 helper.mark_definition(param_id);
2198
2199 let type_id = helper.add_type(&type_text, None);
2201 helper.add_typeof_edge_with_context(
2202 param_id,
2203 type_id,
2204 Some(TypeOfContext::Parameter),
2205 Some(param_index),
2206 Some(param_name),
2207 );
2208
2209 for type_name in &all_type_names {
2211 let ref_type_id = helper.add_type(type_name, None);
2212 helper.add_reference_edge(param_id, ref_type_id);
2213 }
2214
2215 param_index += 1;
2216 }
2217 }
2218}
2219
2220fn process_method_return_type(
2235 node: Node,
2236 method_name: &str,
2237 content: &[u8],
2238 helper: &mut GraphBuildHelper,
2239) {
2240 let return_type_node = node
2242 .child_by_field_name("return_type")
2243 .or_else(|| node.child_by_field_name("returns"))
2244 .or_else(|| node.child_by_field_name("type"));
2245
2246 let Some(return_type_node) = return_type_node else {
2247 return;
2248 };
2249
2250 if let Ok(type_text) = return_type_node.utf8_text(content)
2252 && type_text.trim() == "void"
2253 {
2254 return;
2255 }
2256
2257 let Some(type_text) = extract_type_string(return_type_node, content) else {
2259 return;
2260 };
2261
2262 let all_type_names = extract_all_type_names_from_annotation(return_type_node, content);
2264
2265 let method_span = Span::from_bytes(node.start_byte(), node.end_byte());
2267 let method_id = helper.add_method(method_name, Some(method_span), false, false);
2268 helper.mark_definition(method_id);
2269
2270 let type_id = helper.add_type(&type_text, None);
2272 helper.add_typeof_edge_with_context(
2273 method_id,
2274 type_id,
2275 Some(TypeOfContext::Return),
2276 Some(0), Some(method_name),
2278 );
2279
2280 for type_name in &all_type_names {
2282 let ref_type_id = helper.add_type(type_name, None);
2283 helper.add_reference_edge(method_id, ref_type_id);
2284 }
2285}
2286
2287fn process_type_parameter_declarations(
2333 decl_node: Node,
2334 content: &[u8],
2335 parent_qualified_name: &str,
2336 helper: &mut GraphBuildHelper,
2337) {
2338 let mut decl_cursor = decl_node.walk();
2341 let Some(params_node) = decl_node
2342 .children(&mut decl_cursor)
2343 .find(|c| c.kind() == "type_parameter_list")
2344 else {
2345 return;
2346 };
2347
2348 let mut param_ids: HashMap<String, sqry_core::graph::unified::node::NodeId> = HashMap::new();
2351
2352 let mut params_cursor = params_node.walk();
2353 for param_node in params_node.children(&mut params_cursor) {
2354 if param_node.kind() != "type_parameter" {
2355 continue;
2356 }
2357
2358 let Some(name_node) = param_node.child_by_field_name("name") else {
2360 continue;
2361 };
2362 let Ok(param_name) = name_node.utf8_text(content) else {
2363 continue;
2364 };
2365
2366 let qualified_param = format!("{parent_qualified_name}.{param_name}");
2367 let span = Span::from_bytes(name_node.start_byte(), name_node.end_byte());
2368 let param_id = helper.add_type(&qualified_param, Some(span));
2372 helper.mark_definition(param_id);
2373 param_ids.insert(param_name.to_string(), param_id);
2374 }
2375
2376 let mut clause_cursor = decl_node.walk();
2380 for clause_node in decl_node.children(&mut clause_cursor) {
2381 if clause_node.kind() != "type_parameter_constraints_clause" {
2382 continue;
2383 }
2384 emit_type_parameter_constraint_clause(clause_node, content, ¶m_ids, helper);
2385 }
2386}
2387
2388fn emit_type_parameter_constraint_clause(
2403 clause_node: Node,
2404 content: &[u8],
2405 param_ids: &HashMap<String, sqry_core::graph::unified::node::NodeId>,
2406 helper: &mut GraphBuildHelper,
2407) {
2408 let mut cursor = clause_node.walk();
2412 let mut named_children = clause_node
2413 .children(&mut cursor)
2414 .filter(tree_sitter::Node::is_named);
2415
2416 let Some(param_id_node) = named_children.next() else {
2417 return;
2418 };
2419 if param_id_node.kind() != "identifier" {
2420 return;
2421 }
2422 let Ok(param_name) = param_id_node.utf8_text(content) else {
2423 return;
2424 };
2425 let Some(¶m_id) = param_ids.get(param_name) else {
2426 return;
2429 };
2430
2431 for constraint_node in named_children {
2432 if constraint_node.kind() != "type_parameter_constraint" {
2433 continue;
2434 }
2435 let Some(constraint_target_name) = extract_constraint_target_name(constraint_node, content)
2436 else {
2437 continue;
2438 };
2439 let constraint_id = helper.add_type(&constraint_target_name, None);
2440 helper.add_typeof_edge_with_context(
2441 param_id,
2442 constraint_id,
2443 Some(TypeOfContext::Constraint),
2444 None,
2445 None,
2446 );
2447 }
2448}
2449
2450fn extract_constraint_target_name(constraint_node: Node, content: &[u8]) -> Option<String> {
2468 let mut cursor = constraint_node.walk();
2471 let mut had_named_constructor = false;
2472 let mut keyword_token: Option<&'static str> = None;
2473
2474 for child in constraint_node.children(&mut cursor) {
2475 match child.kind() {
2476 "constructor_constraint" => {
2477 had_named_constructor = true;
2478 }
2479 "class" if !child.is_named() => {
2480 keyword_token = Some("class");
2481 }
2482 "struct" if !child.is_named() => {
2483 keyword_token = Some("struct");
2484 }
2485 "unmanaged" if !child.is_named() => {
2486 keyword_token = Some("unmanaged");
2487 }
2488 "notnull" if !child.is_named() => {
2489 keyword_token = Some("notnull");
2490 }
2491 _ => {}
2492 }
2493 }
2494
2495 if had_named_constructor {
2496 return Some("new()".to_string());
2497 }
2498 if let Some(kw) = keyword_token {
2499 return Some(kw.to_string());
2500 }
2501
2502 let type_node = constraint_node.child_by_field_name("type")?;
2505 Some(extract_constraint_base_type_name(type_node, content))
2506}
2507
2508fn extract_constraint_base_type_name(type_node: Node, content: &[u8]) -> String {
2517 match type_node.kind() {
2518 "generic_name" => {
2519 let mut cursor = type_node.walk();
2522 for child in type_node.children(&mut cursor) {
2523 if matches!(child.kind(), "identifier" | "qualified_name") {
2524 return extract_constraint_base_type_name(child, content);
2525 }
2526 }
2527 type_node.utf8_text(content).unwrap_or_default().to_string()
2528 }
2529 "qualified_name" => {
2530 let mut cursor = type_node.walk();
2535 let children: Vec<_> = type_node
2536 .children(&mut cursor)
2537 .filter(tree_sitter::Node::is_named)
2538 .collect();
2539 if let Some(last) = children.last()
2541 && last.kind() == "generic_name"
2542 {
2543 let prefix_segments: Vec<String> = children
2545 .iter()
2546 .take(children.len() - 1)
2547 .filter_map(|c| c.utf8_text(content).ok().map(str::to_string))
2548 .collect();
2549 let leaf_base = extract_constraint_base_type_name(*last, content);
2550 if prefix_segments.is_empty() {
2551 return leaf_base;
2552 }
2553 return format!("{}.{}", prefix_segments.join("."), leaf_base);
2554 }
2555 type_node.utf8_text(content).unwrap_or_default().to_string()
2556 }
2557 "nullable_type" => {
2558 let mut cursor = type_node.walk();
2561 for child in type_node.children(&mut cursor) {
2562 if child.is_named() {
2563 return extract_constraint_base_type_name(child, content);
2564 }
2565 }
2566 type_node.utf8_text(content).unwrap_or_default().to_string()
2567 }
2568 _ => type_node.utf8_text(content).unwrap_or_default().to_string(),
2569 }
2570}
2571
2572#[cfg(test)]
2573mod shape_tests {
2574 use super::*;
2575 use sqry_core::graph::unified::build::shape::{
2576 CfBucket, ShapeBudget, compute_shape_descriptor,
2577 };
2578
2579 const SAMPLE: &str = include_str!(concat!(
2580 env!("CARGO_MANIFEST_DIR"),
2581 "/../test-fixtures/shape/systems/sample.cs"
2582 ));
2583
2584 fn parse(src: &str) -> Tree {
2585 let mut parser = tree_sitter::Parser::new();
2586 parser
2587 .set_language(&tree_sitter_c_sharp::LANGUAGE.into())
2588 .expect("load C# grammar");
2589 parser.parse(src, None).expect("parse C# sample")
2590 }
2591
2592 fn first_of_kind<'a>(node: Node<'a>, kind: &str) -> Option<Node<'a>> {
2593 if node.kind() == kind {
2594 return Some(node);
2595 }
2596 let mut cursor = node.walk();
2597 for child in node.children(&mut cursor) {
2598 if let Some(found) = first_of_kind(child, kind) {
2599 return Some(found);
2600 }
2601 }
2602 None
2603 }
2604
2605 #[test]
2606 fn csharp_mapping_is_non_empty() {
2607 let mapping = csharp_shape_mapping();
2608 let lang: tree_sitter::Language = tree_sitter_c_sharp::LANGUAGE.into();
2609 let count = (0..lang.node_kind_count())
2610 .filter_map(|id| u16::try_from(id).ok())
2611 .filter(|id| mapping.cf_bucket(*id).is_some())
2612 .count();
2613 assert!(
2614 count > 0,
2615 "C# cf_bucket map should cover real control-flow kinds"
2616 );
2617 }
2618
2619 #[test]
2620 fn csharp_histogram_covers_control_flow() {
2621 let tree = parse(SAMPLE);
2622 let func = first_of_kind(tree.root_node(), "method_declaration")
2623 .expect("sample has a method_declaration");
2624 let desc = compute_shape_descriptor(
2625 func,
2626 SAMPLE.as_bytes(),
2627 csharp_shape_mapping(),
2628 &ShapeBudget::default(),
2629 );
2630 let h = &desc.cf_histogram;
2631 assert!(h[CfBucket::Branch.index()] >= 1, "branch present");
2632 assert!(h[CfBucket::Loop.index()] >= 1, "loop present");
2633 assert!(h[CfBucket::Match.index()] >= 1, "switch present");
2634 assert!(h[CfBucket::Call.index()] >= 1, "call present");
2635 assert!(h[CfBucket::Return.index()] >= 1, "return present");
2636 assert!(
2637 h[CfBucket::BreakContinue.index()] >= 1,
2638 "break/continue present"
2639 );
2640 assert!(h[CfBucket::Try.index()] >= 1, "try present");
2641 assert!(h[CfBucket::Catch.index()] >= 1, "catch present");
2642 assert!(h[CfBucket::Throw.index()] >= 1, "throw present");
2643 assert!(h[CfBucket::Closure.index()] >= 1, "lambda present");
2644 }
2645
2646 #[test]
2647 fn csharp_signature_shape_reads_params() {
2648 let tree = parse(SAMPLE);
2649 let func = first_of_kind(tree.root_node(), "method_declaration")
2650 .expect("sample has a method_declaration");
2651 let shape = csharp_shape_mapping().signature_shape(func, SAMPLE.as_bytes());
2652 assert_eq!(shape.arity_positional, 2, "two positional params");
2655 assert!(shape.has_defaults, "default value detected");
2656 assert!(shape.has_return_annotation, "C# return type slot present");
2657 }
2658}