Skip to main content

sqry_lang_csharp/relations/
graph_builder.rs

1//! C# `GraphBuilder` implementation for tier-2 graph coverage.
2//!
3//! Migrated to use unified `GraphBuildHelper` following Phase 2.
4//!
5//! # Supported Features
6//!
7//! - Function/method definitions
8//! - Class definitions
9//! - Interface definitions
10//! - Method definitions (including static methods)
11//! - Function calls
12//! - Method calls
13//! - Static method calls
14//! - Async/await detection
15//! - Namespace handling
16//! - Using directive imports (simple, qualified, static, aliased)
17//! - Class inheritance (Inherits edges)
18//! - Interface implementation (Implements edges)
19//! - Export edges (public and internal types/members)
20
21use 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
37/// File-level module name for exports.
38/// Distinct from `<module>` to avoid node kind collision in `GraphBuildHelper` cache.
39const FILE_MODULE_NAME: &str = "<file_module>";
40
41/// Graph builder for C# files.
42#[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        // Build AST context for O(1) function lookups
73        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        // Map qualified names to NodeIds for call edge creation
81        let mut node_map = HashMap::new();
82
83        // Phase 1: Create function/method/class/interface nodes
84        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                    // Use add_function_with_signature for returns: queries
91                    helper.add_function_with_signature(
92                        qualified_name,
93                        Some(span),
94                        is_async,
95                        false,
96                        None, // visibility
97                        context.return_type.as_deref(),
98                    )
99                }
100                ContextKind::Method {
101                    is_async,
102                    is_static,
103                } => {
104                    // Use add_method_with_signature for returns: queries
105                    helper.add_method_with_signature(
106                        qualified_name,
107                        Some(span),
108                        is_async,
109                        is_static,
110                        None, // visibility
111                        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        // Build local scope tree for variable reference resolution
121        let mut scope_tree = local_scopes::build(tree.root_node(), content)?;
122
123        // Phase 2: Walk the tree to find calls and OOP edges
124        // Track namespace and class context for qualified naming
125        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        // P/Invoke detection is now handled in build_graph() via process_pinvoke_method()
151        // This method is required by the trait interface
152        Ok(vec![])
153    }
154}
155
156// ============================================================================
157// AST Graph - tracks callable contexts (functions, methods, classes)
158// ============================================================================
159
160#[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 of the method/function (e.g., `Task<User>`, `void`)
175    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        // Create recursion guard
191        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)] // Central traversal; refactor after API stabilization.
229/// # Errors
230///
231/// Returns error if recursion depth exceeds the guard's limit.
232struct 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            // Build qualified class name
264            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            // Add class context
274            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            // Recurse into class body
284            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            // Build qualified interface name
303            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            // Add interface context
313            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            // Recurse into interface body
323            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            // Check if async
342            let is_async = has_modifier(node, context.content, "async");
343
344            // Check if static method
345            let is_static = has_modifier(node, context.content, "static");
346
347            // Extract return type (tree-sitter-c-sharp may use return_type, returns, or type)
348            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            // Build qualified function name
356            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            // Determine if this is a method (inside a class/interface)
363            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            // Associate all descendants with this context
385            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            // Recurse into function body to find nested functions
392            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            // Extract namespace name
403            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                // Recurse into namespace body
409                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            // Recurse into children for other node types
421            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// ============================================================================
451// Edge Building - calls, imports, inheritance, implements
452// ============================================================================
453
454/// Walk the AST tree to create edges (calls, imports, inheritance, implements)
455/// Tracks namespace and class context for qualified naming.
456#[allow(clippy::too_many_lines)] // Central traversal; refactor after API stabilization.
457fn 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            // Track namespace context
470            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                // Recurse into namespace body
476                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            // Track class context and process OOP edges
498            if let Some(name_node) = node.child_by_field_name("name")
499                && let Ok(class_name) = name_node.utf8_text(content)
500            {
501                // Build qualified class name
502                let qualified_class =
503                    build_qualified_name(namespace_stack, class_stack, class_name);
504                class_stack.push(class_name.to_string());
505
506                // Process OOP edges with qualified name, passing namespace context for base type resolution
507                process_class_declaration(
508                    node,
509                    content,
510                    helper,
511                    node_map,
512                    &qualified_class,
513                    namespace_stack,
514                );
515
516                // Export class if it has public or internal visibility
517                if should_export(node, content)
518                    && let Some(class_id) = node_map.get(&qualified_class)
519                {
520                    export_from_file_module(helper, *class_id);
521                }
522
523                // Recurse into class body to handle method exports
524                if let Some(body) = node.child_by_field_name("body") {
525                    process_class_member_exports(body, content, &qualified_class, helper, node_map);
526
527                    let mut cursor = body.walk();
528                    for child in body.children(&mut cursor) {
529                        walk_tree_for_edges(
530                            child,
531                            content,
532                            ast_graph,
533                            helper,
534                            node_map,
535                            namespace_stack,
536                            class_stack,
537                            scope_tree,
538                        )?;
539                    }
540                }
541
542                class_stack.pop();
543                return Ok(());
544            }
545        }
546        "interface_declaration" => {
547            // Track interface context and process OOP edges
548            if let Some(name_node) = node.child_by_field_name("name")
549                && let Ok(interface_name) = name_node.utf8_text(content)
550            {
551                // Build qualified interface name
552                let qualified_interface =
553                    build_qualified_name(namespace_stack, class_stack, interface_name);
554
555                // Process OOP edges with qualified name, passing namespace context for base type resolution
556                process_interface_declaration(
557                    node,
558                    content,
559                    helper,
560                    node_map,
561                    &qualified_interface,
562                    namespace_stack,
563                );
564
565                // Export interface if it has public or internal visibility
566                if should_export(node, content)
567                    && let Some(interface_id) = node_map.get(&qualified_interface)
568                {
569                    export_from_file_module(helper, *interface_id);
570                }
571
572                // Process interface method exports
573                if let Some(body) = node.child_by_field_name("body") {
574                    process_interface_member_exports(
575                        body,
576                        content,
577                        &qualified_interface,
578                        helper,
579                        node_map,
580                    );
581                }
582
583                // Interfaces don't contain classes, so no need to push to class_stack
584                return Ok(());
585            }
586        }
587        "invocation_expression" => {
588            process_invocation(node, content, ast_graph, helper, node_map);
589        }
590        "object_creation_expression" => {
591            process_object_creation(node, content, ast_graph, helper, node_map);
592        }
593        "using_directive" => {
594            process_using_directive(node, content, helper);
595        }
596        "method_declaration" => {
597            // Check for P/Invoke (DllImport attribute + extern modifier)
598            process_pinvoke_method(node, content, helper, node_map, namespace_stack);
599
600            // Process method parameters and return type for TypeOf edges
601            if let Some(name_node) = node.child_by_field_name("name")
602                && let Ok(method_name) = name_node.utf8_text(content)
603            {
604                // Build qualified method name with namespace and class context
605                // Must match format from extract_callable_context (scope_stack.join("."))
606                let mut scope_parts = namespace_stack.clone();
607                scope_parts.extend(class_stack.iter().cloned());
608
609                let qualified_name = if scope_parts.is_empty() {
610                    method_name.to_string()
611                } else {
612                    format!("{}.{}", scope_parts.join("."), method_name)
613                };
614
615                process_method_parameters(node, &qualified_name, content, helper);
616                process_method_return_type(node, &qualified_name, content, helper);
617            }
618        }
619        "local_declaration_statement" => {
620            // Process local variable declarations with TypeOf edges
621            process_local_variables(node, content, helper, class_stack);
622        }
623        "field_declaration" => {
624            // Process field declarations with TypeOf edges
625            process_field_declaration(node, content, helper, class_stack);
626        }
627        "property_declaration" => {
628            // Process property declarations with TypeOf edges
629            process_property_declaration(node, content, helper, class_stack);
630        }
631        "identifier" => {
632            local_scopes::handle_identifier_for_reference(node, content, scope_tree, helper);
633        }
634        _ => {}
635    }
636
637    // Recurse into children
638    let mut cursor = node.walk();
639    for child in node.children(&mut cursor) {
640        walk_tree_for_edges(
641            child,
642            content,
643            ast_graph,
644            helper,
645            node_map,
646            namespace_stack,
647            class_stack,
648            scope_tree,
649        )?;
650    }
651
652    Ok(())
653}
654
655/// Build a qualified name from namespace and class context.
656fn build_qualified_name(namespace_stack: &[String], class_stack: &[String], name: &str) -> String {
657    let mut parts = Vec::new();
658    parts.extend(namespace_stack.iter().cloned());
659    parts.extend(class_stack.iter().cloned());
660    parts.push(name.to_string());
661    parts.join(".")
662}
663
664/// Qualify a type name with namespace context if it's not already qualified.
665///
666/// For types that are already qualified (contain '.'), returns as-is.
667/// For unqualified types in a namespace, prefixes with the namespace.
668/// This is a best-effort heuristic - without full import resolution,
669/// we assume unqualified types in a namespace are from that namespace.
670fn qualify_type_name(type_name: &str, namespace_stack: &[String]) -> String {
671    // If already qualified (contains '.'), use as-is
672    if type_name.contains('.') {
673        return type_name.to_string();
674    }
675
676    // If no namespace context, use type name as-is
677    if namespace_stack.is_empty() {
678        return type_name.to_string();
679    }
680
681    // Prefix with namespace
682    format!("{}.{}", namespace_stack.join("."), type_name)
683}
684
685fn process_invocation(
686    node: Node,
687    content: &[u8],
688    ast_graph: &ASTGraph,
689    helper: &mut GraphBuildHelper,
690    node_map: &mut HashMap<String, NodeId>,
691) {
692    let Some(function_node) = node.child_by_field_name("function") else {
693        return;
694    };
695
696    let Ok(callee_text) = function_node.utf8_text(content) else {
697        return;
698    };
699
700    // Get the caller context
701    let Some(call_context) = ast_graph.get_callable_context(node.id()) else {
702        return;
703    };
704
705    // Handle different invocation patterns:
706    // - Simple: methodName()
707    // - Member access: obj.methodName()
708    // - Static: ClassName.methodName()
709    let callee_qualified = if callee_text.contains('.') {
710        // Handle member access or static calls
711        callee_text.to_string()
712    } else if let Some(class_name) = &call_context.class_name {
713        // For simple calls inside a class, resolve to ClassName.method
714        format!("{class_name}.{callee_text}")
715    } else {
716        callee_text.to_string()
717    };
718
719    // Get or create caller node
720    let caller_function_id = *node_map
721        .entry(call_context.qualified_name.clone())
722        .or_insert_with(|| helper.add_function(&call_context.qualified_name, None, false, false));
723
724    // Get or create callee node
725    let target_function_id = *node_map
726        .entry(callee_qualified.clone())
727        .or_insert_with(|| helper.add_function(&callee_qualified, None, false, false));
728
729    let argument_count = count_call_arguments(node);
730    let call_span = Span::from_bytes(node.start_byte(), node.end_byte());
731    helper.add_call_edge_full_with_span(
732        caller_function_id,
733        target_function_id,
734        argument_count,
735        false,
736        vec![call_span],
737    );
738}
739
740fn process_object_creation(
741    node: Node,
742    content: &[u8],
743    ast_graph: &ASTGraph,
744    helper: &mut GraphBuildHelper,
745    node_map: &mut HashMap<String, NodeId>,
746) {
747    let Some(type_node) = node.child_by_field_name("type") else {
748        return;
749    };
750
751    let Ok(type_name) = type_node.utf8_text(content) else {
752        return;
753    };
754
755    // Get the caller context
756    let Some(call_context) = ast_graph.get_callable_context(node.id()) else {
757        return;
758    };
759
760    // Treat constructor calls as calls to ClassName.ctor
761    let callee_qualified = format!("{type_name}.ctor");
762
763    // Get or create caller node
764    let caller_function_id = *node_map
765        .entry(call_context.qualified_name.clone())
766        .or_insert_with(|| helper.add_function(&call_context.qualified_name, None, false, false));
767
768    // Get or create callee node (constructor)
769    let target_function_id = *node_map
770        .entry(callee_qualified.clone())
771        .or_insert_with(|| helper.add_method(&callee_qualified, None, false, false));
772
773    let argument_count = count_call_arguments(node);
774    let call_span = Span::from_bytes(node.start_byte(), node.end_byte());
775    helper.add_call_edge_full_with_span(
776        caller_function_id,
777        target_function_id,
778        argument_count,
779        false,
780        vec![call_span],
781    );
782}
783
784fn count_call_arguments(call_node: Node<'_>) -> u8 {
785    let args_node = call_node
786        .child_by_field_name("arguments")
787        .or_else(|| call_node.child_by_field_name("argument_list"))
788        .or_else(|| {
789            let mut cursor = call_node.walk();
790            call_node
791                .children(&mut cursor)
792                .find(|child| child.kind() == "argument_list")
793        });
794
795    let Some(args_node) = args_node else {
796        return 255;
797    };
798
799    let count = args_node.named_child_count();
800    if count <= 254 {
801        u8::try_from(count).unwrap_or(u8::MAX)
802    } else {
803        u8::MAX
804    }
805}
806
807// ============================================================================
808// Import Processing - using directives
809// ============================================================================
810
811/// Process using directive to create Import edges.
812///
813/// Handles patterns like:
814/// - `using System;` - simple namespace import
815/// - `using System.Collections.Generic;` - qualified namespace import
816/// - `using static System.Math;` - static using (`is_wildcard`: true for all static members)
817/// - `using Alias = Namespace.Type;` - aliased using (populate alias field)
818///
819/// # tree-sitter-c-sharp AST structure
820///
821/// Simple using:
822/// ```text
823/// using_directive [0..13] "using System;"
824///   using [0..5] "using"
825///   identifier [6..12] "System"
826///   ; [12..13] ";"
827/// ```
828///
829/// Qualified using:
830/// ```text
831/// using_directive
832///   using
833///   qualified_name "System.Collections.Generic"
834///     identifier "System"
835///     . "."
836///     identifier "Collections"
837///     . "."
838///     identifier "Generic"
839///   ;
840/// ```
841///
842/// Static using:
843/// ```text
844/// using_directive
845///   using
846///   static
847///   qualified_name "System.Math"
848///   ;
849/// ```
850///
851/// Aliased using:
852/// ```text
853/// using_directive [0..21] "using IO = System.IO;"
854///   using [0..5] "using"
855///   identifier [6..8] "IO"          <- alias
856///   = [9..10] "="
857///   qualified_name [11..20] "System.IO"  <- target
858///   ; [20..21] ";"
859/// ```
860fn process_using_directive(node: Node, content: &[u8], helper: &mut GraphBuildHelper) {
861    // Check for static using modifier
862    let is_static = node
863        .children(&mut node.walk())
864        .any(|child| child.kind() == "static");
865
866    // Detect aliased using: pattern is "using <identifier> = <target>;"
867    // We look for an "=" child, which indicates aliased using
868    let has_equals = node
869        .children(&mut node.walk())
870        .any(|child| child.kind() == "=");
871
872    // Extract alias and target based on the structure
873    let (alias, imported_name) = if has_equals {
874        // Aliased using: first identifier is alias, qualified_name/identifier after "=" is target
875        extract_aliased_using(node, content)
876    } else {
877        // Simple or static using: first identifier/qualified_name is the target
878        (None, extract_simple_using_target(node, content))
879    };
880
881    let Some(imported_name) = imported_name else {
882        return;
883    };
884
885    // Create module node (represents the current file as the importing entity)
886    let module_id = helper.add_module("<file>", None);
887
888    // Create import node for the imported namespace/type
889    let span = Span::from_bytes(node.start_byte(), node.end_byte());
890    let import_name = if is_static {
891        format!("static {imported_name}")
892    } else {
893        imported_name.clone()
894    };
895    let imported_id = helper.add_import(&import_name, Some(span));
896
897    // Add import edge with appropriate metadata
898    // Static usings are wildcard imports (all static members are accessible)
899    // Aliased usings have an alias but are not wildcard
900    match (alias.as_deref(), is_static) {
901        (Some(alias_str), _) => {
902            // Aliased import: using IO = System.IO;
903            helper.add_import_edge_full(module_id, imported_id, Some(alias_str), false);
904        }
905        (None, true) => {
906            // Static import: using static System.Math;
907            // All static members are imported, so is_wildcard = true
908            helper.add_import_edge_full(module_id, imported_id, None, true);
909        }
910        (None, false) => {
911            // Simple import: using System;
912            helper.add_import_edge(module_id, imported_id);
913        }
914    }
915}
916
917/// Extract alias and target from an aliased using directive.
918///
919/// Structure: `using <alias> = <target>;`
920/// - The first identifier before "=" is the alias
921/// - The `identifier/qualified_name` after "=" is the target
922fn extract_aliased_using(node: Node, content: &[u8]) -> (Option<String>, Option<String>) {
923    let mut alias: Option<String> = None;
924    let mut target: Option<String> = None;
925    let mut past_equals = false;
926
927    let mut cursor = node.walk();
928    for child in node.children(&mut cursor) {
929        let kind = child.kind();
930
931        if kind == "=" {
932            past_equals = true;
933            continue;
934        }
935
936        // Skip keywords and punctuation
937        if kind == "using" || kind == "static" || kind == ";" {
938            continue;
939        }
940
941        if past_equals {
942            // After "=" - this is the target (identifier or qualified_name)
943            if matches!(kind, "identifier" | "qualified_name") && target.is_none() {
944                target = child
945                    .utf8_text(content)
946                    .ok()
947                    .map(std::string::ToString::to_string);
948            }
949        } else if kind == "identifier" && alias.is_none() {
950            // Before "=" - this is the alias (should be identifier)
951            alias = child
952                .utf8_text(content)
953                .ok()
954                .map(std::string::ToString::to_string);
955        }
956    }
957
958    (alias, target)
959}
960
961/// Extract the target from a simple or static using directive.
962///
963/// Structure: `using [static] <target>;`
964/// - The first `identifier/qualified_name` (after optional "static") is the target
965fn extract_simple_using_target(node: Node, content: &[u8]) -> Option<String> {
966    let mut cursor = node.walk();
967
968    for child in node.children(&mut cursor) {
969        let kind = child.kind();
970
971        // Skip keywords and punctuation
972        if kind == "using" || kind == "static" || kind == ";" || kind == "=" {
973            continue;
974        }
975
976        // First identifier or qualified_name is the target
977        if matches!(kind, "identifier" | "identifier_name" | "qualified_name") {
978            return child
979                .utf8_text(content)
980                .ok()
981                .map(std::string::ToString::to_string);
982        }
983    }
984
985    // Fallback: try to get via field name
986    node.child_by_field_name("name")
987        .and_then(|n| n.utf8_text(content).ok())
988        .map(std::string::ToString::to_string)
989}
990
991// ============================================================================
992// OOP Processing - Inheritance and Interface Implementation
993// ============================================================================
994
995/// Process class declaration to extract Inherits and Implements edges.
996///
997/// Handles patterns like:
998/// - `class Child : Parent` → Inherits edge
999/// - `class Foo : IBar` → Implements edge (I prefix convention)
1000/// - `class Foo : Parent, IBar, IBaz` → One Inherits + multiple Implements edges
1001///
1002/// tree-sitter-c-sharp structure:
1003/// ```text
1004/// class_declaration
1005///   class (keyword)
1006///   name: identifier
1007///   [base_list]
1008///     : (colon)
1009///     [type_identifier | generic_name | qualified_name]+
1010///   body: declaration_list
1011/// ```
1012fn process_class_declaration(
1013    node: Node,
1014    content: &[u8],
1015    helper: &mut GraphBuildHelper,
1016    node_map: &mut HashMap<String, NodeId>,
1017    qualified_class_name: &str,
1018    namespace_stack: &[String],
1019) {
1020    // Get or create class node using qualified name (same as Phase 1)
1021    let class_id = *node_map
1022        .entry(qualified_class_name.to_string())
1023        .or_insert_with(|| helper.add_class(qualified_class_name, None));
1024
1025    // Find the base_list node (contains inheritance and interface implementation)
1026    let mut cursor = node.walk();
1027    let base_list = node
1028        .children(&mut cursor)
1029        .find(|child| child.kind() == "base_list");
1030
1031    let Some(base_list) = base_list else {
1032        return;
1033    };
1034
1035    // Track whether we've seen a class inheritance (first non-interface type)
1036    let mut first_base_class = true;
1037
1038    // Process each type in the base list
1039    let mut base_cursor = base_list.walk();
1040    for base_child in base_list.children(&mut base_cursor) {
1041        let base_type_name = match base_child.kind() {
1042            "identifier" | "identifier_name" | "type_identifier" | "qualified_name" => base_child
1043                .utf8_text(content)
1044                .ok()
1045                .map(std::string::ToString::to_string),
1046            "generic_name" => {
1047                // Generic type like List<T> - extract the base type name
1048                base_child
1049                    .child_by_field_name("name")
1050                    .or_else(|| base_child.child(0))
1051                    .and_then(|n| n.utf8_text(content).ok())
1052                    .map(std::string::ToString::to_string)
1053            }
1054            _ => None,
1055        };
1056
1057        let Some(base_name) = base_type_name else {
1058            continue;
1059        };
1060
1061        // Qualify base type name if it's unqualified and we have namespace context
1062        // If already qualified (contains '.'), use as-is; otherwise prefix with namespace
1063        let qualified_base_name = qualify_type_name(&base_name, namespace_stack);
1064
1065        // Determine if this is an interface using base-list position semantics:
1066        // In C#, if a class has both a base class and interfaces, the base class must come first.
1067        // We also check the I* naming convention as a secondary heuristic.
1068        let is_interface = is_interface_name(&base_name);
1069
1070        if is_interface {
1071            // Create interface node and add Implements edge
1072            let interface_id = *node_map
1073                .entry(qualified_base_name.clone())
1074                .or_insert_with(|| helper.add_interface(&qualified_base_name, None));
1075            helper.add_implements_edge(class_id, interface_id);
1076        } else if first_base_class {
1077            // First non-interface type is the base class
1078            let parent_id = *node_map
1079                .entry(qualified_base_name.clone())
1080                .or_insert_with(|| helper.add_class(&qualified_base_name, None));
1081            helper.add_inherits_edge(class_id, parent_id);
1082            first_base_class = false;
1083        }
1084        // Note: C# only allows single class inheritance, so we only process the first class
1085    }
1086}
1087
1088/// Process interface declaration to extract Inherits edges for interface extension.
1089///
1090/// Handles patterns like:
1091/// - `interface IChild : IParent` → Inherits edge
1092/// - `interface IChild : IParent, IOther` → Multiple Inherits edges
1093///
1094/// tree-sitter-c-sharp structure:
1095/// ```text
1096/// interface_declaration
1097///   interface (keyword)
1098///   name: identifier
1099///   [base_list]
1100///     : (colon)
1101///     [identifier | qualified_name]+
1102///   body: declaration_list
1103/// ```
1104fn process_interface_declaration(
1105    node: Node,
1106    content: &[u8],
1107    helper: &mut GraphBuildHelper,
1108    node_map: &mut HashMap<String, NodeId>,
1109    qualified_interface_name: &str,
1110    namespace_stack: &[String],
1111) {
1112    // Get or create interface node using qualified name (same as Phase 1)
1113    let interface_id = *node_map
1114        .entry(qualified_interface_name.to_string())
1115        .or_insert_with(|| helper.add_interface(qualified_interface_name, None));
1116
1117    // Find the base_list node
1118    let mut cursor = node.walk();
1119    let base_list = node
1120        .children(&mut cursor)
1121        .find(|child| child.kind() == "base_list");
1122
1123    let Some(base_list) = base_list else {
1124        return;
1125    };
1126
1127    // Process each parent interface in the base list
1128    let mut base_cursor = base_list.walk();
1129    for base_child in base_list.children(&mut base_cursor) {
1130        let parent_name = match base_child.kind() {
1131            "identifier" | "identifier_name" | "type_identifier" | "qualified_name" => base_child
1132                .utf8_text(content)
1133                .ok()
1134                .map(std::string::ToString::to_string),
1135            "generic_name" => base_child
1136                .child_by_field_name("name")
1137                .or_else(|| base_child.child(0))
1138                .and_then(|n| n.utf8_text(content).ok())
1139                .map(std::string::ToString::to_string),
1140            _ => None,
1141        };
1142
1143        let Some(parent_name) = parent_name else {
1144            continue;
1145        };
1146
1147        // Qualify parent type name if it's unqualified and we have namespace context
1148        let qualified_parent_name = qualify_type_name(&parent_name, namespace_stack);
1149
1150        // All base types for interfaces are parent interfaces → Inherits edge
1151        let parent_id = *node_map
1152            .entry(qualified_parent_name.clone())
1153            .or_insert_with(|| helper.add_interface(&qualified_parent_name, None));
1154        helper.add_inherits_edge(interface_id, parent_id);
1155    }
1156}
1157
1158/// Determine if a type name is an interface based on C# naming convention.
1159///
1160/// In C#, interfaces by convention start with 'I' followed by an uppercase letter.
1161/// Examples: `IDisposable`, `IEnumerable`, `IRepository`
1162/// Counter-examples: Int32, Image, Item
1163fn is_interface_name(name: &str) -> bool {
1164    let chars: Vec<char> = name.chars().collect();
1165    if chars.len() >= 2 {
1166        // Must start with 'I' followed by uppercase letter
1167        chars[0] == 'I' && chars[1].is_ascii_uppercase()
1168    } else {
1169        false
1170    }
1171}
1172
1173// ============================================================================
1174// Visibility Detection for Export Edges
1175// ============================================================================
1176
1177/// Check if a node has the `public` visibility modifier.
1178fn is_public(node: Node, content: &[u8]) -> bool {
1179    has_visibility_modifier(node, content, "public")
1180}
1181
1182/// Check if a node has the `internal` visibility modifier.
1183/// In C#, internal members are accessible within the same assembly and should be exported.
1184fn is_internal(node: Node, content: &[u8]) -> bool {
1185    has_visibility_modifier(node, content, "internal")
1186}
1187
1188/// Check if a node has the `private` visibility modifier.
1189fn is_private(node: Node, content: &[u8]) -> bool {
1190    has_visibility_modifier(node, content, "private")
1191}
1192
1193/// Check if a node has the `protected` visibility modifier.
1194#[allow(dead_code)] // Reserved for potential future use
1195fn is_protected(node: Node, content: &[u8]) -> bool {
1196    has_visibility_modifier(node, content, "protected")
1197}
1198
1199/// Check if a node has a specific visibility modifier.
1200fn has_visibility_modifier(node: Node, content: &[u8], modifier: &str) -> bool {
1201    node.children(&mut node.walk())
1202        .any(|child| child.kind() == modifier || child.utf8_text(content).unwrap_or("") == modifier)
1203}
1204
1205fn has_modifier(node: Node, content: &[u8], modifier: &str) -> bool {
1206    node.children(&mut node.walk())
1207        .any(|child| child.kind() == modifier || child.utf8_text(content).unwrap_or("") == modifier)
1208}
1209
1210/// Check if a member should be exported (public or internal visibility).
1211/// In C#, both public and internal types/members are exported:
1212/// - public: accessible everywhere
1213/// - internal: accessible within the same assembly
1214/// - private and protected: NOT exported
1215fn should_export(node: Node, content: &[u8]) -> bool {
1216    is_public(node, content) || is_internal(node, content)
1217}
1218
1219/// Create an export edge from the file module to the exported node.
1220fn export_from_file_module(helper: &mut GraphBuildHelper, exported: NodeId) {
1221    let module_id = helper.add_module(FILE_MODULE_NAME, None);
1222    helper.add_export_edge(module_id, exported);
1223}
1224
1225/// Process public/internal methods and fields within a class body for export edges.
1226fn process_class_member_exports(
1227    body_node: Node,
1228    content: &[u8],
1229    class_qualified_name: &str,
1230    helper: &mut GraphBuildHelper,
1231    node_map: &mut HashMap<String, NodeId>,
1232) {
1233    let mut cursor = body_node.walk();
1234    for child in body_node.children(&mut cursor) {
1235        match child.kind() {
1236            "method_declaration" | "constructor_declaration" => {
1237                // Export method/constructor if it has public or internal visibility
1238                if should_export(child, content)
1239                    && let Some(name_node) = child.child_by_field_name("name")
1240                    && let Ok(method_name) = name_node.utf8_text(content)
1241                {
1242                    let qualified_name = format!("{class_qualified_name}.{method_name}");
1243                    // Get the method from node_map if it exists
1244                    if let Some(method_id) = node_map.get(&qualified_name) {
1245                        export_from_file_module(helper, *method_id);
1246                    }
1247                } else if should_export(child, content) && child.kind() == "constructor_declaration"
1248                {
1249                    // Constructors don't have a name field, use the class name
1250                    let class_name = class_qualified_name
1251                        .rsplit('.')
1252                        .next()
1253                        .unwrap_or(class_qualified_name);
1254                    let qualified_name = format!("{class_qualified_name}.{class_name}");
1255                    if let Some(method_id) = node_map.get(&qualified_name) {
1256                        export_from_file_module(helper, *method_id);
1257                    }
1258                }
1259            }
1260            "field_declaration" | "property_declaration" => {
1261                // Export field/property if it has public or internal visibility
1262                if should_export(child, content) {
1263                    // Fields and properties can have multiple declarators
1264                    let mut field_cursor = child.walk();
1265                    for field_child in child.children(&mut field_cursor) {
1266                        if field_child.kind() == "variable_declarator"
1267                            && let Some(name_node) = field_child.child_by_field_name("name")
1268                            && let Ok(field_name) = name_node.utf8_text(content)
1269                        {
1270                            let qualified_name = format!("{class_qualified_name}.{field_name}");
1271                            let span =
1272                                Span::from_bytes(field_child.start_byte(), field_child.end_byte());
1273
1274                            // Create variable node and export it
1275                            let field_id = helper.add_variable(&qualified_name, Some(span));
1276                            export_from_file_module(helper, field_id);
1277                        } else if field_child.kind() == "identifier"
1278                            && let Ok(prop_name) = field_child.utf8_text(content)
1279                        {
1280                            // Property name is directly an identifier
1281                            let qualified_name = format!("{class_qualified_name}.{prop_name}");
1282                            let span = Span::from_bytes(child.start_byte(), child.end_byte());
1283
1284                            // Create variable node for property and export it
1285                            let prop_id = helper.add_variable(&qualified_name, Some(span));
1286                            export_from_file_module(helper, prop_id);
1287                        }
1288                    }
1289                }
1290            }
1291            _ => {}
1292        }
1293    }
1294}
1295
1296/// Process interface method exports.
1297/// In C#, interface methods are implicitly public unless explicitly marked private (C# 8.0+).
1298fn process_interface_member_exports(
1299    body_node: Node,
1300    content: &[u8],
1301    interface_qualified_name: &str,
1302    helper: &mut GraphBuildHelper,
1303    node_map: &mut HashMap<String, NodeId>,
1304) {
1305    let mut cursor = body_node.walk();
1306    for child in body_node.children(&mut cursor) {
1307        if child.kind() == "method_declaration"
1308            && !is_private(child, content)
1309            && let Some(name_node) = child.child_by_field_name("name")
1310            && let Ok(method_name) = name_node.utf8_text(content)
1311        {
1312            // Interface methods are implicitly public unless explicitly private
1313            let qualified_name = format!("{interface_qualified_name}.{method_name}");
1314            // Get the method from node_map if it exists
1315            if let Some(method_id) = node_map.get(&qualified_name) {
1316                export_from_file_module(helper, *method_id);
1317            }
1318        }
1319    }
1320}
1321
1322// ============================================================================
1323// P/Invoke (FFI) Processing
1324// ============================================================================
1325
1326/// Process method declaration to detect P/Invoke (Platform Invocation Services).
1327///
1328/// P/Invoke pattern in C#:
1329/// ```csharp
1330/// [DllImport("user32.dll")]
1331/// static extern int MessageBox(IntPtr hWnd, string text, string caption, uint type);
1332///
1333/// [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
1334/// static extern bool Beep(uint frequency, uint duration);
1335/// ```
1336///
1337/// tree-sitter-c-sharp structure:
1338/// ```text
1339/// method_declaration
1340///   attribute_list
1341///     attribute
1342///       name: identifier ("DllImport")
1343///       argument_list
1344///         attribute_argument
1345///           expression: string_literal ("user32.dll")
1346///   modifier: static
1347///   modifier: extern
1348///   return_type: predefined_type
1349///   name: identifier
1350///   parameter_list
1351/// ```
1352fn process_pinvoke_method(
1353    node: Node,
1354    content: &[u8],
1355    helper: &mut GraphBuildHelper,
1356    node_map: &mut HashMap<String, NodeId>,
1357    namespace_stack: &[String],
1358) {
1359    // Check for extern modifier
1360    let has_extern = node
1361        .children(&mut node.walk())
1362        .any(|child| child.kind() == "extern");
1363
1364    if !has_extern {
1365        return;
1366    }
1367
1368    // Look for DllImport attribute
1369    let mut cursor = node.walk();
1370    let attribute_list = node
1371        .children(&mut cursor)
1372        .find(|child| child.kind() == "attribute_list");
1373
1374    let Some(attribute_list) = attribute_list else {
1375        return;
1376    };
1377
1378    // Find DllImport attribute and extract library name
1379    let (dll_name, calling_convention) = extract_dllimport_info(attribute_list, content);
1380
1381    let Some(dll_name) = dll_name else {
1382        return;
1383    };
1384
1385    // Extract method name
1386    let method_name = node
1387        .child_by_field_name("name")
1388        .and_then(|n| n.utf8_text(content).ok())
1389        .map(std::string::ToString::to_string);
1390
1391    let Some(method_name) = method_name else {
1392        return;
1393    };
1394
1395    // Build qualified method name
1396    let qualified_method = if namespace_stack.is_empty() {
1397        method_name.clone()
1398    } else {
1399        format!("{}.{}", namespace_stack.join("."), method_name)
1400    };
1401
1402    // Get or create method node (caller - the C# method declaration)
1403    let method_span = Span::from_bytes(node.start_byte(), node.end_byte());
1404    let method_id = *node_map
1405        .entry(qualified_method.clone())
1406        .or_insert_with(|| helper.add_method(&qualified_method, Some(method_span), false, true));
1407
1408    // Create FFI function node (the native function in the DLL)
1409    let ffi_func_name = format!("ffi::{dll_name}::{method_name}");
1410    let ffi_func_id = *node_map
1411        .entry(ffi_func_name.clone())
1412        .or_insert_with(|| helper.add_function(&ffi_func_name, None, false, false));
1413
1414    // Determine FFI convention from CallingConvention parameter
1415    let convention = match calling_convention.as_deref() {
1416        Some("CallingConvention.Cdecl" | "Cdecl") => FfiConvention::Cdecl,
1417        Some("CallingConvention.FastCall" | "FastCall") => FfiConvention::Fastcall,
1418        // Default Windows convention is StdCall for P/Invoke
1419        _ => FfiConvention::Stdcall,
1420    };
1421
1422    // Add FfiCall edge
1423    helper.add_ffi_edge(method_id, ffi_func_id, convention);
1424}
1425
1426/// Extract `DllImport` attribute information (library name and calling convention).
1427///
1428/// Returns (`dll_name`, `calling_convention`) tuple.
1429fn extract_dllimport_info(
1430    attribute_list: Node,
1431    content: &[u8],
1432) -> (Option<String>, Option<String>) {
1433    let mut dll_name = None;
1434    let mut calling_convention = None;
1435
1436    let mut list_cursor = attribute_list.walk();
1437    for attr_child in attribute_list.children(&mut list_cursor) {
1438        if attr_child.kind() != "attribute" {
1439            continue;
1440        }
1441
1442        // Check if this is DllImport attribute
1443        let attr_name = attr_child
1444            .child_by_field_name("name")
1445            .and_then(|n| n.utf8_text(content).ok());
1446
1447        let is_dllimport = attr_name.is_some_and(|name| {
1448            name == "DllImport" || name == "System.Runtime.InteropServices.DllImport"
1449        });
1450
1451        if !is_dllimport {
1452            continue;
1453        }
1454
1455        // Extract arguments
1456        let mut attr_cursor = attr_child.walk();
1457        let arg_list = attr_child
1458            .children(&mut attr_cursor)
1459            .find(|child| child.kind() == "attribute_argument_list");
1460
1461        let Some(arg_list) = arg_list else {
1462            continue;
1463        };
1464
1465        let mut arg_cursor = arg_list.walk();
1466        for arg in arg_list.children(&mut arg_cursor) {
1467            if arg.kind() != "attribute_argument" {
1468                continue;
1469            }
1470
1471            // Check for named argument (CallingConvention = ...)
1472            if let Some(name_node) = arg.child_by_field_name("name")
1473                && let Ok(name) = name_node.utf8_text(content)
1474            {
1475                if name == "CallingConvention"
1476                    && let Some(expr) = arg.child_by_field_name("expression")
1477                    && let Ok(value) = expr.utf8_text(content)
1478                {
1479                    calling_convention = Some(value.to_string());
1480                }
1481                continue;
1482            }
1483
1484            // Positional argument - first one is the DLL name
1485            if dll_name.is_none() {
1486                // Find the string literal
1487                let expr = arg.child_by_field_name("expression").or_else(|| {
1488                    // Sometimes the string is a direct child
1489                    let mut c = arg.walk();
1490                    arg.children(&mut c)
1491                        .find(|child| child.kind() == "string_literal")
1492                });
1493
1494                if let Some(expr) = expr
1495                    && let Ok(text) = expr.utf8_text(content)
1496                {
1497                    // Remove quotes from string literal
1498                    let trimmed = text.trim();
1499                    if (trimmed.starts_with('"') && trimmed.ends_with('"'))
1500                        || (trimmed.starts_with('@') && trimmed.len() > 2)
1501                    {
1502                        let start = if trimmed.starts_with('@') { 2 } else { 1 };
1503                        dll_name = Some(trimmed[start..trimmed.len() - 1].to_string());
1504                    } else {
1505                        dll_name = Some(trimmed.to_string());
1506                    }
1507                }
1508            }
1509        }
1510    }
1511
1512    (dll_name, calling_convention)
1513}
1514
1515// ============================================================================
1516// TypeOf and Reference Edge Processing
1517// ============================================================================
1518
1519/// Process local variable declarations to create `TypeOf` and Reference edges.
1520///
1521/// Handles patterns like:
1522/// - `int x = 5;`
1523/// - `string name = "test";`
1524/// - `int a = 1, b = 2;` (multiple declarators)
1525/// - `List<User> users = new List<User>();` (generics - extract base type)
1526/// - `int? count = null;` (nullable - extract base type)
1527/// - `int[] numbers = new int[5];` (arrays - extract base type)
1528///
1529/// tree-sitter-c-sharp structure:
1530/// ```text
1531/// local_declaration_statement
1532///   variable_declaration
1533///     type: predefined_type | identifier | generic_name | nullable_type | array_type
1534///     variable_declarator
1535///       name: identifier
1536///       initializer: [expression]
1537/// ```
1538fn process_local_variables(
1539    node: Node,
1540    content: &[u8],
1541    helper: &mut GraphBuildHelper,
1542    _class_stack: &[String],
1543) {
1544    // Get variable_declaration child
1545    let mut cursor = node.walk();
1546    let var_decl = node
1547        .children(&mut cursor)
1548        .find(|child| child.kind() == "variable_declaration");
1549
1550    let Some(var_decl) = var_decl else {
1551        return;
1552    };
1553
1554    // Extract type
1555    let type_node = var_decl.child_by_field_name("type");
1556    let Some(type_node) = type_node else {
1557        return;
1558    };
1559
1560    // Extract full type signature for TypeOf edge
1561    let type_text = extract_type_string(type_node, content);
1562    let Some(type_text) = type_text else {
1563        return;
1564    };
1565
1566    // Extract all type names for Reference edges
1567    let all_type_names = extract_all_type_names_from_annotation(type_node, content);
1568
1569    // Process all variable declarators (may be multiple: int a = 1, b = 2;)
1570    let mut var_cursor = var_decl.walk();
1571    for child in var_decl.children(&mut var_cursor) {
1572        if child.kind() == "variable_declarator"
1573            && let Some(name_node) = child.child_by_field_name("name")
1574            && let Ok(var_name) = name_node.utf8_text(content)
1575        {
1576            let span = Span::from_bytes(child.start_byte(), child.end_byte());
1577
1578            // Create variable node
1579            let var_id = helper.add_variable(var_name, Some(span));
1580
1581            // Create TypeOf edge with full type signature and Variable context
1582            let type_id = helper.add_type(&type_text, None);
1583            helper.add_typeof_edge_with_context(
1584                var_id,
1585                type_id,
1586                Some(TypeOfContext::Variable),
1587                None,
1588                Some(var_name),
1589            );
1590
1591            // Create Reference edges for all nested types
1592            for type_name in &all_type_names {
1593                let ref_type_id = helper.add_type(type_name, None);
1594                helper.add_reference_edge(var_id, ref_type_id);
1595            }
1596        }
1597    }
1598}
1599
1600/// Process field declarations to create `TypeOf` and Reference edges.
1601///
1602/// Handles patterns like:
1603/// - `private UserRepository repository;`
1604/// - `private int age;`
1605/// - `public List<User> users;` (generics - extract base type)
1606///
1607/// tree-sitter-c-sharp structure:
1608/// ```text
1609/// field_declaration
1610///   modifiers: [...]
1611///   declaration:
1612///     type: predefined_type | identifier | generic_name
1613///     variable_declarator
1614///       name: identifier
1615///       initializer: [expression]
1616/// ```
1617fn process_field_declaration(
1618    node: Node,
1619    content: &[u8],
1620    helper: &mut GraphBuildHelper,
1621    class_stack: &[String],
1622) {
1623    // Get the declaration child (variable_declaration)
1624    let decl_node = node
1625        .children(&mut node.walk())
1626        .find(|child| child.kind() == "variable_declaration");
1627
1628    let Some(decl_node) = decl_node else {
1629        return;
1630    };
1631
1632    // Extract type
1633    let type_node = decl_node.child_by_field_name("type");
1634    let Some(type_node) = type_node else {
1635        return;
1636    };
1637
1638    // Extract full type signature for TypeOf edge
1639    let type_text = extract_type_string(type_node, content);
1640    let Some(type_text) = type_text else {
1641        return;
1642    };
1643
1644    // Extract all type names for Reference edges
1645    let all_type_names = extract_all_type_names_from_annotation(type_node, content);
1646
1647    // Get the containing class name
1648    let class_name = class_stack.last().map_or("", String::as_str);
1649
1650    // Process all variable declarators
1651    let mut var_cursor = decl_node.walk();
1652    for child in decl_node.children(&mut var_cursor) {
1653        if child.kind() == "variable_declarator"
1654            && let Some(name_node) = child.child_by_field_name("name")
1655            && let Ok(field_name) = name_node.utf8_text(content)
1656        {
1657            // Build qualified field name
1658            let qualified_name = if class_name.is_empty() {
1659                field_name.to_string()
1660            } else {
1661                format!("{class_name}.{field_name}")
1662            };
1663
1664            let span = Span::from_bytes(child.start_byte(), child.end_byte());
1665
1666            // Create variable node for the field
1667            let field_id = helper.add_variable(&qualified_name, Some(span));
1668
1669            // Create TypeOf edge with full type signature and Field context
1670            let type_id = helper.add_type(&type_text, None);
1671            helper.add_typeof_edge_with_context(
1672                field_id,
1673                type_id,
1674                Some(TypeOfContext::Field),
1675                None,
1676                Some(&qualified_name),
1677            );
1678
1679            // Create Reference edges for all nested types
1680            for type_name in &all_type_names {
1681                let ref_type_id = helper.add_type(type_name, None);
1682                helper.add_reference_edge(field_id, ref_type_id);
1683            }
1684        }
1685    }
1686}
1687
1688/// Process property declarations to create `TypeOf` and Reference edges.
1689///
1690/// Handles patterns like:
1691/// - `public int Age { get; set; }` (auto-property)
1692/// - `public string Name { get; }` (read-only property)
1693/// - `public bool IsAdult { get { return age >= 18; } }` (computed property)
1694/// - `public List<User> Users { get; set; }` (generic property)
1695///
1696/// tree-sitter-c-sharp structure:
1697/// ```text
1698/// property_declaration
1699///   modifiers: [...]
1700///   type: predefined_type | identifier | generic_name
1701///   name: identifier
1702///   accessor_list: { get; set; }
1703/// ```
1704fn process_property_declaration(
1705    node: Node,
1706    content: &[u8],
1707    helper: &mut GraphBuildHelper,
1708    class_stack: &[String],
1709) {
1710    // Extract type
1711    let type_node = node.child_by_field_name("type");
1712    let Some(type_node) = type_node else {
1713        return;
1714    };
1715
1716    // Extract full type signature for TypeOf edge
1717    let type_text = extract_type_string(type_node, content);
1718    let Some(type_text) = type_text else {
1719        return;
1720    };
1721
1722    // Extract all type names for Reference edges
1723    let all_type_names = extract_all_type_names_from_annotation(type_node, content);
1724
1725    // Extract property name
1726    let name_node = node.child_by_field_name("name");
1727    let Some(name_node) = name_node else {
1728        return;
1729    };
1730
1731    let Ok(prop_name) = name_node.utf8_text(content) else {
1732        return;
1733    };
1734
1735    // Get the containing class name
1736    let class_name = class_stack.last().map_or("", String::as_str);
1737
1738    // Build qualified property name
1739    let qualified_name = if class_name.is_empty() {
1740        prop_name.to_string()
1741    } else {
1742        format!("{class_name}.{prop_name}")
1743    };
1744
1745    let span = Span::from_bytes(node.start_byte(), node.end_byte());
1746
1747    // Create variable node for the property
1748    let prop_id = helper.add_variable(&qualified_name, Some(span));
1749
1750    // Create TypeOf edge with full type signature and Field context
1751    // (Properties are treated as fields for TypeOf context)
1752    let type_id = helper.add_type(&type_text, None);
1753    helper.add_typeof_edge_with_context(
1754        prop_id,
1755        type_id,
1756        Some(TypeOfContext::Field),
1757        None,
1758        Some(&qualified_name),
1759    );
1760
1761    // Create Reference edges for all nested types
1762    for type_name in &all_type_names {
1763        let ref_type_id = helper.add_type(type_name, None);
1764        helper.add_reference_edge(prop_id, ref_type_id);
1765    }
1766}
1767/// Process method parameters to create `TypeOf` and Reference edges.
1768///
1769/// Handles patterns like:
1770/// - `void Method(int count, string name)`
1771/// - `User Process(Repository repo, List<Item> items)`
1772///
1773/// tree-sitter-c-sharp structure:
1774/// ```text
1775/// method_declaration
1776///   return_type: predefined_type | identifier | generic_name
1777///   name: identifier
1778///   parameter_list
1779///     parameter
1780///       type: predefined_type | identifier | generic_name
1781///       name: identifier
1782/// ```
1783fn process_method_parameters(
1784    node: Node,
1785    _method_name: &str,
1786    content: &[u8],
1787    helper: &mut GraphBuildHelper,
1788) {
1789    // Get parameter_list
1790    let Some(param_list) = node.child_by_field_name("parameter_list") else {
1791        return;
1792    };
1793
1794    // Process each parameter
1795    let mut cursor = param_list.walk();
1796    let mut param_index: u16 = 0;
1797
1798    // Iterate through children - tree-sitter-c-sharp may use various node kinds for parameters
1799    for child in param_list.children(&mut cursor) {
1800        // Skip punctuation nodes (parentheses, commas)
1801        if !child.is_named() {
1802            continue;
1803        }
1804        // Most parameter-like nodes should be processed
1805        {
1806            // Extract parameter name
1807            let Some(name_node) = child.child_by_field_name("name") else {
1808                continue;
1809            };
1810            let Ok(param_name) = name_node.utf8_text(content) else {
1811                continue;
1812            };
1813
1814            // Extract parameter type
1815            let Some(type_node) = child.child_by_field_name("type") else {
1816                continue;
1817            };
1818
1819            // Extract full type signature for TypeOf edge
1820            let Some(type_text) = extract_type_string(type_node, content) else {
1821                continue;
1822            };
1823
1824            // Extract all type names for Reference edges
1825            let all_type_names = extract_all_type_names_from_annotation(type_node, content);
1826
1827            // Create parameter variable node
1828            let param_span = Span::from_bytes(child.start_byte(), child.end_byte());
1829            let param_id = helper.add_variable(param_name, Some(param_span));
1830
1831            // Create TypeOf edge with full type signature and Parameter context
1832            let type_id = helper.add_type(&type_text, None);
1833            helper.add_typeof_edge_with_context(
1834                param_id,
1835                type_id,
1836                Some(TypeOfContext::Parameter),
1837                Some(param_index),
1838                Some(param_name),
1839            );
1840
1841            // Create Reference edges for all nested types
1842            for type_name in &all_type_names {
1843                let ref_type_id = helper.add_type(type_name, None);
1844                helper.add_reference_edge(param_id, ref_type_id);
1845            }
1846
1847            param_index += 1;
1848        }
1849    }
1850}
1851
1852/// Process method return type to create `TypeOf` and Reference edges.
1853///
1854/// Handles patterns like:
1855/// - `string GetName()`
1856/// - `List<User> GetUsers()`
1857/// - `Task<Result> ProcessAsync()`
1858///
1859/// tree-sitter-c-sharp structure:
1860/// ```text
1861/// method_declaration
1862///   return_type: predefined_type | identifier | generic_name
1863///   name: identifier
1864///   parameter_list
1865/// ```
1866fn process_method_return_type(
1867    node: Node,
1868    method_name: &str,
1869    content: &[u8],
1870    helper: &mut GraphBuildHelper,
1871) {
1872    // Get return_type - C# uses "return_type", "returns", or "type" field
1873    let return_type_node = node
1874        .child_by_field_name("return_type")
1875        .or_else(|| node.child_by_field_name("returns"))
1876        .or_else(|| node.child_by_field_name("type"));
1877
1878    let Some(return_type_node) = return_type_node else {
1879        return;
1880    };
1881
1882    // Skip void return types
1883    if let Ok(type_text) = return_type_node.utf8_text(content)
1884        && type_text.trim() == "void"
1885    {
1886        return;
1887    }
1888
1889    // Extract full type signature for TypeOf edge
1890    let Some(type_text) = extract_type_string(return_type_node, content) else {
1891        return;
1892    };
1893
1894    // Extract all type names for Reference edges
1895    let all_type_names = extract_all_type_names_from_annotation(return_type_node, content);
1896
1897    // Get or find the method node ID
1898    let method_span = Span::from_bytes(node.start_byte(), node.end_byte());
1899    let method_id = helper.add_method(method_name, Some(method_span), false, false);
1900
1901    // Create TypeOf edge with full type signature and Return context
1902    let type_id = helper.add_type(&type_text, None);
1903    helper.add_typeof_edge_with_context(
1904        method_id,
1905        type_id,
1906        Some(TypeOfContext::Return),
1907        Some(0), // Return always has index 0
1908        Some(method_name),
1909    );
1910
1911    // Create Reference edges for all nested types
1912    for type_name in &all_type_names {
1913        let ref_type_id = helper.add_type(type_name, None);
1914        helper.add_reference_edge(method_id, ref_type_id);
1915    }
1916}