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                // Emit per-type-parameter Type nodes + where-clause
517                // Constraint edges (REQ:R0028 / U19 AC-1, AC-2, AC-3,
518                // AC-4, AC-5).
519                process_type_parameter_declarations(node, content, &qualified_class, helper);
520
521                // Export class if it has public or internal visibility
522                if should_export(node, content)
523                    && let Some(class_id) = node_map.get(&qualified_class)
524                {
525                    export_from_file_module(helper, *class_id);
526                }
527
528                // Recurse into class body to handle method exports
529                if let Some(body) = node.child_by_field_name("body") {
530                    process_class_member_exports(body, content, &qualified_class, helper, node_map);
531
532                    let mut cursor = body.walk();
533                    for child in body.children(&mut cursor) {
534                        walk_tree_for_edges(
535                            child,
536                            content,
537                            ast_graph,
538                            helper,
539                            node_map,
540                            namespace_stack,
541                            class_stack,
542                            scope_tree,
543                        )?;
544                    }
545                }
546
547                class_stack.pop();
548                return Ok(());
549            }
550        }
551        "interface_declaration" => {
552            // Track interface context and process OOP edges
553            if let Some(name_node) = node.child_by_field_name("name")
554                && let Ok(interface_name) = name_node.utf8_text(content)
555            {
556                // Build qualified interface name
557                let qualified_interface =
558                    build_qualified_name(namespace_stack, class_stack, interface_name);
559
560                // Process OOP edges with qualified name, passing namespace context for base type resolution
561                process_interface_declaration(
562                    node,
563                    content,
564                    helper,
565                    node_map,
566                    &qualified_interface,
567                    namespace_stack,
568                );
569
570                // Emit per-type-parameter Type nodes + where-clause
571                // Constraint edges (REQ:R0028 / U19 AC-1, AC-2, AC-3,
572                // AC-4, AC-5).
573                process_type_parameter_declarations(node, content, &qualified_interface, helper);
574
575                // Export interface if it has public or internal visibility
576                if should_export(node, content)
577                    && let Some(interface_id) = node_map.get(&qualified_interface)
578                {
579                    export_from_file_module(helper, *interface_id);
580                }
581
582                // Process interface method exports and recurse into the
583                // interface body so generic interface methods reach the
584                // method_declaration arm of this walker (REQ:R0028 / U19
585                // AC-1: interface-method type-parameters + where-clause
586                // Constraint edges). The interface name is pushed onto
587                // class_stack for the duration of the body walk so the
588                // method walker builds the correct
589                // `<namespace>.<InterfaceName>.<MethodName>.<ParamName>`
590                // qualified name.
591                if let Some(body) = node.child_by_field_name("body") {
592                    process_interface_member_exports(
593                        body,
594                        content,
595                        &qualified_interface,
596                        helper,
597                        node_map,
598                    );
599
600                    class_stack.push(interface_name.to_string());
601                    let mut cursor = body.walk();
602                    for child in body.children(&mut cursor) {
603                        walk_tree_for_edges(
604                            child,
605                            content,
606                            ast_graph,
607                            helper,
608                            node_map,
609                            namespace_stack,
610                            class_stack,
611                            scope_tree,
612                        )?;
613                    }
614                    class_stack.pop();
615                }
616
617                return Ok(());
618            }
619        }
620        "invocation_expression" => {
621            process_invocation(node, content, ast_graph, helper, node_map);
622        }
623        "object_creation_expression" => {
624            process_object_creation(node, content, ast_graph, helper, node_map);
625        }
626        "using_directive" => {
627            process_using_directive(node, content, helper);
628        }
629        "method_declaration" => {
630            // Check for P/Invoke (DllImport attribute + extern modifier)
631            process_pinvoke_method(node, content, helper, node_map, namespace_stack);
632
633            // Process method parameters and return type for TypeOf edges
634            if let Some(name_node) = node.child_by_field_name("name")
635                && let Ok(method_name) = name_node.utf8_text(content)
636            {
637                // Build qualified method name with namespace and class context
638                // Must match format from extract_callable_context (scope_stack.join("."))
639                let mut scope_parts = namespace_stack.clone();
640                scope_parts.extend(class_stack.iter().cloned());
641
642                let qualified_name = if scope_parts.is_empty() {
643                    method_name.to_string()
644                } else {
645                    format!("{}.{}", scope_parts.join("."), method_name)
646                };
647
648                process_method_parameters(node, &qualified_name, content, helper);
649                process_method_return_type(node, &qualified_name, content, helper);
650
651                // Emit per-type-parameter Type nodes + where-clause
652                // Constraint edges for generic methods (REQ:R0028 /
653                // U19 AC-1, AC-2, AC-3, AC-4).
654                process_type_parameter_declarations(node, content, &qualified_name, helper);
655            }
656        }
657        "local_declaration_statement" => {
658            // Process local variable declarations with TypeOf edges
659            process_local_variables(node, content, helper, class_stack);
660        }
661        "field_declaration" => {
662            // Process field declarations with TypeOf edges
663            process_field_declaration(node, content, helper, class_stack);
664        }
665        "property_declaration" => {
666            // Process property declarations with TypeOf edges
667            process_property_declaration(node, content, helper, class_stack);
668        }
669        "identifier" => {
670            local_scopes::handle_identifier_for_reference(node, content, scope_tree, helper);
671        }
672        _ => {}
673    }
674
675    // Recurse into children
676    let mut cursor = node.walk();
677    for child in node.children(&mut cursor) {
678        walk_tree_for_edges(
679            child,
680            content,
681            ast_graph,
682            helper,
683            node_map,
684            namespace_stack,
685            class_stack,
686            scope_tree,
687        )?;
688    }
689
690    Ok(())
691}
692
693/// Build a qualified name from namespace and class context.
694fn build_qualified_name(namespace_stack: &[String], class_stack: &[String], name: &str) -> String {
695    let mut parts = Vec::new();
696    parts.extend(namespace_stack.iter().cloned());
697    parts.extend(class_stack.iter().cloned());
698    parts.push(name.to_string());
699    parts.join(".")
700}
701
702/// Qualify a type name with namespace context if it's not already qualified.
703///
704/// For types that are already qualified (contain '.'), returns as-is.
705/// For unqualified types in a namespace, prefixes with the namespace.
706/// This is a best-effort heuristic - without full import resolution,
707/// we assume unqualified types in a namespace are from that namespace.
708fn qualify_type_name(type_name: &str, namespace_stack: &[String]) -> String {
709    // If already qualified (contains '.'), use as-is
710    if type_name.contains('.') {
711        return type_name.to_string();
712    }
713
714    // If no namespace context, use type name as-is
715    if namespace_stack.is_empty() {
716        return type_name.to_string();
717    }
718
719    // Prefix with namespace
720    format!("{}.{}", namespace_stack.join("."), type_name)
721}
722
723fn process_invocation(
724    node: Node,
725    content: &[u8],
726    ast_graph: &ASTGraph,
727    helper: &mut GraphBuildHelper,
728    node_map: &mut HashMap<String, NodeId>,
729) {
730    let Some(function_node) = node.child_by_field_name("function") else {
731        return;
732    };
733
734    let Ok(callee_text) = function_node.utf8_text(content) else {
735        return;
736    };
737
738    // Get the caller context
739    let Some(call_context) = ast_graph.get_callable_context(node.id()) else {
740        return;
741    };
742
743    // Handle different invocation patterns:
744    // - Simple: methodName()
745    // - Member access: obj.methodName()
746    // - Static: ClassName.methodName()
747    let callee_qualified = if callee_text.contains('.') {
748        // Handle member access or static calls
749        callee_text.to_string()
750    } else if let Some(class_name) = &call_context.class_name {
751        // For simple calls inside a class, resolve to ClassName.method
752        format!("{class_name}.{callee_text}")
753    } else {
754        callee_text.to_string()
755    };
756
757    // Get or create caller node
758    let caller_function_id = *node_map
759        .entry(call_context.qualified_name.clone())
760        .or_insert_with(|| helper.add_function(&call_context.qualified_name, None, false, false));
761
762    // Get or create callee node
763    let target_function_id = *node_map
764        .entry(callee_qualified.clone())
765        .or_insert_with(|| helper.add_function(&callee_qualified, None, false, false));
766
767    let argument_count = count_call_arguments(node);
768    let call_span = Span::from_bytes(node.start_byte(), node.end_byte());
769    helper.add_call_edge_full_with_span(
770        caller_function_id,
771        target_function_id,
772        argument_count,
773        false,
774        vec![call_span],
775    );
776}
777
778fn process_object_creation(
779    node: Node,
780    content: &[u8],
781    ast_graph: &ASTGraph,
782    helper: &mut GraphBuildHelper,
783    node_map: &mut HashMap<String, NodeId>,
784) {
785    let Some(type_node) = node.child_by_field_name("type") else {
786        return;
787    };
788
789    let Ok(type_name) = type_node.utf8_text(content) else {
790        return;
791    };
792
793    // Get the caller context
794    let Some(call_context) = ast_graph.get_callable_context(node.id()) else {
795        return;
796    };
797
798    // Treat constructor calls as calls to ClassName.ctor
799    let callee_qualified = format!("{type_name}.ctor");
800
801    // Get or create caller node
802    let caller_function_id = *node_map
803        .entry(call_context.qualified_name.clone())
804        .or_insert_with(|| helper.add_function(&call_context.qualified_name, None, false, false));
805
806    // Get or create callee node (constructor)
807    let target_function_id = *node_map
808        .entry(callee_qualified.clone())
809        .or_insert_with(|| helper.add_method(&callee_qualified, None, false, false));
810
811    let argument_count = count_call_arguments(node);
812    let call_span = Span::from_bytes(node.start_byte(), node.end_byte());
813    helper.add_call_edge_full_with_span(
814        caller_function_id,
815        target_function_id,
816        argument_count,
817        false,
818        vec![call_span],
819    );
820}
821
822fn count_call_arguments(call_node: Node<'_>) -> u8 {
823    let args_node = call_node
824        .child_by_field_name("arguments")
825        .or_else(|| call_node.child_by_field_name("argument_list"))
826        .or_else(|| {
827            let mut cursor = call_node.walk();
828            call_node
829                .children(&mut cursor)
830                .find(|child| child.kind() == "argument_list")
831        });
832
833    let Some(args_node) = args_node else {
834        return 255;
835    };
836
837    let count = args_node.named_child_count();
838    if count <= 254 {
839        u8::try_from(count).unwrap_or(u8::MAX)
840    } else {
841        u8::MAX
842    }
843}
844
845// ============================================================================
846// Import Processing - using directives
847// ============================================================================
848
849/// Process using directive to create Import edges.
850///
851/// Handles patterns like:
852/// - `using System;` - simple namespace import
853/// - `using System.Collections.Generic;` - qualified namespace import
854/// - `using static System.Math;` - static using (`is_wildcard`: true for all static members)
855/// - `using Alias = Namespace.Type;` - aliased using (populate alias field)
856///
857/// # tree-sitter-c-sharp AST structure
858///
859/// Simple using:
860/// ```text
861/// using_directive [0..13] "using System;"
862///   using [0..5] "using"
863///   identifier [6..12] "System"
864///   ; [12..13] ";"
865/// ```
866///
867/// Qualified using:
868/// ```text
869/// using_directive
870///   using
871///   qualified_name "System.Collections.Generic"
872///     identifier "System"
873///     . "."
874///     identifier "Collections"
875///     . "."
876///     identifier "Generic"
877///   ;
878/// ```
879///
880/// Static using:
881/// ```text
882/// using_directive
883///   using
884///   static
885///   qualified_name "System.Math"
886///   ;
887/// ```
888///
889/// Aliased using:
890/// ```text
891/// using_directive [0..21] "using IO = System.IO;"
892///   using [0..5] "using"
893///   identifier [6..8] "IO"          <- alias
894///   = [9..10] "="
895///   qualified_name [11..20] "System.IO"  <- target
896///   ; [20..21] ";"
897/// ```
898fn process_using_directive(node: Node, content: &[u8], helper: &mut GraphBuildHelper) {
899    // Check for static using modifier
900    let is_static = node
901        .children(&mut node.walk())
902        .any(|child| child.kind() == "static");
903
904    // Detect aliased using: pattern is "using <identifier> = <target>;"
905    // We look for an "=" child, which indicates aliased using
906    let has_equals = node
907        .children(&mut node.walk())
908        .any(|child| child.kind() == "=");
909
910    // Extract alias and target based on the structure
911    let (alias, imported_name) = if has_equals {
912        // Aliased using: first identifier is alias, qualified_name/identifier after "=" is target
913        extract_aliased_using(node, content)
914    } else {
915        // Simple or static using: first identifier/qualified_name is the target
916        (None, extract_simple_using_target(node, content))
917    };
918
919    let Some(imported_name) = imported_name else {
920        return;
921    };
922
923    // Create module node (represents the current file as the importing entity)
924    let module_id = helper.add_module("<file>", None);
925
926    // Create import node for the imported namespace/type
927    let span = Span::from_bytes(node.start_byte(), node.end_byte());
928    let import_name = if is_static {
929        format!("static {imported_name}")
930    } else {
931        imported_name.clone()
932    };
933    let imported_id = helper.add_import(&import_name, Some(span));
934
935    // Add import edge with appropriate metadata
936    // Static usings are wildcard imports (all static members are accessible)
937    // Aliased usings have an alias but are not wildcard
938    match (alias.as_deref(), is_static) {
939        (Some(alias_str), _) => {
940            // Aliased import: using IO = System.IO;
941            helper.add_import_edge_full(module_id, imported_id, Some(alias_str), false);
942        }
943        (None, true) => {
944            // Static import: using static System.Math;
945            // All static members are imported, so is_wildcard = true
946            helper.add_import_edge_full(module_id, imported_id, None, true);
947        }
948        (None, false) => {
949            // Simple import: using System;
950            helper.add_import_edge(module_id, imported_id);
951        }
952    }
953}
954
955/// Extract alias and target from an aliased using directive.
956///
957/// Structure: `using <alias> = <target>;`
958/// - The first identifier before "=" is the alias
959/// - The `identifier/qualified_name` after "=" is the target
960fn extract_aliased_using(node: Node, content: &[u8]) -> (Option<String>, Option<String>) {
961    let mut alias: Option<String> = None;
962    let mut target: Option<String> = None;
963    let mut past_equals = false;
964
965    let mut cursor = node.walk();
966    for child in node.children(&mut cursor) {
967        let kind = child.kind();
968
969        if kind == "=" {
970            past_equals = true;
971            continue;
972        }
973
974        // Skip keywords and punctuation
975        if kind == "using" || kind == "static" || kind == ";" {
976            continue;
977        }
978
979        if past_equals {
980            // After "=" - this is the target (identifier or qualified_name)
981            if matches!(kind, "identifier" | "qualified_name") && target.is_none() {
982                target = child
983                    .utf8_text(content)
984                    .ok()
985                    .map(std::string::ToString::to_string);
986            }
987        } else if kind == "identifier" && alias.is_none() {
988            // Before "=" - this is the alias (should be identifier)
989            alias = child
990                .utf8_text(content)
991                .ok()
992                .map(std::string::ToString::to_string);
993        }
994    }
995
996    (alias, target)
997}
998
999/// Extract the target from a simple or static using directive.
1000///
1001/// Structure: `using [static] <target>;`
1002/// - The first `identifier/qualified_name` (after optional "static") is the target
1003fn extract_simple_using_target(node: Node, content: &[u8]) -> Option<String> {
1004    let mut cursor = node.walk();
1005
1006    for child in node.children(&mut cursor) {
1007        let kind = child.kind();
1008
1009        // Skip keywords and punctuation
1010        if kind == "using" || kind == "static" || kind == ";" || kind == "=" {
1011            continue;
1012        }
1013
1014        // First identifier or qualified_name is the target
1015        if matches!(kind, "identifier" | "identifier_name" | "qualified_name") {
1016            return child
1017                .utf8_text(content)
1018                .ok()
1019                .map(std::string::ToString::to_string);
1020        }
1021    }
1022
1023    // Fallback: try to get via field name
1024    node.child_by_field_name("name")
1025        .and_then(|n| n.utf8_text(content).ok())
1026        .map(std::string::ToString::to_string)
1027}
1028
1029// ============================================================================
1030// OOP Processing - Inheritance and Interface Implementation
1031// ============================================================================
1032
1033/// Process class declaration to extract Inherits and Implements edges.
1034///
1035/// Handles patterns like:
1036/// - `class Child : Parent` → Inherits edge
1037/// - `class Foo : IBar` → Implements edge (I prefix convention)
1038/// - `class Foo : Parent, IBar, IBaz` → One Inherits + multiple Implements edges
1039///
1040/// tree-sitter-c-sharp structure:
1041/// ```text
1042/// class_declaration
1043///   class (keyword)
1044///   name: identifier
1045///   [base_list]
1046///     : (colon)
1047///     [type_identifier | generic_name | qualified_name]+
1048///   body: declaration_list
1049/// ```
1050fn process_class_declaration(
1051    node: Node,
1052    content: &[u8],
1053    helper: &mut GraphBuildHelper,
1054    node_map: &mut HashMap<String, NodeId>,
1055    qualified_class_name: &str,
1056    namespace_stack: &[String],
1057) {
1058    // Get or create class node using qualified name (same as Phase 1)
1059    let class_id = *node_map
1060        .entry(qualified_class_name.to_string())
1061        .or_insert_with(|| helper.add_class(qualified_class_name, None));
1062
1063    // Find the base_list node (contains inheritance and interface implementation)
1064    let mut cursor = node.walk();
1065    let base_list = node
1066        .children(&mut cursor)
1067        .find(|child| child.kind() == "base_list");
1068
1069    let Some(base_list) = base_list else {
1070        return;
1071    };
1072
1073    // Track whether we've seen a class inheritance (first non-interface type)
1074    let mut first_base_class = true;
1075
1076    // Process each type in the base list
1077    let mut base_cursor = base_list.walk();
1078    for base_child in base_list.children(&mut base_cursor) {
1079        let base_type_name = match base_child.kind() {
1080            "identifier" | "identifier_name" | "type_identifier" | "qualified_name" => base_child
1081                .utf8_text(content)
1082                .ok()
1083                .map(std::string::ToString::to_string),
1084            "generic_name" => {
1085                // Generic type like List<T> - extract the base type name
1086                base_child
1087                    .child_by_field_name("name")
1088                    .or_else(|| base_child.child(0))
1089                    .and_then(|n| n.utf8_text(content).ok())
1090                    .map(std::string::ToString::to_string)
1091            }
1092            _ => None,
1093        };
1094
1095        let Some(base_name) = base_type_name else {
1096            continue;
1097        };
1098
1099        // Qualify base type name if it's unqualified and we have namespace context
1100        // If already qualified (contains '.'), use as-is; otherwise prefix with namespace
1101        let qualified_base_name = qualify_type_name(&base_name, namespace_stack);
1102
1103        // Determine if this is an interface using base-list position semantics:
1104        // In C#, if a class has both a base class and interfaces, the base class must come first.
1105        // We also check the I* naming convention as a secondary heuristic.
1106        let is_interface = is_interface_name(&base_name);
1107
1108        if is_interface {
1109            // Create interface node and add Implements edge
1110            let interface_id = *node_map
1111                .entry(qualified_base_name.clone())
1112                .or_insert_with(|| helper.add_interface(&qualified_base_name, None));
1113            helper.add_implements_edge(class_id, interface_id);
1114        } else if first_base_class {
1115            // First non-interface type is the base class
1116            let parent_id = *node_map
1117                .entry(qualified_base_name.clone())
1118                .or_insert_with(|| helper.add_class(&qualified_base_name, None));
1119            helper.add_inherits_edge(class_id, parent_id);
1120            first_base_class = false;
1121        }
1122        // Note: C# only allows single class inheritance, so we only process the first class
1123    }
1124}
1125
1126/// Process interface declaration to extract Inherits edges for interface extension.
1127///
1128/// Handles patterns like:
1129/// - `interface IChild : IParent` → Inherits edge
1130/// - `interface IChild : IParent, IOther` → Multiple Inherits edges
1131///
1132/// tree-sitter-c-sharp structure:
1133/// ```text
1134/// interface_declaration
1135///   interface (keyword)
1136///   name: identifier
1137///   [base_list]
1138///     : (colon)
1139///     [identifier | qualified_name]+
1140///   body: declaration_list
1141/// ```
1142fn process_interface_declaration(
1143    node: Node,
1144    content: &[u8],
1145    helper: &mut GraphBuildHelper,
1146    node_map: &mut HashMap<String, NodeId>,
1147    qualified_interface_name: &str,
1148    namespace_stack: &[String],
1149) {
1150    // Get or create interface node using qualified name (same as Phase 1)
1151    let interface_id = *node_map
1152        .entry(qualified_interface_name.to_string())
1153        .or_insert_with(|| helper.add_interface(qualified_interface_name, None));
1154
1155    // Find the base_list node
1156    let mut cursor = node.walk();
1157    let base_list = node
1158        .children(&mut cursor)
1159        .find(|child| child.kind() == "base_list");
1160
1161    let Some(base_list) = base_list else {
1162        return;
1163    };
1164
1165    // Process each parent interface in the base list
1166    let mut base_cursor = base_list.walk();
1167    for base_child in base_list.children(&mut base_cursor) {
1168        let parent_name = match base_child.kind() {
1169            "identifier" | "identifier_name" | "type_identifier" | "qualified_name" => base_child
1170                .utf8_text(content)
1171                .ok()
1172                .map(std::string::ToString::to_string),
1173            "generic_name" => base_child
1174                .child_by_field_name("name")
1175                .or_else(|| base_child.child(0))
1176                .and_then(|n| n.utf8_text(content).ok())
1177                .map(std::string::ToString::to_string),
1178            _ => None,
1179        };
1180
1181        let Some(parent_name) = parent_name else {
1182            continue;
1183        };
1184
1185        // Qualify parent type name if it's unqualified and we have namespace context
1186        let qualified_parent_name = qualify_type_name(&parent_name, namespace_stack);
1187
1188        // All base types for interfaces are parent interfaces → Inherits edge
1189        let parent_id = *node_map
1190            .entry(qualified_parent_name.clone())
1191            .or_insert_with(|| helper.add_interface(&qualified_parent_name, None));
1192        helper.add_inherits_edge(interface_id, parent_id);
1193    }
1194}
1195
1196/// Determine if a type name is an interface based on C# naming convention.
1197///
1198/// In C#, interfaces by convention start with 'I' followed by an uppercase letter.
1199/// Examples: `IDisposable`, `IEnumerable`, `IRepository`
1200/// Counter-examples: Int32, Image, Item
1201fn is_interface_name(name: &str) -> bool {
1202    let chars: Vec<char> = name.chars().collect();
1203    if chars.len() >= 2 {
1204        // Must start with 'I' followed by uppercase letter
1205        chars[0] == 'I' && chars[1].is_ascii_uppercase()
1206    } else {
1207        false
1208    }
1209}
1210
1211// ============================================================================
1212// Visibility Detection for Export Edges
1213// ============================================================================
1214
1215/// Check if a node has the `public` visibility modifier.
1216fn is_public(node: Node, content: &[u8]) -> bool {
1217    has_visibility_modifier(node, content, "public")
1218}
1219
1220/// Check if a node has the `internal` visibility modifier.
1221/// In C#, internal members are accessible within the same assembly and should be exported.
1222fn is_internal(node: Node, content: &[u8]) -> bool {
1223    has_visibility_modifier(node, content, "internal")
1224}
1225
1226/// Check if a node has the `private` visibility modifier.
1227fn is_private(node: Node, content: &[u8]) -> bool {
1228    has_visibility_modifier(node, content, "private")
1229}
1230
1231/// Check if a node has the `protected` visibility modifier.
1232#[allow(dead_code)] // Reserved for potential future use
1233fn is_protected(node: Node, content: &[u8]) -> bool {
1234    has_visibility_modifier(node, content, "protected")
1235}
1236
1237/// Check if a node has a specific visibility modifier.
1238fn has_visibility_modifier(node: Node, content: &[u8], modifier: &str) -> bool {
1239    node.children(&mut node.walk())
1240        .any(|child| child.kind() == modifier || child.utf8_text(content).unwrap_or("") == modifier)
1241}
1242
1243fn has_modifier(node: Node, content: &[u8], modifier: &str) -> bool {
1244    node.children(&mut node.walk())
1245        .any(|child| child.kind() == modifier || child.utf8_text(content).unwrap_or("") == modifier)
1246}
1247
1248/// Check if a member should be exported (public or internal visibility).
1249/// In C#, both public and internal types/members are exported:
1250/// - public: accessible everywhere
1251/// - internal: accessible within the same assembly
1252/// - private and protected: NOT exported
1253fn should_export(node: Node, content: &[u8]) -> bool {
1254    is_public(node, content) || is_internal(node, content)
1255}
1256
1257/// Create an export edge from the file module to the exported node.
1258fn export_from_file_module(helper: &mut GraphBuildHelper, exported: NodeId) {
1259    let module_id = helper.add_module(FILE_MODULE_NAME, None);
1260    helper.add_export_edge(module_id, exported);
1261}
1262
1263/// Process public/internal methods and fields within a class body for export edges.
1264fn process_class_member_exports(
1265    body_node: Node,
1266    content: &[u8],
1267    class_qualified_name: &str,
1268    helper: &mut GraphBuildHelper,
1269    node_map: &mut HashMap<String, NodeId>,
1270) {
1271    let mut cursor = body_node.walk();
1272    for child in body_node.children(&mut cursor) {
1273        match child.kind() {
1274            "method_declaration" | "constructor_declaration" => {
1275                // Export method/constructor if it has public or internal visibility
1276                if should_export(child, content)
1277                    && let Some(name_node) = child.child_by_field_name("name")
1278                    && let Ok(method_name) = name_node.utf8_text(content)
1279                {
1280                    let qualified_name = format!("{class_qualified_name}.{method_name}");
1281                    // Get the method from node_map if it exists
1282                    if let Some(method_id) = node_map.get(&qualified_name) {
1283                        export_from_file_module(helper, *method_id);
1284                    }
1285                } else if should_export(child, content) && child.kind() == "constructor_declaration"
1286                {
1287                    // Constructors don't have a name field, use the class name
1288                    let class_name = class_qualified_name
1289                        .rsplit('.')
1290                        .next()
1291                        .unwrap_or(class_qualified_name);
1292                    let qualified_name = format!("{class_qualified_name}.{class_name}");
1293                    if let Some(method_id) = node_map.get(&qualified_name) {
1294                        export_from_file_module(helper, *method_id);
1295                    }
1296                }
1297            }
1298            "field_declaration" | "property_declaration" => {
1299                // Export field/property if it has public or internal
1300                // visibility. Re-emit through the same Property/Constant
1301                // discrimination as the typeof pass so the helper's
1302                // (qualified_name + kind) dedup key collapses to one node
1303                // (REQ:R0001, R0003, R0004, R0005, R0018, R0019).
1304                if should_export(child, content) {
1305                    let is_property = child.kind() == "property_declaration";
1306                    let is_const = !is_property && has_modifier(child, content, "const");
1307                    let is_readonly = !is_property && has_modifier(child, content, "readonly");
1308                    let is_static = is_const || has_modifier(child, content, "static");
1309                    let visibility = extract_field_visibility(child, content);
1310                    let get_only = is_property && is_get_only_property(child);
1311                    let emit_constant = is_const || is_readonly || get_only;
1312
1313                    // Fields and properties can have multiple declarators
1314                    let mut field_cursor = child.walk();
1315                    for field_child in child.children(&mut field_cursor) {
1316                        if field_child.kind() == "variable_declarator"
1317                            && let Some(name_node) = field_child.child_by_field_name("name")
1318                            && let Ok(field_name) = name_node.utf8_text(content)
1319                        {
1320                            let qualified_name = format!("{class_qualified_name}.{field_name}");
1321                            let span =
1322                                Span::from_bytes(field_child.start_byte(), field_child.end_byte());
1323
1324                            let field_id = if emit_constant {
1325                                helper.add_constant_with_static_and_visibility(
1326                                    &qualified_name,
1327                                    Some(span),
1328                                    is_static,
1329                                    Some(visibility),
1330                                )
1331                            } else {
1332                                helper.add_property_with_static_and_visibility(
1333                                    &qualified_name,
1334                                    Some(span),
1335                                    is_static,
1336                                    Some(visibility),
1337                                )
1338                            };
1339                            export_from_file_module(helper, field_id);
1340                        } else if field_child.kind() == "identifier"
1341                            && let Ok(prop_name) = field_child.utf8_text(content)
1342                        {
1343                            // Property name is directly an identifier
1344                            let qualified_name = format!("{class_qualified_name}.{prop_name}");
1345                            let span = Span::from_bytes(child.start_byte(), child.end_byte());
1346
1347                            let prop_id = if emit_constant {
1348                                helper.add_constant_with_static_and_visibility(
1349                                    &qualified_name,
1350                                    Some(span),
1351                                    is_static,
1352                                    Some(visibility),
1353                                )
1354                            } else {
1355                                helper.add_property_with_static_and_visibility(
1356                                    &qualified_name,
1357                                    Some(span),
1358                                    is_static,
1359                                    Some(visibility),
1360                                )
1361                            };
1362                            export_from_file_module(helper, prop_id);
1363                        }
1364                    }
1365                }
1366            }
1367            _ => {}
1368        }
1369    }
1370}
1371
1372/// Process interface method exports.
1373/// In C#, interface methods are implicitly public unless explicitly marked private (C# 8.0+).
1374fn process_interface_member_exports(
1375    body_node: Node,
1376    content: &[u8],
1377    interface_qualified_name: &str,
1378    helper: &mut GraphBuildHelper,
1379    node_map: &mut HashMap<String, NodeId>,
1380) {
1381    let mut cursor = body_node.walk();
1382    for child in body_node.children(&mut cursor) {
1383        if child.kind() == "method_declaration"
1384            && !is_private(child, content)
1385            && let Some(name_node) = child.child_by_field_name("name")
1386            && let Ok(method_name) = name_node.utf8_text(content)
1387        {
1388            // Interface methods are implicitly public unless explicitly private
1389            let qualified_name = format!("{interface_qualified_name}.{method_name}");
1390            // Get the method from node_map if it exists
1391            if let Some(method_id) = node_map.get(&qualified_name) {
1392                export_from_file_module(helper, *method_id);
1393            }
1394        }
1395    }
1396}
1397
1398// ============================================================================
1399// P/Invoke (FFI) Processing
1400// ============================================================================
1401
1402/// Process method declaration to detect P/Invoke (Platform Invocation Services).
1403///
1404/// P/Invoke pattern in C#:
1405/// ```csharp
1406/// [DllImport("user32.dll")]
1407/// static extern int MessageBox(IntPtr hWnd, string text, string caption, uint type);
1408///
1409/// [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
1410/// static extern bool Beep(uint frequency, uint duration);
1411/// ```
1412///
1413/// tree-sitter-c-sharp structure:
1414/// ```text
1415/// method_declaration
1416///   attribute_list
1417///     attribute
1418///       name: identifier ("DllImport")
1419///       argument_list
1420///         attribute_argument
1421///           expression: string_literal ("user32.dll")
1422///   modifier: static
1423///   modifier: extern
1424///   return_type: predefined_type
1425///   name: identifier
1426///   parameter_list
1427/// ```
1428fn process_pinvoke_method(
1429    node: Node,
1430    content: &[u8],
1431    helper: &mut GraphBuildHelper,
1432    node_map: &mut HashMap<String, NodeId>,
1433    namespace_stack: &[String],
1434) {
1435    // Check for extern modifier
1436    let has_extern = node
1437        .children(&mut node.walk())
1438        .any(|child| child.kind() == "extern");
1439
1440    if !has_extern {
1441        return;
1442    }
1443
1444    // Look for DllImport attribute
1445    let mut cursor = node.walk();
1446    let attribute_list = node
1447        .children(&mut cursor)
1448        .find(|child| child.kind() == "attribute_list");
1449
1450    let Some(attribute_list) = attribute_list else {
1451        return;
1452    };
1453
1454    // Find DllImport attribute and extract library name
1455    let (dll_name, calling_convention) = extract_dllimport_info(attribute_list, content);
1456
1457    let Some(dll_name) = dll_name else {
1458        return;
1459    };
1460
1461    // Extract method name
1462    let method_name = node
1463        .child_by_field_name("name")
1464        .and_then(|n| n.utf8_text(content).ok())
1465        .map(std::string::ToString::to_string);
1466
1467    let Some(method_name) = method_name else {
1468        return;
1469    };
1470
1471    // Build qualified method name
1472    let qualified_method = if namespace_stack.is_empty() {
1473        method_name.clone()
1474    } else {
1475        format!("{}.{}", namespace_stack.join("."), method_name)
1476    };
1477
1478    // Get or create method node (caller - the C# method declaration)
1479    let method_span = Span::from_bytes(node.start_byte(), node.end_byte());
1480    let method_id = *node_map
1481        .entry(qualified_method.clone())
1482        .or_insert_with(|| helper.add_method(&qualified_method, Some(method_span), false, true));
1483
1484    // Create FFI function node (the native function in the DLL)
1485    let ffi_func_name = format!("ffi::{dll_name}::{method_name}");
1486    let ffi_func_id = *node_map
1487        .entry(ffi_func_name.clone())
1488        .or_insert_with(|| helper.add_function(&ffi_func_name, None, false, false));
1489
1490    // Determine FFI convention from CallingConvention parameter
1491    let convention = match calling_convention.as_deref() {
1492        Some("CallingConvention.Cdecl" | "Cdecl") => FfiConvention::Cdecl,
1493        Some("CallingConvention.FastCall" | "FastCall") => FfiConvention::Fastcall,
1494        // Default Windows convention is StdCall for P/Invoke
1495        _ => FfiConvention::Stdcall,
1496    };
1497
1498    // Add FfiCall edge
1499    helper.add_ffi_edge(method_id, ffi_func_id, convention);
1500}
1501
1502/// Extract `DllImport` attribute information (library name and calling convention).
1503///
1504/// Returns (`dll_name`, `calling_convention`) tuple.
1505fn extract_dllimport_info(
1506    attribute_list: Node,
1507    content: &[u8],
1508) -> (Option<String>, Option<String>) {
1509    let mut dll_name = None;
1510    let mut calling_convention = None;
1511
1512    let mut list_cursor = attribute_list.walk();
1513    for attr_child in attribute_list.children(&mut list_cursor) {
1514        if attr_child.kind() != "attribute" {
1515            continue;
1516        }
1517
1518        // Check if this is DllImport attribute
1519        let attr_name = attr_child
1520            .child_by_field_name("name")
1521            .and_then(|n| n.utf8_text(content).ok());
1522
1523        let is_dllimport = attr_name.is_some_and(|name| {
1524            name == "DllImport" || name == "System.Runtime.InteropServices.DllImport"
1525        });
1526
1527        if !is_dllimport {
1528            continue;
1529        }
1530
1531        // Extract arguments
1532        let mut attr_cursor = attr_child.walk();
1533        let arg_list = attr_child
1534            .children(&mut attr_cursor)
1535            .find(|child| child.kind() == "attribute_argument_list");
1536
1537        let Some(arg_list) = arg_list else {
1538            continue;
1539        };
1540
1541        let mut arg_cursor = arg_list.walk();
1542        for arg in arg_list.children(&mut arg_cursor) {
1543            if arg.kind() != "attribute_argument" {
1544                continue;
1545            }
1546
1547            // Check for named argument (CallingConvention = ...)
1548            if let Some(name_node) = arg.child_by_field_name("name")
1549                && let Ok(name) = name_node.utf8_text(content)
1550            {
1551                if name == "CallingConvention"
1552                    && let Some(expr) = arg.child_by_field_name("expression")
1553                    && let Ok(value) = expr.utf8_text(content)
1554                {
1555                    calling_convention = Some(value.to_string());
1556                }
1557                continue;
1558            }
1559
1560            // Positional argument - first one is the DLL name
1561            if dll_name.is_none() {
1562                // Find the string literal
1563                let expr = arg.child_by_field_name("expression").or_else(|| {
1564                    // Sometimes the string is a direct child
1565                    let mut c = arg.walk();
1566                    arg.children(&mut c)
1567                        .find(|child| child.kind() == "string_literal")
1568                });
1569
1570                if let Some(expr) = expr
1571                    && let Ok(text) = expr.utf8_text(content)
1572                {
1573                    // Remove quotes from string literal
1574                    let trimmed = text.trim();
1575                    if (trimmed.starts_with('"') && trimmed.ends_with('"'))
1576                        || (trimmed.starts_with('@') && trimmed.len() > 2)
1577                    {
1578                        let start = if trimmed.starts_with('@') { 2 } else { 1 };
1579                        dll_name = Some(trimmed[start..trimmed.len() - 1].to_string());
1580                    } else {
1581                        dll_name = Some(trimmed.to_string());
1582                    }
1583                }
1584            }
1585        }
1586    }
1587
1588    (dll_name, calling_convention)
1589}
1590
1591// ============================================================================
1592// TypeOf and Reference Edge Processing
1593// ============================================================================
1594
1595/// Process local variable declarations to create `TypeOf` and Reference edges.
1596///
1597/// Handles patterns like:
1598/// - `int x = 5;`
1599/// - `string name = "test";`
1600/// - `int a = 1, b = 2;` (multiple declarators)
1601/// - `List<User> users = new List<User>();` (generics - extract base type)
1602/// - `int? count = null;` (nullable - extract base type)
1603/// - `int[] numbers = new int[5];` (arrays - extract base type)
1604///
1605/// tree-sitter-c-sharp structure:
1606/// ```text
1607/// local_declaration_statement
1608///   variable_declaration
1609///     type: predefined_type | identifier | generic_name | nullable_type | array_type
1610///     variable_declarator
1611///       name: identifier
1612///       initializer: [expression]
1613/// ```
1614fn process_local_variables(
1615    node: Node,
1616    content: &[u8],
1617    helper: &mut GraphBuildHelper,
1618    _class_stack: &[String],
1619) {
1620    // Get variable_declaration child
1621    let mut cursor = node.walk();
1622    let var_decl = node
1623        .children(&mut cursor)
1624        .find(|child| child.kind() == "variable_declaration");
1625
1626    let Some(var_decl) = var_decl else {
1627        return;
1628    };
1629
1630    // Extract type
1631    let type_node = var_decl.child_by_field_name("type");
1632    let Some(type_node) = type_node else {
1633        return;
1634    };
1635
1636    // Extract full type signature for TypeOf edge
1637    let type_text = extract_type_string(type_node, content);
1638    let Some(type_text) = type_text else {
1639        return;
1640    };
1641
1642    // Extract all type names for Reference edges
1643    let all_type_names = extract_all_type_names_from_annotation(type_node, content);
1644
1645    // Process all variable declarators (may be multiple: int a = 1, b = 2;)
1646    let mut var_cursor = var_decl.walk();
1647    for child in var_decl.children(&mut var_cursor) {
1648        if child.kind() == "variable_declarator"
1649            && let Some(name_node) = child.child_by_field_name("name")
1650            && let Ok(var_name) = name_node.utf8_text(content)
1651        {
1652            let span = Span::from_bytes(child.start_byte(), child.end_byte());
1653
1654            // Create variable node
1655            let var_id = helper.add_variable(var_name, Some(span));
1656
1657            // Create TypeOf edge with full type signature and Variable context
1658            let type_id = helper.add_type(&type_text, None);
1659            helper.add_typeof_edge_with_context(
1660                var_id,
1661                type_id,
1662                Some(TypeOfContext::Variable),
1663                None,
1664                Some(var_name),
1665            );
1666
1667            // Create Reference edges for all nested types
1668            for type_name in &all_type_names {
1669                let ref_type_id = helper.add_type(type_name, None);
1670                helper.add_reference_edge(var_id, ref_type_id);
1671            }
1672        }
1673    }
1674}
1675
1676/// Process field declarations to create `TypeOf` and Reference edges.
1677///
1678/// Handles patterns like:
1679/// - `private UserRepository repository;`
1680/// - `private int age;`
1681/// - `public List<User> users;` (generics - extract base type)
1682///
1683/// tree-sitter-c-sharp structure:
1684/// ```text
1685/// field_declaration
1686///   modifiers: [...]
1687///   declaration:
1688///     type: predefined_type | identifier | generic_name
1689///     variable_declarator
1690///       name: identifier
1691///       initializer: [expression]
1692/// ```
1693fn process_field_declaration(
1694    node: Node,
1695    content: &[u8],
1696    helper: &mut GraphBuildHelper,
1697    class_stack: &[String],
1698) {
1699    // Get the declaration child (variable_declaration)
1700    let decl_node = node
1701        .children(&mut node.walk())
1702        .find(|child| child.kind() == "variable_declaration");
1703
1704    let Some(decl_node) = decl_node else {
1705        return;
1706    };
1707
1708    // Extract type
1709    let type_node = decl_node.child_by_field_name("type");
1710    let Some(type_node) = type_node else {
1711        return;
1712    };
1713
1714    // Extract full type signature for TypeOf edge
1715    let type_text = extract_type_string(type_node, content);
1716    let Some(type_text) = type_text else {
1717        return;
1718    };
1719
1720    // Extract all type names for Reference edges
1721    let all_type_names = extract_all_type_names_from_annotation(type_node, content);
1722
1723    // Get the containing class name
1724    let class_name = class_stack.last().map_or("", String::as_str);
1725
1726    // Process all variable declarators
1727    let mut var_cursor = decl_node.walk();
1728    for child in decl_node.children(&mut var_cursor) {
1729        if child.kind() == "variable_declarator"
1730            && let Some(name_node) = child.child_by_field_name("name")
1731            && let Ok(field_name) = name_node.utf8_text(content)
1732        {
1733            // Build qualified field name
1734            let qualified_name = if class_name.is_empty() {
1735                field_name.to_string()
1736            } else {
1737                format!("{class_name}.{field_name}")
1738            };
1739
1740            let span = Span::from_bytes(child.start_byte(), child.end_byte());
1741
1742            // Branch on `readonly` / `const` / `static` modifiers (REQ:R0001,
1743            // R0003, R0004, R0005, R0018, R0019). C# semantics:
1744            //   - `const` is implicitly static and immutable -> Constant.
1745            //   - `readonly` -> Constant (immutable post-construction).
1746            //   - otherwise mutable field -> Property.
1747            // Visibility comes from the accessibility modifier; the C# default
1748            // for a class member is `private` (REQ:R0023). `internal` is a
1749            // first-class accessibility level and is passed through verbatim.
1750            let is_const = has_modifier(node, content, "const");
1751            let is_readonly = has_modifier(node, content, "readonly");
1752            let is_static = is_const || has_modifier(node, content, "static");
1753            let visibility = extract_field_visibility(node, content);
1754
1755            let field_id = if is_const || is_readonly {
1756                helper.add_constant_with_static_and_visibility(
1757                    &qualified_name,
1758                    Some(span),
1759                    is_static,
1760                    Some(visibility),
1761                )
1762            } else {
1763                helper.add_property_with_static_and_visibility(
1764                    &qualified_name,
1765                    Some(span),
1766                    is_static,
1767                    Some(visibility),
1768                )
1769            };
1770
1771            // Create TypeOf edge with full type signature, Field context, and
1772            // the bare field name (REQ:R0023 / AC-4). Cross-language byte-exact
1773            // `field:<name>` planner queries depend on the unqualified form.
1774            let type_id = helper.add_type(&type_text, None);
1775            helper.add_typeof_edge_with_context(
1776                field_id,
1777                type_id,
1778                Some(TypeOfContext::Field),
1779                None,
1780                Some(field_name),
1781            );
1782
1783            // Create Reference edges for all nested types
1784            for type_name in &all_type_names {
1785                let ref_type_id = helper.add_type(type_name, None);
1786                helper.add_reference_edge(field_id, ref_type_id);
1787            }
1788        }
1789    }
1790}
1791
1792/// Process property declarations to create `TypeOf` and Reference edges.
1793///
1794/// Handles patterns like:
1795/// - `public int Age { get; set; }` (auto-property)
1796/// - `public string Name { get; }` (read-only property)
1797/// - `public bool IsAdult { get { return age >= 18; } }` (computed property)
1798/// - `public List<User> Users { get; set; }` (generic property)
1799///
1800/// tree-sitter-c-sharp structure:
1801/// ```text
1802/// property_declaration
1803///   modifiers: [...]
1804///   type: predefined_type | identifier | generic_name
1805///   name: identifier
1806///   accessor_list: { get; set; }
1807/// ```
1808fn process_property_declaration(
1809    node: Node,
1810    content: &[u8],
1811    helper: &mut GraphBuildHelper,
1812    class_stack: &[String],
1813) {
1814    // Extract type
1815    let type_node = node.child_by_field_name("type");
1816    let Some(type_node) = type_node else {
1817        return;
1818    };
1819
1820    // Extract full type signature for TypeOf edge
1821    let type_text = extract_type_string(type_node, content);
1822    let Some(type_text) = type_text else {
1823        return;
1824    };
1825
1826    // Extract all type names for Reference edges
1827    let all_type_names = extract_all_type_names_from_annotation(type_node, content);
1828
1829    // Extract property name
1830    let name_node = node.child_by_field_name("name");
1831    let Some(name_node) = name_node else {
1832        return;
1833    };
1834
1835    let Ok(prop_name) = name_node.utf8_text(content) else {
1836        return;
1837    };
1838
1839    // Get the containing class name
1840    let class_name = class_stack.last().map_or("", String::as_str);
1841
1842    // Build qualified property name
1843    let qualified_name = if class_name.is_empty() {
1844        prop_name.to_string()
1845    } else {
1846        format!("{class_name}.{prop_name}")
1847    };
1848
1849    let span = Span::from_bytes(node.start_byte(), node.end_byte());
1850
1851    // Branch on modifiers + accessor shape (REQ:R0001, R0003, R0004, R0005,
1852    // R0018, R0019). C# property semantics:
1853    //   - `const` is illegal on properties (rejected by the compiler).
1854    //   - `readonly` is illegal on properties.
1855    //   - A get-only auto-property (`{ get; }` with no setter and no body) is
1856    //     externally immutable -> Constant.
1857    //   - Computed get-only properties (`{ get { ... } }`) and
1858    //     expression-bodied properties (`=> expr`) are computed callable
1859    //     surfaces; their backing storage (if any) is mutable from the
1860    //     implementation side, so they emit Property, not Constant.
1861    //   - Property with any setter (or `init`) -> Property.
1862    // Visibility comes from the accessibility modifier; the default for a
1863    // class member is `private` (REQ:R0023). `internal` is passed through.
1864    let is_static = has_modifier(node, content, "static");
1865    let visibility = extract_field_visibility(node, content);
1866    let get_only = is_get_only_property(node);
1867
1868    let prop_id = if get_only {
1869        helper.add_constant_with_static_and_visibility(
1870            &qualified_name,
1871            Some(span),
1872            is_static,
1873            Some(visibility),
1874        )
1875    } else {
1876        helper.add_property_with_static_and_visibility(
1877            &qualified_name,
1878            Some(span),
1879            is_static,
1880            Some(visibility),
1881        )
1882    };
1883
1884    // Create TypeOf edge with full type signature, Field context, and the bare
1885    // property name (REQ:R0023 / AC-4). Cross-language byte-exact
1886    // `field:<name>` planner queries depend on the unqualified form.
1887    // (Properties are treated as fields for TypeOf context.)
1888    let type_id = helper.add_type(&type_text, None);
1889    helper.add_typeof_edge_with_context(
1890        prop_id,
1891        type_id,
1892        Some(TypeOfContext::Field),
1893        None,
1894        Some(prop_name),
1895    );
1896
1897    // Create Reference edges for all nested types
1898    for type_name in &all_type_names {
1899        let ref_type_id = helper.add_type(type_name, None);
1900        helper.add_reference_edge(prop_id, ref_type_id);
1901    }
1902}
1903
1904/// Extract the accessibility modifier from a field/property declaration.
1905///
1906/// C# class members default to `private` when no accessibility modifier is
1907/// present (REQ:R0023). `internal` is a first-class accessibility level and is
1908/// passed through verbatim. `protected internal` (more accessible than either
1909/// alone) is normalized to the canonical two-word form.
1910fn extract_field_visibility(node: Node, content: &[u8]) -> &'static str {
1911    let has_protected = has_modifier(node, content, "protected");
1912    let has_internal = has_modifier(node, content, "internal");
1913    if has_protected && has_internal {
1914        "protected internal"
1915    } else if has_modifier(node, content, "public") {
1916        "public"
1917    } else if has_protected {
1918        "protected"
1919    } else if has_internal {
1920        "internal"
1921    } else if has_modifier(node, content, "private") {
1922        "private"
1923    } else {
1924        // C# class-member default.
1925        "private"
1926    }
1927}
1928
1929/// Determine whether a property declaration is a true get-only
1930/// auto-implemented property (`{ get; }`), as distinct from a computed
1931/// getter or an expression-bodied property.
1932///
1933/// Returns `true` only for:
1934///   - An `accessor_list` containing exactly one `get` accessor with NO body
1935///     (i.e. `get;`) and no `set;` / `init;` accessor. This is C#'s
1936///     auto-implemented immutable property, externally indistinguishable from
1937///     a `readonly` field.
1938///
1939/// Returns `false` for:
1940///   - `{ get { ... } }` — computed body. The backing storage (if any) is
1941///     mutable from the implementation's perspective; the surface is a
1942///     callable, not an immutable value.
1943///   - `T X => expr;` (`arrow_expression_clause`) — expression-bodied
1944///     properties are computed getters, same rationale as above.
1945///   - Any property with a `set` or `init` accessor.
1946///   - Any property without an `accessor_list` (defensive default).
1947fn is_get_only_property(node: Node) -> bool {
1948    // Expression-bodied property: `T X => expr;` is a computed getter, NOT
1949    // an auto-implemented immutable property. Reject before scanning the
1950    // accessor list.
1951    if node
1952        .children(&mut node.walk())
1953        .any(|child| child.kind() == "arrow_expression_clause")
1954    {
1955        return false;
1956    }
1957
1958    let Some(accessor_list) = node
1959        .children(&mut node.walk())
1960        .find(|child| child.kind() == "accessor_list")
1961    else {
1962        // No accessor list and no expression body -> not classifiable as a
1963        // get-only auto-property; default to mutable.
1964        return false;
1965    };
1966
1967    let mut has_auto_get = false;
1968    let mut has_set_like = false;
1969    for accessor in accessor_list.children(&mut accessor_list.walk()) {
1970        if accessor.kind() != "accessor_declaration" {
1971            continue;
1972        }
1973
1974        // Determine the accessor keyword and whether it has a body. An
1975        // auto-implemented accessor terminates immediately with `;` (no
1976        // `block` and no `arrow_expression_clause` child). A computed
1977        // accessor carries a `block` (`get { ... }`) or
1978        // `arrow_expression_clause` (`get => expr`).
1979        let mut keyword: Option<&str> = None;
1980        let mut has_body = false;
1981        for tok in accessor.children(&mut accessor.walk()) {
1982            match tok.kind() {
1983                "get" | "set" | "init" => keyword = Some(tok.kind()),
1984                "block" | "arrow_expression_clause" => has_body = true,
1985                _ => {}
1986            }
1987        }
1988
1989        match keyword {
1990            Some("get") => {
1991                if has_body {
1992                    // Computed getter — disqualifies the property from being
1993                    // a get-only auto-property.
1994                    return false;
1995                }
1996                has_auto_get = true;
1997            }
1998            Some("set" | "init") => has_set_like = true,
1999            _ => {}
2000        }
2001    }
2002
2003    has_auto_get && !has_set_like
2004}
2005/// Process method parameters to create `TypeOf` and Reference edges.
2006///
2007/// Handles patterns like:
2008/// - `void Method(int count, string name)`
2009/// - `User Process(Repository repo, List<Item> items)`
2010///
2011/// tree-sitter-c-sharp structure:
2012/// ```text
2013/// method_declaration
2014///   return_type: predefined_type | identifier | generic_name
2015///   name: identifier
2016///   parameter_list
2017///     parameter
2018///       type: predefined_type | identifier | generic_name
2019///       name: identifier
2020/// ```
2021fn process_method_parameters(
2022    node: Node,
2023    _method_name: &str,
2024    content: &[u8],
2025    helper: &mut GraphBuildHelper,
2026) {
2027    // Get parameter_list
2028    let Some(param_list) = node.child_by_field_name("parameter_list") else {
2029        return;
2030    };
2031
2032    // Process each parameter
2033    let mut cursor = param_list.walk();
2034    let mut param_index: u16 = 0;
2035
2036    // Iterate through children - tree-sitter-c-sharp may use various node kinds for parameters
2037    for child in param_list.children(&mut cursor) {
2038        // Skip punctuation nodes (parentheses, commas)
2039        if !child.is_named() {
2040            continue;
2041        }
2042        // Most parameter-like nodes should be processed
2043        {
2044            // Extract parameter name
2045            let Some(name_node) = child.child_by_field_name("name") else {
2046                continue;
2047            };
2048            let Ok(param_name) = name_node.utf8_text(content) else {
2049                continue;
2050            };
2051
2052            // Extract parameter type
2053            let Some(type_node) = child.child_by_field_name("type") else {
2054                continue;
2055            };
2056
2057            // Extract full type signature for TypeOf edge
2058            let Some(type_text) = extract_type_string(type_node, content) else {
2059                continue;
2060            };
2061
2062            // Extract all type names for Reference edges
2063            let all_type_names = extract_all_type_names_from_annotation(type_node, content);
2064
2065            // Create parameter variable node
2066            let param_span = Span::from_bytes(child.start_byte(), child.end_byte());
2067            let param_id = helper.add_variable(param_name, Some(param_span));
2068
2069            // Create TypeOf edge with full type signature and Parameter context
2070            let type_id = helper.add_type(&type_text, None);
2071            helper.add_typeof_edge_with_context(
2072                param_id,
2073                type_id,
2074                Some(TypeOfContext::Parameter),
2075                Some(param_index),
2076                Some(param_name),
2077            );
2078
2079            // Create Reference edges for all nested types
2080            for type_name in &all_type_names {
2081                let ref_type_id = helper.add_type(type_name, None);
2082                helper.add_reference_edge(param_id, ref_type_id);
2083            }
2084
2085            param_index += 1;
2086        }
2087    }
2088}
2089
2090/// Process method return type to create `TypeOf` and Reference edges.
2091///
2092/// Handles patterns like:
2093/// - `string GetName()`
2094/// - `List<User> GetUsers()`
2095/// - `Task<Result> ProcessAsync()`
2096///
2097/// tree-sitter-c-sharp structure:
2098/// ```text
2099/// method_declaration
2100///   return_type: predefined_type | identifier | generic_name
2101///   name: identifier
2102///   parameter_list
2103/// ```
2104fn process_method_return_type(
2105    node: Node,
2106    method_name: &str,
2107    content: &[u8],
2108    helper: &mut GraphBuildHelper,
2109) {
2110    // Get return_type - C# uses "return_type", "returns", or "type" field
2111    let return_type_node = node
2112        .child_by_field_name("return_type")
2113        .or_else(|| node.child_by_field_name("returns"))
2114        .or_else(|| node.child_by_field_name("type"));
2115
2116    let Some(return_type_node) = return_type_node else {
2117        return;
2118    };
2119
2120    // Skip void return types
2121    if let Ok(type_text) = return_type_node.utf8_text(content)
2122        && type_text.trim() == "void"
2123    {
2124        return;
2125    }
2126
2127    // Extract full type signature for TypeOf edge
2128    let Some(type_text) = extract_type_string(return_type_node, content) else {
2129        return;
2130    };
2131
2132    // Extract all type names for Reference edges
2133    let all_type_names = extract_all_type_names_from_annotation(return_type_node, content);
2134
2135    // Get or find the method node ID
2136    let method_span = Span::from_bytes(node.start_byte(), node.end_byte());
2137    let method_id = helper.add_method(method_name, Some(method_span), false, false);
2138
2139    // Create TypeOf edge with full type signature and Return context
2140    let type_id = helper.add_type(&type_text, None);
2141    helper.add_typeof_edge_with_context(
2142        method_id,
2143        type_id,
2144        Some(TypeOfContext::Return),
2145        Some(0), // Return always has index 0
2146        Some(method_name),
2147    );
2148
2149    // Create Reference edges for all nested types
2150    for type_name in &all_type_names {
2151        let ref_type_id = helper.add_type(type_name, None);
2152        helper.add_reference_edge(method_id, ref_type_id);
2153    }
2154}
2155
2156// ============================================================================
2157// Generic Type Parameter Emission (REQ:R0028 / U19 — C2_GEN_TP_CSHARP)
2158// ============================================================================
2159
2160/// Emit per-type-parameter `Type` nodes and `TypeOf{Constraint}` edges
2161/// for a C# generic declaration (class, interface, or method).
2162///
2163/// Handles all three shapes that carry a `type_parameter_list` child on
2164/// tree-sitter-c-sharp declarations:
2165///
2166/// 1. `class_declaration`     → `<namespace>.<ClassName>.<ParamName>`
2167/// 2. `interface_declaration` → `<namespace>.<InterfaceName>.<ParamName>`
2168/// 3. `method_declaration`    → `<namespace>.<ClassName>.<MethodName>.<ParamName>`
2169///
2170/// Tree-sitter-c-sharp grammar shape:
2171///
2172/// ```text
2173/// type_parameter_list:
2174///   '<' commaSep1(type_parameter) '>'
2175/// type_parameter:
2176///   repeat(attribute_list)
2177///   optional('in' | 'out')      // variance — emit base node, attribute deferred
2178///   name: identifier
2179/// type_parameter_constraints_clause:
2180///   'where' identifier ':' commaSep1(type_parameter_constraint)
2181/// type_parameter_constraint:
2182///   class_constraint  ('class')
2183///   | struct_constraint ('struct')
2184///   | unmanaged ('unmanaged')
2185///   | notnull ('notnull')
2186///   | constructor_constraint ('new()')
2187///   | type: <named-type>
2188/// ```
2189///
2190/// In the live tree-sitter-c-sharp 0.23.x grammar `class`/`struct`/
2191/// `unmanaged`/`notnull` arrive as **unnamed** keyword children of the
2192/// `type_parameter_constraint` node (not as separate named rules).
2193/// `constructor_constraint` is named. The bound type itself comes
2194/// through the `type` field on `type_parameter_constraint` and may be
2195/// an `identifier`, `qualified_name`, `generic_name`, or
2196/// `nullable_type` etc. — handled by `extract_constraint_base_type_name`.
2197///
2198/// AC-5: variance modifiers (`in`/`out`) are recorded on the parameter
2199/// only via the base Type-node emission. The variance attribute itself
2200/// is deferred to a future `EdgeKind` extension.
2201fn process_type_parameter_declarations(
2202    decl_node: Node,
2203    content: &[u8],
2204    parent_qualified_name: &str,
2205    helper: &mut GraphBuildHelper,
2206) {
2207    // type_parameter_list is an unnamed-position child of the
2208    // declaration (no `type_parameters` field name in c-sharp grammar).
2209    let mut decl_cursor = decl_node.walk();
2210    let Some(params_node) = decl_node
2211        .children(&mut decl_cursor)
2212        .find(|c| c.kind() == "type_parameter_list")
2213    else {
2214        return;
2215    };
2216
2217    // Map parameter-name -> NodeId so where-clauses can target the
2218    // right parameter Type node by identifier match.
2219    let mut param_ids: HashMap<String, sqry_core::graph::unified::node::NodeId> = HashMap::new();
2220
2221    let mut params_cursor = params_node.walk();
2222    for param_node in params_node.children(&mut params_cursor) {
2223        if param_node.kind() != "type_parameter" {
2224            continue;
2225        }
2226
2227        // Parameter name lives under the `name` field.
2228        let Some(name_node) = param_node.child_by_field_name("name") else {
2229            continue;
2230        };
2231        let Ok(param_name) = name_node.utf8_text(content) else {
2232            continue;
2233        };
2234
2235        let qualified_param = format!("{parent_qualified_name}.{param_name}");
2236        let span = Span::from_bytes(name_node.start_byte(), name_node.end_byte());
2237        // AC-2: span anchored on the parameter identifier so
2238        // "Find Definition" / hover navigation lands on the declaration
2239        // site rather than the synthetic `(0, 0)` sentinel.
2240        let param_id = helper.add_type(&qualified_param, Some(span));
2241        param_ids.insert(param_name.to_string(), param_id);
2242    }
2243
2244    // AC-3 / AC-4: walk every `type_parameter_constraints_clause`
2245    // sibling on the same declaration node and emit one
2246    // `TypeOf{Constraint}` edge per constraint entry.
2247    let mut clause_cursor = decl_node.walk();
2248    for clause_node in decl_node.children(&mut clause_cursor) {
2249        if clause_node.kind() != "type_parameter_constraints_clause" {
2250            continue;
2251        }
2252        emit_type_parameter_constraint_clause(clause_node, content, &param_ids, helper);
2253    }
2254}
2255
2256/// Emit `TypeOf{Constraint}` edges for one
2257/// `type_parameter_constraints_clause`.
2258///
2259/// The clause shape is:
2260/// ```text
2261/// type_parameter_constraints_clause:
2262///   'where' identifier ':' commaSep1(type_parameter_constraint)
2263/// ```
2264///
2265/// The leading `identifier` child (after the `where` keyword) names the
2266/// type parameter being constrained. We match it against the parameter
2267/// IDs collected during the `type_parameter_list` walk; an unknown
2268/// parameter is skipped silently (defensive — the grammar guarantees
2269/// it matches one of the declared parameters).
2270fn emit_type_parameter_constraint_clause(
2271    clause_node: Node,
2272    content: &[u8],
2273    param_ids: &HashMap<String, sqry_core::graph::unified::node::NodeId>,
2274    helper: &mut GraphBuildHelper,
2275) {
2276    // The first named child is the parameter identifier (the `where`
2277    // keyword and `:` are unnamed). Subsequent named children are
2278    // `type_parameter_constraint` nodes.
2279    let mut cursor = clause_node.walk();
2280    let mut named_children = clause_node
2281        .children(&mut cursor)
2282        .filter(tree_sitter::Node::is_named);
2283
2284    let Some(param_id_node) = named_children.next() else {
2285        return;
2286    };
2287    if param_id_node.kind() != "identifier" {
2288        return;
2289    }
2290    let Ok(param_name) = param_id_node.utf8_text(content) else {
2291        return;
2292    };
2293    let Some(&param_id) = param_ids.get(param_name) else {
2294        // Constraint clause references a parameter we did not emit —
2295        // skip silently. (Should not happen for well-formed C#.)
2296        return;
2297    };
2298
2299    for constraint_node in named_children {
2300        if constraint_node.kind() != "type_parameter_constraint" {
2301            continue;
2302        }
2303        let Some(constraint_target_name) = extract_constraint_target_name(constraint_node, content)
2304        else {
2305            continue;
2306        };
2307        let constraint_id = helper.add_type(&constraint_target_name, None);
2308        helper.add_typeof_edge_with_context(
2309            param_id,
2310            constraint_id,
2311            Some(TypeOfContext::Constraint),
2312            None,
2313            None,
2314        );
2315    }
2316}
2317
2318/// Extract the constraint target identifier from a single
2319/// `type_parameter_constraint` node.
2320///
2321/// Constraint shapes recognised:
2322///
2323/// - **Synthetic keywords**: `class`, `struct`, `unmanaged`, `notnull`
2324///   appear as **unnamed** keyword children → return the keyword text
2325///   verbatim. These become interned synthetic Type nodes named
2326///   exactly `class` / `struct` / `unmanaged` / `notnull`.
2327/// - **`constructor_constraint`** (`new()`) is a named child → return
2328///   the literal string `"new()"` as the synthetic target name.
2329/// - **Named-type bound** (`where T : IFoo`, `where T : Comparable<T>`):
2330///   the `type` field carries an `identifier`, `qualified_name`,
2331///   `generic_name`, or `nullable_type` — return the base name with
2332///   any generic argument list stripped (so `Comparable<T>` becomes
2333///   `Comparable`). Cross-file unification (Phase 4c-prime) collapses
2334///   the synthetic stub into the canonical declaration when one exists.
2335fn extract_constraint_target_name(constraint_node: Node, content: &[u8]) -> Option<String> {
2336    // Iterate all children: first check for the named `constructor_constraint`,
2337    // then the `type` field, then fall back to unnamed keyword tokens.
2338    let mut cursor = constraint_node.walk();
2339    let mut had_named_constructor = false;
2340    let mut keyword_token: Option<&'static str> = None;
2341
2342    for child in constraint_node.children(&mut cursor) {
2343        match child.kind() {
2344            "constructor_constraint" => {
2345                had_named_constructor = true;
2346            }
2347            "class" if !child.is_named() => {
2348                keyword_token = Some("class");
2349            }
2350            "struct" if !child.is_named() => {
2351                keyword_token = Some("struct");
2352            }
2353            "unmanaged" if !child.is_named() => {
2354                keyword_token = Some("unmanaged");
2355            }
2356            "notnull" if !child.is_named() => {
2357                keyword_token = Some("notnull");
2358            }
2359            _ => {}
2360        }
2361    }
2362
2363    if had_named_constructor {
2364        return Some("new()".to_string());
2365    }
2366    if let Some(kw) = keyword_token {
2367        return Some(kw.to_string());
2368    }
2369
2370    // Otherwise the bound is a named type carried by the `type` field
2371    // (identifier, qualified_name, generic_name, nullable_type, ...).
2372    let type_node = constraint_node.child_by_field_name("type")?;
2373    Some(extract_constraint_base_type_name(type_node, content))
2374}
2375
2376/// Extract the base name from a constraint bound type, stripping any
2377/// generic type-argument list.
2378///
2379/// `where T : Comparable<T>` should produce a `Comparable` Type node
2380/// (not `Comparable<T>`), matching the Java implementation's behaviour
2381/// in `extract_bound_type_base_name`. This lets cross-file unification
2382/// collapse the synthetic stub into the canonical `Comparable`
2383/// declaration when one exists in the same workspace.
2384fn extract_constraint_base_type_name(type_node: Node, content: &[u8]) -> String {
2385    match type_node.kind() {
2386        "generic_name" => {
2387            // `generic_name` has shape: identifier type_argument_list.
2388            // Take the leading identifier as the base name.
2389            let mut cursor = type_node.walk();
2390            for child in type_node.children(&mut cursor) {
2391                if matches!(child.kind(), "identifier" | "qualified_name") {
2392                    return extract_constraint_base_type_name(child, content);
2393                }
2394            }
2395            type_node.utf8_text(content).unwrap_or_default().to_string()
2396        }
2397        "qualified_name" => {
2398            // `qualified_name` may end in a `generic_name` (e.g.
2399            // `System.Collections.Generic.IEnumerable<T>`). Recurse into
2400            // the trailing child to strip the type-argument list while
2401            // preserving the dotted prefix.
2402            let mut cursor = type_node.walk();
2403            let children: Vec<_> = type_node
2404                .children(&mut cursor)
2405                .filter(tree_sitter::Node::is_named)
2406                .collect();
2407            // Last named child is the leaf: identifier or generic_name.
2408            if let Some(last) = children.last()
2409                && last.kind() == "generic_name"
2410            {
2411                // Reconstruct dotted prefix + stripped leaf base.
2412                let prefix_segments: Vec<String> = children
2413                    .iter()
2414                    .take(children.len() - 1)
2415                    .filter_map(|c| c.utf8_text(content).ok().map(str::to_string))
2416                    .collect();
2417                let leaf_base = extract_constraint_base_type_name(*last, content);
2418                if prefix_segments.is_empty() {
2419                    return leaf_base;
2420                }
2421                return format!("{}.{}", prefix_segments.join("."), leaf_base);
2422            }
2423            type_node.utf8_text(content).unwrap_or_default().to_string()
2424        }
2425        "nullable_type" => {
2426            // `T?` — strip the trailing `?` by recursing into the inner
2427            // type (first named child).
2428            let mut cursor = type_node.walk();
2429            for child in type_node.children(&mut cursor) {
2430                if child.is_named() {
2431                    return extract_constraint_base_type_name(child, content);
2432                }
2433            }
2434            type_node.utf8_text(content).unwrap_or_default().to_string()
2435        }
2436        _ => type_node.utf8_text(content).unwrap_or_default().to_string(),
2437    }
2438}