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;
23use std::sync::OnceLock;
24
25use sqry_core::graph::unified::build::shape::{CfBucket, ShapeMapping};
26use sqry_core::graph::unified::edge::FfiConvention;
27use sqry_core::graph::unified::edge::kind::TypeOfContext;
28use sqry_core::graph::unified::storage::shape::SignatureShape;
29use sqry_core::graph::unified::{GraphBuildHelper, NodeId, StagingGraph};
30use sqry_core::graph::{
31    GraphBuilder, GraphBuilderError, GraphResult, GraphSnapshot, Language, Span,
32};
33use tree_sitter::{Node, Tree};
34
35use super::local_scopes;
36use super::type_extractor::{extract_all_type_names_from_annotation, extract_type_string};
37
38const DEFAULT_MAX_SCOPE_DEPTH: usize = 6;
39
40/// File-level module name for exports.
41/// Distinct from `<module>` to avoid node kind collision in `GraphBuildHelper` cache.
42const FILE_MODULE_NAME: &str = "<file_module>";
43
44/// Graph builder for C# files.
45#[derive(Debug, Clone, Copy)]
46pub struct CSharpGraphBuilder {
47    max_scope_depth: usize,
48}
49
50impl Default for CSharpGraphBuilder {
51    fn default() -> Self {
52        Self {
53            max_scope_depth: DEFAULT_MAX_SCOPE_DEPTH,
54        }
55    }
56}
57
58impl CSharpGraphBuilder {
59    #[must_use]
60    pub fn new(max_scope_depth: usize) -> Self {
61        Self { max_scope_depth }
62    }
63}
64
65impl GraphBuilder for CSharpGraphBuilder {
66    fn build_graph(
67        &self,
68        tree: &Tree,
69        content: &[u8],
70        file: &Path,
71        staging: &mut StagingGraph,
72    ) -> GraphResult<()> {
73        let mut helper = GraphBuildHelper::new(staging, file, Language::CSharp);
74
75        // Build AST context for O(1) function lookups
76        let ast_graph = ASTGraph::from_tree(tree, content, self.max_scope_depth).map_err(|e| {
77            GraphBuilderError::ParseError {
78                span: Span::default(),
79                reason: e,
80            }
81        })?;
82
83        // Map qualified names to NodeIds for call edge creation
84        let mut node_map = HashMap::new();
85
86        // Phase 1: Create function/method/class/interface nodes
87        for context in ast_graph.contexts() {
88            let qualified_name = &context.qualified_name;
89            let span = Span::from_bytes(context.span.0, context.span.1);
90
91            let node_id = match context.kind {
92                ContextKind::Function { is_async } => {
93                    // Use add_function_with_signature for returns: queries
94                    helper.add_function_with_signature(
95                        qualified_name,
96                        Some(span),
97                        is_async,
98                        false,
99                        None, // visibility
100                        context.return_type.as_deref(),
101                    )
102                }
103                ContextKind::Method {
104                    is_async,
105                    is_static,
106                } => {
107                    // Use add_method_with_signature for returns: queries
108                    helper.add_method_with_signature(
109                        qualified_name,
110                        Some(span),
111                        is_async,
112                        is_static,
113                        None, // visibility
114                        context.return_type.as_deref(),
115                    )
116                }
117                ContextKind::Class => helper.add_class(qualified_name, Some(span)),
118                ContextKind::Interface => helper.add_interface(qualified_name, Some(span)),
119            };
120            // Every arm above materializes a real source declaration (function,
121            // method, class, interface). Opt the dual-use bare add_class/
122            // add_interface helpers into is_definition = true (issue #394); the
123            // _with_signature function/method variants are already definition.
124            helper.mark_definition(node_id);
125            node_map.insert(qualified_name.clone(), node_id);
126        }
127
128        // Build local scope tree for variable reference resolution
129        let mut scope_tree = local_scopes::build(tree.root_node(), content)?;
130
131        // Phase 2: Walk the tree to find calls and OOP edges
132        // Track namespace and class context for qualified naming
133        let mut namespace_stack = Vec::new();
134        let mut class_stack = Vec::new();
135        let root = tree.root_node();
136        walk_tree_for_edges(
137            root,
138            content,
139            &ast_graph,
140            &mut helper,
141            &mut node_map,
142            &mut namespace_stack,
143            &mut class_stack,
144            &mut scope_tree,
145        )?;
146
147        Ok(())
148    }
149
150    fn language(&self) -> Language {
151        Language::CSharp
152    }
153
154    fn shape_mapping(&self) -> Option<&dyn ShapeMapping> {
155        Some(csharp_shape_mapping())
156    }
157
158    fn detect_cross_language_edges(
159        &self,
160        _snapshot: &GraphSnapshot,
161    ) -> GraphResult<Vec<sqry_core::graph::CodeEdge>> {
162        // P/Invoke detection is now handled in build_graph() via process_pinvoke_method()
163        // This method is required by the trait interface
164        Ok(vec![])
165    }
166}
167
168/// Per-language [`ShapeMapping`] for C#: a precomputed `kind_id -> CfBucket` table
169/// over the tree-sitter-c-sharp grammar, shared process-wide via
170/// [`csharp_shape_mapping`]. Mirrors the C reference impl: a single array index
171/// per node on the hot shape walk, identifier-blind throughout.
172pub struct CSharpShapeMapping {
173    cf_by_kind_id: Vec<Option<CfBucket>>,
174}
175
176impl CSharpShapeMapping {
177    fn build() -> Self {
178        let lang: tree_sitter::Language = tree_sitter_c_sharp::LANGUAGE.into();
179        let count = lang.node_kind_count();
180        let mut cf_by_kind_id = vec![None; count];
181        for (id, slot) in cf_by_kind_id.iter_mut().enumerate() {
182            let Ok(kind_id) = u16::try_from(id) else {
183                break;
184            };
185            if !lang.node_kind_is_named(kind_id) {
186                continue;
187            }
188            if let Some(name) = lang.node_kind_for_id(kind_id) {
189                *slot = cf_bucket_for_csharp_kind(name);
190            }
191        }
192        Self { cf_by_kind_id }
193    }
194}
195
196impl ShapeMapping for CSharpShapeMapping {
197    fn cf_bucket(&self, ts_node_kind_id: u16) -> Option<CfBucket> {
198        self.cf_by_kind_id
199            .get(ts_node_kind_id as usize)
200            .copied()
201            .flatten()
202    }
203
204    fn signature_shape(&self, fn_node: Node, src: &[u8]) -> SignatureShape {
205        let mut shape = SignatureShape::default();
206        // A `method_declaration` exposes its parameters through the `parameters`
207        // field (a `parameter_list`), holding `parameter` children. The `type` and
208        // `name` fields are the structural slots; a default value (`= literal`)
209        // shows up as one extra non-field named child of any expression kind, and a
210        // `params` varargs tail surfaces as a `modifier` child spelled `params`.
211        if let Some(params) = fn_node.child_by_field_name("parameters") {
212            let mut cursor = params.walk();
213            for child in params.named_children(&mut cursor) {
214                if child.kind() != "parameter" {
215                    continue;
216                }
217                shape.arity_positional = shape.arity_positional.saturating_add(1);
218                let type_node = child.child_by_field_name("type");
219                let name_node = child.child_by_field_name("name");
220                let mut pc = child.walk();
221                for part in child.named_children(&mut pc) {
222                    if Some(part) == type_node || Some(part) == name_node {
223                        continue;
224                    }
225                    match part.kind() {
226                        "attribute_list" => {}
227                        "modifier" => {
228                            if part.utf8_text(src).map(str::trim) == Ok("params") {
229                                shape.has_varargs = true;
230                            }
231                        }
232                        // Whatever remains is the default-value expression.
233                        _ => shape.has_defaults = true,
234                    }
235                }
236            }
237        }
238        // The return type lives in the `returns` field of `method_declaration`.
239        shape.has_return_annotation = fn_node.child_by_field_name("returns").is_some();
240        shape
241    }
242}
243
244/// Map one tree-sitter-c-sharp grammar node-kind name to its canonical
245/// control-flow bucket. Additive-only against the frozen [`CfBucket`] set.
246fn cf_bucket_for_csharp_kind(name: &str) -> Option<CfBucket> {
247    let bucket = match name {
248        "if_statement"
249        | "conditional_expression"
250        | "when_clause"
251        | "preproc_if"
252        | "preproc_elif" => CfBucket::Branch,
253        "for_statement" | "foreach_statement" | "while_statement" | "do_statement" => {
254            CfBucket::Loop
255        }
256        "switch_statement" | "switch_expression" | "switch_section" | "switch_expression_arm" => {
257            CfBucket::Match
258        }
259        "try_statement" => CfBucket::Try,
260        "catch_clause" => CfBucket::Catch,
261        "throw_statement" | "throw_expression" => CfBucket::Throw,
262        "using_statement" | "finally_clause" => CfBucket::Resource,
263        "return_statement" => CfBucket::Return,
264        "yield_statement" => CfBucket::Yield,
265        "await_expression" => CfBucket::Await,
266        "break_statement" | "continue_statement" | "goto_statement" => CfBucket::BreakContinue,
267        "invocation_expression" | "object_creation_expression" => CfBucket::Call,
268        "assignment_expression" | "variable_declaration" | "local_declaration_statement" => {
269            CfBucket::Assign
270        }
271        "lambda_expression" | "anonymous_method_expression" => CfBucket::Closure,
272        _ => return None,
273    };
274    Some(bucket)
275}
276
277/// The process-wide C# shape mapping, built once on first use.
278#[must_use]
279pub fn csharp_shape_mapping() -> &'static CSharpShapeMapping {
280    static MAPPING: OnceLock<CSharpShapeMapping> = OnceLock::new();
281    MAPPING.get_or_init(CSharpShapeMapping::build)
282}
283
284// ============================================================================
285// AST Graph - tracks callable contexts (functions, methods, classes)
286// ============================================================================
287
288#[derive(Debug, Clone)]
289enum ContextKind {
290    Function { is_async: bool },
291    Method { is_async: bool, is_static: bool },
292    Class,
293    Interface,
294}
295
296#[derive(Debug, Clone)]
297struct CallContext {
298    qualified_name: String,
299    span: (usize, usize),
300    kind: ContextKind,
301    class_name: Option<String>,
302    /// Return type of the method/function (e.g., `Task<User>`, `void`)
303    return_type: Option<String>,
304}
305
306struct ASTGraph {
307    contexts: Vec<CallContext>,
308    node_to_context: HashMap<usize, usize>,
309}
310
311impl ASTGraph {
312    fn from_tree(tree: &Tree, content: &[u8], max_depth: usize) -> Result<Self, String> {
313        let mut contexts = Vec::new();
314        let mut node_to_context = HashMap::new();
315        let mut scope_stack: Vec<String> = Vec::new();
316        let mut class_stack: Vec<String> = Vec::new();
317
318        // Create recursion guard
319        let recursion_limits = sqry_core::config::RecursionLimits::load_or_default()
320            .map_err(|e| format!("Failed to load recursion limits: {e}"))?;
321        let file_ops_depth = recursion_limits
322            .effective_file_ops_depth()
323            .map_err(|e| format!("Invalid file_ops_depth configuration: {e}"))?;
324        let mut guard = sqry_core::query::security::RecursionGuard::new(file_ops_depth)
325            .map_err(|e| format!("Failed to create recursion guard: {e}"))?;
326
327        let mut walk_context = WalkContext {
328            content,
329            contexts: &mut contexts,
330            node_to_context: &mut node_to_context,
331            scope_stack: &mut scope_stack,
332            class_stack: &mut class_stack,
333            max_depth,
334            guard: &mut guard,
335        };
336
337        walk_ast(tree.root_node(), &mut walk_context)?;
338
339        Ok(Self {
340            contexts,
341            node_to_context,
342        })
343    }
344
345    fn contexts(&self) -> &[CallContext] {
346        &self.contexts
347    }
348
349    fn get_callable_context(&self, node_id: usize) -> Option<&CallContext> {
350        self.node_to_context
351            .get(&node_id)
352            .and_then(|idx| self.contexts.get(*idx))
353    }
354}
355
356#[allow(clippy::too_many_lines)] // Central traversal; refactor after API stabilization.
357/// # Errors
358///
359/// Returns error if recursion depth exceeds the guard's limit.
360struct WalkContext<'a> {
361    content: &'a [u8],
362    contexts: &'a mut Vec<CallContext>,
363    node_to_context: &'a mut HashMap<usize, usize>,
364    scope_stack: &'a mut Vec<String>,
365    class_stack: &'a mut Vec<String>,
366    max_depth: usize,
367    guard: &'a mut sqry_core::query::security::RecursionGuard,
368}
369
370#[allow(clippy::too_many_lines)]
371fn walk_ast(node: Node, context: &mut WalkContext<'_>) -> Result<(), String> {
372    context
373        .guard
374        .enter()
375        .map_err(|e| format!("Recursion limit exceeded: {e}"))?;
376
377    if context.scope_stack.len() > context.max_depth {
378        context.guard.exit();
379        return Ok(());
380    }
381
382    match node.kind() {
383        "class_declaration" => {
384            let name_node = node
385                .child_by_field_name("name")
386                .ok_or_else(|| "class_declaration missing name".to_string())?;
387            let class_name = name_node
388                .utf8_text(context.content)
389                .map_err(|_| "failed to read class name".to_string())?;
390
391            // Build qualified class name
392            let qualified_class = if context.scope_stack.is_empty() {
393                class_name.to_string()
394            } else {
395                format!("{}.{}", context.scope_stack.join("."), class_name)
396            };
397
398            context.class_stack.push(qualified_class.clone());
399            context.scope_stack.push(class_name.to_string());
400
401            // Add class context
402            let _context_idx = context.contexts.len();
403            context.contexts.push(CallContext {
404                qualified_name: qualified_class.clone(),
405                span: (node.start_byte(), node.end_byte()),
406                kind: ContextKind::Class,
407                class_name: Some(qualified_class),
408                return_type: None,
409            });
410
411            // Recurse into class body
412            if let Some(body) = node.child_by_field_name("body") {
413                let mut cursor = body.walk();
414                for child in body.children(&mut cursor) {
415                    walk_ast(child, context)?;
416                }
417            }
418
419            context.class_stack.pop();
420            context.scope_stack.pop();
421        }
422        "interface_declaration" => {
423            let name_node = node
424                .child_by_field_name("name")
425                .ok_or_else(|| "interface_declaration missing name".to_string())?;
426            let interface_name = name_node
427                .utf8_text(context.content)
428                .map_err(|_| "failed to read interface name".to_string())?;
429
430            // Build qualified interface name
431            let qualified_interface = if context.scope_stack.is_empty() {
432                interface_name.to_string()
433            } else {
434                format!("{}.{}", context.scope_stack.join("."), interface_name)
435            };
436
437            context.class_stack.push(qualified_interface.clone());
438            context.scope_stack.push(interface_name.to_string());
439
440            // Add interface context
441            let _context_idx = context.contexts.len();
442            context.contexts.push(CallContext {
443                qualified_name: qualified_interface.clone(),
444                span: (node.start_byte(), node.end_byte()),
445                kind: ContextKind::Interface,
446                class_name: Some(qualified_interface),
447                return_type: None,
448            });
449
450            // Recurse into interface body
451            if let Some(body) = node.child_by_field_name("body") {
452                let mut cursor = body.walk();
453                for child in body.children(&mut cursor) {
454                    walk_ast(child, context)?;
455                }
456            }
457
458            context.class_stack.pop();
459            context.scope_stack.pop();
460        }
461        "method_declaration" | "constructor_declaration" | "local_function_statement" => {
462            let name_node = node
463                .child_by_field_name("name")
464                .ok_or_else(|| format!("{} missing name", node.kind()).to_string())?;
465            let func_name = name_node
466                .utf8_text(context.content)
467                .map_err(|_| "failed to read function name".to_string())?;
468
469            // Check if async
470            let is_async = has_modifier(node, context.content, "async");
471
472            // Check if static method
473            let is_static = has_modifier(node, context.content, "static");
474
475            // Extract return type (tree-sitter-c-sharp may use return_type, returns, or type)
476            let return_type = node
477                .child_by_field_name("return_type")
478                .or_else(|| node.child_by_field_name("returns"))
479                .or_else(|| node.child_by_field_name("type"))
480                .and_then(|type_node| type_node.utf8_text(context.content).ok())
481                .map(std::string::ToString::to_string);
482
483            // Build qualified function name
484            let qualified_func = if context.scope_stack.is_empty() {
485                func_name.to_string()
486            } else {
487                format!("{}.{}", context.scope_stack.join("."), func_name)
488            };
489
490            // Determine if this is a method (inside a class/interface)
491            let is_method = !context.class_stack.is_empty();
492            let class_name = context.class_stack.last().cloned();
493
494            let kind = if is_method {
495                ContextKind::Method {
496                    is_async,
497                    is_static,
498                }
499            } else {
500                ContextKind::Function { is_async }
501            };
502
503            let context_idx = context.contexts.len();
504            context.contexts.push(CallContext {
505                qualified_name: qualified_func.clone(),
506                span: (node.start_byte(), node.end_byte()),
507                kind,
508                class_name,
509                return_type,
510            });
511
512            // Associate all descendants with this context
513            if let Some(body) = node.child_by_field_name("body") {
514                associate_descendants(body, context_idx, context.node_to_context);
515            }
516
517            context.scope_stack.push(func_name.to_string());
518
519            // Recurse into function body to find nested functions
520            if let Some(body) = node.child_by_field_name("body") {
521                let mut cursor = body.walk();
522                for child in body.children(&mut cursor) {
523                    walk_ast(child, context)?;
524                }
525            }
526
527            context.scope_stack.pop();
528        }
529        "namespace_declaration" => {
530            // Extract namespace name
531            if let Some(name_node) = node.child_by_field_name("name")
532                && let Ok(namespace_name) = name_node.utf8_text(context.content)
533            {
534                context.scope_stack.push(namespace_name.to_string());
535
536                // Recurse into namespace body
537                if let Some(body) = node.child_by_field_name("body") {
538                    let mut cursor = body.walk();
539                    for child in body.children(&mut cursor) {
540                        walk_ast(child, context)?;
541                    }
542                }
543
544                context.scope_stack.pop();
545            }
546        }
547        _ => {
548            // Recurse into children for other node types
549            let mut cursor = node.walk();
550            for child in node.children(&mut cursor) {
551                walk_ast(child, context)?;
552            }
553        }
554    }
555
556    context.guard.exit();
557    Ok(())
558}
559
560fn associate_descendants(
561    node: Node,
562    context_idx: usize,
563    node_to_context: &mut HashMap<usize, usize>,
564) {
565    node_to_context.insert(node.id(), context_idx);
566
567    let mut stack = vec![node];
568    while let Some(current) = stack.pop() {
569        node_to_context.insert(current.id(), context_idx);
570
571        let mut cursor = current.walk();
572        for child in current.children(&mut cursor) {
573            stack.push(child);
574        }
575    }
576}
577
578// ============================================================================
579// Edge Building - calls, imports, inheritance, implements
580// ============================================================================
581
582/// Walk the AST tree to create edges (calls, imports, inheritance, implements)
583/// Tracks namespace and class context for qualified naming.
584#[allow(clippy::too_many_lines)] // Central traversal; refactor after API stabilization.
585fn walk_tree_for_edges(
586    node: Node,
587    content: &[u8],
588    ast_graph: &ASTGraph,
589    helper: &mut GraphBuildHelper,
590    node_map: &mut HashMap<String, NodeId>,
591    namespace_stack: &mut Vec<String>,
592    class_stack: &mut Vec<String>,
593    scope_tree: &mut local_scopes::CSharpScopeTree,
594) -> GraphResult<()> {
595    match node.kind() {
596        "namespace_declaration" => {
597            // Track namespace context
598            if let Some(name_node) = node.child_by_field_name("name")
599                && let Ok(namespace_name) = name_node.utf8_text(content)
600            {
601                namespace_stack.push(namespace_name.to_string());
602
603                // Recurse into namespace body
604                if let Some(body) = node.child_by_field_name("body") {
605                    let mut cursor = body.walk();
606                    for child in body.children(&mut cursor) {
607                        walk_tree_for_edges(
608                            child,
609                            content,
610                            ast_graph,
611                            helper,
612                            node_map,
613                            namespace_stack,
614                            class_stack,
615                            scope_tree,
616                        )?;
617                    }
618                }
619
620                namespace_stack.pop();
621                return Ok(());
622            }
623        }
624        "class_declaration" => {
625            // Track class context and process OOP edges
626            if let Some(name_node) = node.child_by_field_name("name")
627                && let Ok(class_name) = name_node.utf8_text(content)
628            {
629                // Build qualified class name
630                let qualified_class =
631                    build_qualified_name(namespace_stack, class_stack, class_name);
632                class_stack.push(class_name.to_string());
633
634                // Process OOP edges with qualified name, passing namespace context for base type resolution
635                process_class_declaration(
636                    node,
637                    content,
638                    helper,
639                    node_map,
640                    &qualified_class,
641                    namespace_stack,
642                );
643
644                // Emit per-type-parameter Type nodes + where-clause
645                // Constraint edges (REQ:R0028 / U19 AC-1, AC-2, AC-3,
646                // AC-4, AC-5).
647                process_type_parameter_declarations(node, content, &qualified_class, helper);
648
649                // Export class if it has public or internal visibility
650                if should_export(node, content)
651                    && let Some(class_id) = node_map.get(&qualified_class)
652                {
653                    export_from_file_module(helper, *class_id);
654                }
655
656                // Recurse into class body to handle method exports
657                if let Some(body) = node.child_by_field_name("body") {
658                    process_class_member_exports(body, content, &qualified_class, helper, node_map);
659
660                    let mut cursor = body.walk();
661                    for child in body.children(&mut cursor) {
662                        walk_tree_for_edges(
663                            child,
664                            content,
665                            ast_graph,
666                            helper,
667                            node_map,
668                            namespace_stack,
669                            class_stack,
670                            scope_tree,
671                        )?;
672                    }
673                }
674
675                class_stack.pop();
676                return Ok(());
677            }
678        }
679        "interface_declaration" => {
680            // Track interface context and process OOP edges
681            if let Some(name_node) = node.child_by_field_name("name")
682                && let Ok(interface_name) = name_node.utf8_text(content)
683            {
684                // Build qualified interface name
685                let qualified_interface =
686                    build_qualified_name(namespace_stack, class_stack, interface_name);
687
688                // Process OOP edges with qualified name, passing namespace context for base type resolution
689                process_interface_declaration(
690                    node,
691                    content,
692                    helper,
693                    node_map,
694                    &qualified_interface,
695                    namespace_stack,
696                );
697
698                // Emit per-type-parameter Type nodes + where-clause
699                // Constraint edges (REQ:R0028 / U19 AC-1, AC-2, AC-3,
700                // AC-4, AC-5).
701                process_type_parameter_declarations(node, content, &qualified_interface, helper);
702
703                // Export interface if it has public or internal visibility
704                if should_export(node, content)
705                    && let Some(interface_id) = node_map.get(&qualified_interface)
706                {
707                    export_from_file_module(helper, *interface_id);
708                }
709
710                // Process interface method exports and recurse into the
711                // interface body so generic interface methods reach the
712                // method_declaration arm of this walker (REQ:R0028 / U19
713                // AC-1: interface-method type-parameters + where-clause
714                // Constraint edges). The interface name is pushed onto
715                // class_stack for the duration of the body walk so the
716                // method walker builds the correct
717                // `<namespace>.<InterfaceName>.<MethodName>.<ParamName>`
718                // qualified name.
719                if let Some(body) = node.child_by_field_name("body") {
720                    process_interface_member_exports(
721                        body,
722                        content,
723                        &qualified_interface,
724                        helper,
725                        node_map,
726                    );
727
728                    class_stack.push(interface_name.to_string());
729                    let mut cursor = body.walk();
730                    for child in body.children(&mut cursor) {
731                        walk_tree_for_edges(
732                            child,
733                            content,
734                            ast_graph,
735                            helper,
736                            node_map,
737                            namespace_stack,
738                            class_stack,
739                            scope_tree,
740                        )?;
741                    }
742                    class_stack.pop();
743                }
744
745                return Ok(());
746            }
747        }
748        "invocation_expression" => {
749            process_invocation(node, content, ast_graph, helper, node_map);
750        }
751        "object_creation_expression" => {
752            process_object_creation(node, content, ast_graph, helper, node_map);
753        }
754        "using_directive" => {
755            process_using_directive(node, content, helper);
756        }
757        "method_declaration" => {
758            // Check for P/Invoke (DllImport attribute + extern modifier)
759            process_pinvoke_method(node, content, helper, node_map, namespace_stack);
760
761            // Process method parameters and return type for TypeOf edges
762            if let Some(name_node) = node.child_by_field_name("name")
763                && let Ok(method_name) = name_node.utf8_text(content)
764            {
765                // Build qualified method name with namespace and class context
766                // Must match format from extract_callable_context (scope_stack.join("."))
767                let mut scope_parts = namespace_stack.clone();
768                scope_parts.extend(class_stack.iter().cloned());
769
770                let qualified_name = if scope_parts.is_empty() {
771                    method_name.to_string()
772                } else {
773                    format!("{}.{}", scope_parts.join("."), method_name)
774                };
775
776                process_method_parameters(node, &qualified_name, content, helper);
777                process_method_return_type(node, &qualified_name, content, helper);
778
779                // Emit per-type-parameter Type nodes + where-clause
780                // Constraint edges for generic methods (REQ:R0028 /
781                // U19 AC-1, AC-2, AC-3, AC-4).
782                process_type_parameter_declarations(node, content, &qualified_name, helper);
783            }
784        }
785        "local_declaration_statement" => {
786            // Process local variable declarations with TypeOf edges
787            process_local_variables(node, content, helper, class_stack);
788        }
789        "field_declaration" => {
790            // Process field declarations with TypeOf edges
791            process_field_declaration(node, content, helper, class_stack);
792        }
793        "property_declaration" => {
794            // Process property declarations with TypeOf edges
795            process_property_declaration(node, content, helper, class_stack);
796        }
797        "identifier" => {
798            local_scopes::handle_identifier_for_reference(node, content, scope_tree, helper);
799        }
800        _ => {}
801    }
802
803    // Recurse into children
804    let mut cursor = node.walk();
805    for child in node.children(&mut cursor) {
806        walk_tree_for_edges(
807            child,
808            content,
809            ast_graph,
810            helper,
811            node_map,
812            namespace_stack,
813            class_stack,
814            scope_tree,
815        )?;
816    }
817
818    Ok(())
819}
820
821/// Build a qualified name from namespace and class context.
822fn build_qualified_name(namespace_stack: &[String], class_stack: &[String], name: &str) -> String {
823    let mut parts = Vec::new();
824    parts.extend(namespace_stack.iter().cloned());
825    parts.extend(class_stack.iter().cloned());
826    parts.push(name.to_string());
827    parts.join(".")
828}
829
830/// Qualify a type name with namespace context if it's not already qualified.
831///
832/// For types that are already qualified (contain '.'), returns as-is.
833/// For unqualified types in a namespace, prefixes with the namespace.
834/// This is a best-effort heuristic - without full import resolution,
835/// we assume unqualified types in a namespace are from that namespace.
836fn qualify_type_name(type_name: &str, namespace_stack: &[String]) -> String {
837    // If already qualified (contains '.'), use as-is
838    if type_name.contains('.') {
839        return type_name.to_string();
840    }
841
842    // If no namespace context, use type name as-is
843    if namespace_stack.is_empty() {
844        return type_name.to_string();
845    }
846
847    // Prefix with namespace
848    format!("{}.{}", namespace_stack.join("."), type_name)
849}
850
851fn process_invocation(
852    node: Node,
853    content: &[u8],
854    ast_graph: &ASTGraph,
855    helper: &mut GraphBuildHelper,
856    node_map: &mut HashMap<String, NodeId>,
857) {
858    let Some(function_node) = node.child_by_field_name("function") else {
859        return;
860    };
861
862    let Ok(callee_text) = function_node.utf8_text(content) else {
863        return;
864    };
865
866    // Get the caller context
867    let Some(call_context) = ast_graph.get_callable_context(node.id()) else {
868        return;
869    };
870
871    // Handle different invocation patterns:
872    // - Simple: methodName()
873    // - Member access: obj.methodName()
874    // - Static: ClassName.methodName()
875    let callee_qualified = if callee_text.contains('.') {
876        // Handle member access or static calls
877        callee_text.to_string()
878    } else if let Some(class_name) = &call_context.class_name {
879        // For simple calls inside a class, resolve to ClassName.method
880        format!("{class_name}.{callee_text}")
881    } else {
882        callee_text.to_string()
883    };
884
885    // Get or create caller node
886    let caller_function_id = *node_map
887        .entry(call_context.qualified_name.clone())
888        .or_insert_with(|| helper.add_function(&call_context.qualified_name, None, false, false));
889
890    // Get or create callee node
891    let target_function_id = *node_map
892        .entry(callee_qualified.clone())
893        .or_insert_with(|| helper.add_function(&callee_qualified, None, false, false));
894
895    let argument_count = count_call_arguments(node);
896    let call_span = Span::from_bytes(node.start_byte(), node.end_byte());
897    helper.add_call_edge_full_with_span(
898        caller_function_id,
899        target_function_id,
900        argument_count,
901        false,
902        vec![call_span],
903    );
904}
905
906fn process_object_creation(
907    node: Node,
908    content: &[u8],
909    ast_graph: &ASTGraph,
910    helper: &mut GraphBuildHelper,
911    node_map: &mut HashMap<String, NodeId>,
912) {
913    let Some(type_node) = node.child_by_field_name("type") else {
914        return;
915    };
916
917    let Ok(type_name) = type_node.utf8_text(content) else {
918        return;
919    };
920
921    // Get the caller context
922    let Some(call_context) = ast_graph.get_callable_context(node.id()) else {
923        return;
924    };
925
926    // Treat constructor calls as calls to ClassName.ctor
927    let callee_qualified = format!("{type_name}.ctor");
928
929    // Get or create caller node
930    let caller_function_id = *node_map
931        .entry(call_context.qualified_name.clone())
932        .or_insert_with(|| helper.add_function(&call_context.qualified_name, None, false, false));
933
934    // Get or create callee node (constructor)
935    let target_function_id = *node_map
936        .entry(callee_qualified.clone())
937        .or_insert_with(|| helper.add_method(&callee_qualified, None, false, false));
938
939    let argument_count = count_call_arguments(node);
940    let call_span = Span::from_bytes(node.start_byte(), node.end_byte());
941    helper.add_call_edge_full_with_span(
942        caller_function_id,
943        target_function_id,
944        argument_count,
945        false,
946        vec![call_span],
947    );
948}
949
950fn count_call_arguments(call_node: Node<'_>) -> u8 {
951    let args_node = call_node
952        .child_by_field_name("arguments")
953        .or_else(|| call_node.child_by_field_name("argument_list"))
954        .or_else(|| {
955            let mut cursor = call_node.walk();
956            call_node
957                .children(&mut cursor)
958                .find(|child| child.kind() == "argument_list")
959        });
960
961    let Some(args_node) = args_node else {
962        return 255;
963    };
964
965    let count = args_node.named_child_count();
966    if count <= 254 {
967        u8::try_from(count).unwrap_or(u8::MAX)
968    } else {
969        u8::MAX
970    }
971}
972
973// ============================================================================
974// Import Processing - using directives
975// ============================================================================
976
977/// Process using directive to create Import edges.
978///
979/// Handles patterns like:
980/// - `using System;` - simple namespace import
981/// - `using System.Collections.Generic;` - qualified namespace import
982/// - `using static System.Math;` - static using (`is_wildcard`: true for all static members)
983/// - `using Alias = Namespace.Type;` - aliased using (populate alias field)
984///
985/// # tree-sitter-c-sharp AST structure
986///
987/// Simple using:
988/// ```text
989/// using_directive [0..13] "using System;"
990///   using [0..5] "using"
991///   identifier [6..12] "System"
992///   ; [12..13] ";"
993/// ```
994///
995/// Qualified using:
996/// ```text
997/// using_directive
998///   using
999///   qualified_name "System.Collections.Generic"
1000///     identifier "System"
1001///     . "."
1002///     identifier "Collections"
1003///     . "."
1004///     identifier "Generic"
1005///   ;
1006/// ```
1007///
1008/// Static using:
1009/// ```text
1010/// using_directive
1011///   using
1012///   static
1013///   qualified_name "System.Math"
1014///   ;
1015/// ```
1016///
1017/// Aliased using:
1018/// ```text
1019/// using_directive [0..21] "using IO = System.IO;"
1020///   using [0..5] "using"
1021///   identifier [6..8] "IO"          <- alias
1022///   = [9..10] "="
1023///   qualified_name [11..20] "System.IO"  <- target
1024///   ; [20..21] ";"
1025/// ```
1026fn process_using_directive(node: Node, content: &[u8], helper: &mut GraphBuildHelper) {
1027    // Check for static using modifier
1028    let is_static = node
1029        .children(&mut node.walk())
1030        .any(|child| child.kind() == "static");
1031
1032    // Detect aliased using: pattern is "using <identifier> = <target>;"
1033    // We look for an "=" child, which indicates aliased using
1034    let has_equals = node
1035        .children(&mut node.walk())
1036        .any(|child| child.kind() == "=");
1037
1038    // Extract alias and target based on the structure
1039    let (alias, imported_name) = if has_equals {
1040        // Aliased using: first identifier is alias, qualified_name/identifier after "=" is target
1041        extract_aliased_using(node, content)
1042    } else {
1043        // Simple or static using: first identifier/qualified_name is the target
1044        (None, extract_simple_using_target(node, content))
1045    };
1046
1047    let Some(imported_name) = imported_name else {
1048        return;
1049    };
1050
1051    // Create module node (represents the current file as the importing entity)
1052    let module_id = helper.add_module("<file>", None);
1053
1054    // Create import node for the imported namespace/type
1055    let span = Span::from_bytes(node.start_byte(), node.end_byte());
1056    let import_name = if is_static {
1057        format!("static {imported_name}")
1058    } else {
1059        imported_name.clone()
1060    };
1061    let imported_id = helper.add_import(&import_name, Some(span));
1062
1063    // Add import edge with appropriate metadata
1064    // Static usings are wildcard imports (all static members are accessible)
1065    // Aliased usings have an alias but are not wildcard
1066    match (alias.as_deref(), is_static) {
1067        (Some(alias_str), _) => {
1068            // Aliased import: using IO = System.IO;
1069            helper.add_import_edge_full(module_id, imported_id, Some(alias_str), false);
1070        }
1071        (None, true) => {
1072            // Static import: using static System.Math;
1073            // All static members are imported, so is_wildcard = true
1074            helper.add_import_edge_full(module_id, imported_id, None, true);
1075        }
1076        (None, false) => {
1077            // Simple import: using System;
1078            helper.add_import_edge(module_id, imported_id);
1079        }
1080    }
1081}
1082
1083/// Extract alias and target from an aliased using directive.
1084///
1085/// Structure: `using <alias> = <target>;`
1086/// - The first identifier before "=" is the alias
1087/// - The `identifier/qualified_name` after "=" is the target
1088fn extract_aliased_using(node: Node, content: &[u8]) -> (Option<String>, Option<String>) {
1089    let mut alias: Option<String> = None;
1090    let mut target: Option<String> = None;
1091    let mut past_equals = false;
1092
1093    let mut cursor = node.walk();
1094    for child in node.children(&mut cursor) {
1095        let kind = child.kind();
1096
1097        if kind == "=" {
1098            past_equals = true;
1099            continue;
1100        }
1101
1102        // Skip keywords and punctuation
1103        if kind == "using" || kind == "static" || kind == ";" {
1104            continue;
1105        }
1106
1107        if past_equals {
1108            // After "=" - this is the target (identifier or qualified_name)
1109            if matches!(kind, "identifier" | "qualified_name") && target.is_none() {
1110                target = child
1111                    .utf8_text(content)
1112                    .ok()
1113                    .map(std::string::ToString::to_string);
1114            }
1115        } else if kind == "identifier" && alias.is_none() {
1116            // Before "=" - this is the alias (should be identifier)
1117            alias = child
1118                .utf8_text(content)
1119                .ok()
1120                .map(std::string::ToString::to_string);
1121        }
1122    }
1123
1124    (alias, target)
1125}
1126
1127/// Extract the target from a simple or static using directive.
1128///
1129/// Structure: `using [static] <target>;`
1130/// - The first `identifier/qualified_name` (after optional "static") is the target
1131fn extract_simple_using_target(node: Node, content: &[u8]) -> Option<String> {
1132    let mut cursor = node.walk();
1133
1134    for child in node.children(&mut cursor) {
1135        let kind = child.kind();
1136
1137        // Skip keywords and punctuation
1138        if kind == "using" || kind == "static" || kind == ";" || kind == "=" {
1139            continue;
1140        }
1141
1142        // First identifier or qualified_name is the target
1143        if matches!(kind, "identifier" | "identifier_name" | "qualified_name") {
1144            return child
1145                .utf8_text(content)
1146                .ok()
1147                .map(std::string::ToString::to_string);
1148        }
1149    }
1150
1151    // Fallback: try to get via field name
1152    node.child_by_field_name("name")
1153        .and_then(|n| n.utf8_text(content).ok())
1154        .map(std::string::ToString::to_string)
1155}
1156
1157// ============================================================================
1158// OOP Processing - Inheritance and Interface Implementation
1159// ============================================================================
1160
1161/// Process class declaration to extract Inherits and Implements edges.
1162///
1163/// Handles patterns like:
1164/// - `class Child : Parent` → Inherits edge
1165/// - `class Foo : IBar` → Implements edge (I prefix convention)
1166/// - `class Foo : Parent, IBar, IBaz` → One Inherits + multiple Implements edges
1167///
1168/// tree-sitter-c-sharp structure:
1169/// ```text
1170/// class_declaration
1171///   class (keyword)
1172///   name: identifier
1173///   [base_list]
1174///     : (colon)
1175///     [type_identifier | generic_name | qualified_name]+
1176///   body: declaration_list
1177/// ```
1178fn process_class_declaration(
1179    node: Node,
1180    content: &[u8],
1181    helper: &mut GraphBuildHelper,
1182    node_map: &mut HashMap<String, NodeId>,
1183    qualified_class_name: &str,
1184    namespace_stack: &[String],
1185) {
1186    // Get or create class node using qualified name (same as Phase 1)
1187    let class_id = *node_map
1188        .entry(qualified_class_name.to_string())
1189        .or_insert_with(|| helper.add_class(qualified_class_name, None));
1190
1191    // Find the base_list node (contains inheritance and interface implementation)
1192    let mut cursor = node.walk();
1193    let base_list = node
1194        .children(&mut cursor)
1195        .find(|child| child.kind() == "base_list");
1196
1197    let Some(base_list) = base_list else {
1198        return;
1199    };
1200
1201    // Track whether we've seen a class inheritance (first non-interface type)
1202    let mut first_base_class = true;
1203
1204    // Process each type in the base list
1205    let mut base_cursor = base_list.walk();
1206    for base_child in base_list.children(&mut base_cursor) {
1207        let base_type_name = match base_child.kind() {
1208            "identifier" | "identifier_name" | "type_identifier" | "qualified_name" => base_child
1209                .utf8_text(content)
1210                .ok()
1211                .map(std::string::ToString::to_string),
1212            "generic_name" => {
1213                // Generic type like List<T> - extract the base type name
1214                base_child
1215                    .child_by_field_name("name")
1216                    .or_else(|| base_child.child(0))
1217                    .and_then(|n| n.utf8_text(content).ok())
1218                    .map(std::string::ToString::to_string)
1219            }
1220            _ => None,
1221        };
1222
1223        let Some(base_name) = base_type_name else {
1224            continue;
1225        };
1226
1227        // Qualify base type name if it's unqualified and we have namespace context
1228        // If already qualified (contains '.'), use as-is; otherwise prefix with namespace
1229        let qualified_base_name = qualify_type_name(&base_name, namespace_stack);
1230
1231        // Determine if this is an interface using base-list position semantics:
1232        // In C#, if a class has both a base class and interfaces, the base class must come first.
1233        // We also check the I* naming convention as a secondary heuristic.
1234        let is_interface = is_interface_name(&base_name);
1235
1236        if is_interface {
1237            // Create interface node and add Implements edge
1238            let interface_id = *node_map
1239                .entry(qualified_base_name.clone())
1240                .or_insert_with(|| helper.add_interface(&qualified_base_name, None));
1241            helper.add_implements_edge(class_id, interface_id);
1242        } else if first_base_class {
1243            // First non-interface type is the base class
1244            let parent_id = *node_map
1245                .entry(qualified_base_name.clone())
1246                .or_insert_with(|| helper.add_class(&qualified_base_name, None));
1247            helper.add_inherits_edge(class_id, parent_id);
1248            first_base_class = false;
1249        }
1250        // Note: C# only allows single class inheritance, so we only process the first class
1251    }
1252}
1253
1254/// Process interface declaration to extract Inherits edges for interface extension.
1255///
1256/// Handles patterns like:
1257/// - `interface IChild : IParent` → Inherits edge
1258/// - `interface IChild : IParent, IOther` → Multiple Inherits edges
1259///
1260/// tree-sitter-c-sharp structure:
1261/// ```text
1262/// interface_declaration
1263///   interface (keyword)
1264///   name: identifier
1265///   [base_list]
1266///     : (colon)
1267///     [identifier | qualified_name]+
1268///   body: declaration_list
1269/// ```
1270fn process_interface_declaration(
1271    node: Node,
1272    content: &[u8],
1273    helper: &mut GraphBuildHelper,
1274    node_map: &mut HashMap<String, NodeId>,
1275    qualified_interface_name: &str,
1276    namespace_stack: &[String],
1277) {
1278    // Get or create interface node using qualified name (same as Phase 1)
1279    let interface_id = *node_map
1280        .entry(qualified_interface_name.to_string())
1281        .or_insert_with(|| helper.add_interface(qualified_interface_name, None));
1282
1283    // Find the base_list node
1284    let mut cursor = node.walk();
1285    let base_list = node
1286        .children(&mut cursor)
1287        .find(|child| child.kind() == "base_list");
1288
1289    let Some(base_list) = base_list else {
1290        return;
1291    };
1292
1293    // Process each parent interface in the base list
1294    let mut base_cursor = base_list.walk();
1295    for base_child in base_list.children(&mut base_cursor) {
1296        let parent_name = match base_child.kind() {
1297            "identifier" | "identifier_name" | "type_identifier" | "qualified_name" => base_child
1298                .utf8_text(content)
1299                .ok()
1300                .map(std::string::ToString::to_string),
1301            "generic_name" => base_child
1302                .child_by_field_name("name")
1303                .or_else(|| base_child.child(0))
1304                .and_then(|n| n.utf8_text(content).ok())
1305                .map(std::string::ToString::to_string),
1306            _ => None,
1307        };
1308
1309        let Some(parent_name) = parent_name else {
1310            continue;
1311        };
1312
1313        // Qualify parent type name if it's unqualified and we have namespace context
1314        let qualified_parent_name = qualify_type_name(&parent_name, namespace_stack);
1315
1316        // All base types for interfaces are parent interfaces → Inherits edge
1317        let parent_id = *node_map
1318            .entry(qualified_parent_name.clone())
1319            .or_insert_with(|| helper.add_interface(&qualified_parent_name, None));
1320        helper.add_inherits_edge(interface_id, parent_id);
1321    }
1322}
1323
1324/// Determine if a type name is an interface based on C# naming convention.
1325///
1326/// In C#, interfaces by convention start with 'I' followed by an uppercase letter.
1327/// Examples: `IDisposable`, `IEnumerable`, `IRepository`
1328/// Counter-examples: Int32, Image, Item
1329fn is_interface_name(name: &str) -> bool {
1330    let chars: Vec<char> = name.chars().collect();
1331    if chars.len() >= 2 {
1332        // Must start with 'I' followed by uppercase letter
1333        chars[0] == 'I' && chars[1].is_ascii_uppercase()
1334    } else {
1335        false
1336    }
1337}
1338
1339// ============================================================================
1340// Visibility Detection for Export Edges
1341// ============================================================================
1342
1343/// Check if a node has the `public` visibility modifier.
1344fn is_public(node: Node, content: &[u8]) -> bool {
1345    has_visibility_modifier(node, content, "public")
1346}
1347
1348/// Check if a node has the `internal` visibility modifier.
1349/// In C#, internal members are accessible within the same assembly and should be exported.
1350fn is_internal(node: Node, content: &[u8]) -> bool {
1351    has_visibility_modifier(node, content, "internal")
1352}
1353
1354/// Check if a node has the `private` visibility modifier.
1355fn is_private(node: Node, content: &[u8]) -> bool {
1356    has_visibility_modifier(node, content, "private")
1357}
1358
1359/// Check if a node has the `protected` visibility modifier.
1360#[allow(dead_code)] // Reserved for potential future use
1361fn is_protected(node: Node, content: &[u8]) -> bool {
1362    has_visibility_modifier(node, content, "protected")
1363}
1364
1365/// Check if a node has a specific visibility modifier.
1366fn has_visibility_modifier(node: Node, content: &[u8], modifier: &str) -> bool {
1367    node.children(&mut node.walk())
1368        .any(|child| child.kind() == modifier || child.utf8_text(content).unwrap_or("") == modifier)
1369}
1370
1371fn has_modifier(node: Node, content: &[u8], modifier: &str) -> bool {
1372    node.children(&mut node.walk())
1373        .any(|child| child.kind() == modifier || child.utf8_text(content).unwrap_or("") == modifier)
1374}
1375
1376/// Check if a member should be exported (public or internal visibility).
1377/// In C#, both public and internal types/members are exported:
1378/// - public: accessible everywhere
1379/// - internal: accessible within the same assembly
1380/// - private and protected: NOT exported
1381fn should_export(node: Node, content: &[u8]) -> bool {
1382    is_public(node, content) || is_internal(node, content)
1383}
1384
1385/// Create an export edge from the file module to the exported node.
1386fn export_from_file_module(helper: &mut GraphBuildHelper, exported: NodeId) {
1387    let module_id = helper.add_module(FILE_MODULE_NAME, None);
1388    helper.add_export_edge(module_id, exported);
1389}
1390
1391/// Process public/internal methods and fields within a class body for export edges.
1392fn process_class_member_exports(
1393    body_node: Node,
1394    content: &[u8],
1395    class_qualified_name: &str,
1396    helper: &mut GraphBuildHelper,
1397    node_map: &mut HashMap<String, NodeId>,
1398) {
1399    let mut cursor = body_node.walk();
1400    for child in body_node.children(&mut cursor) {
1401        match child.kind() {
1402            "method_declaration" | "constructor_declaration" => {
1403                // Export method/constructor if it has public or internal visibility
1404                if should_export(child, content)
1405                    && let Some(name_node) = child.child_by_field_name("name")
1406                    && let Ok(method_name) = name_node.utf8_text(content)
1407                {
1408                    let qualified_name = format!("{class_qualified_name}.{method_name}");
1409                    // Get the method from node_map if it exists
1410                    if let Some(method_id) = node_map.get(&qualified_name) {
1411                        export_from_file_module(helper, *method_id);
1412                    }
1413                } else if should_export(child, content) && child.kind() == "constructor_declaration"
1414                {
1415                    // Constructors don't have a name field, use the class name
1416                    let class_name = class_qualified_name
1417                        .rsplit('.')
1418                        .next()
1419                        .unwrap_or(class_qualified_name);
1420                    let qualified_name = format!("{class_qualified_name}.{class_name}");
1421                    if let Some(method_id) = node_map.get(&qualified_name) {
1422                        export_from_file_module(helper, *method_id);
1423                    }
1424                }
1425            }
1426            "field_declaration" | "property_declaration" => {
1427                // Export field/property if it has public or internal
1428                // visibility. Re-emit through the same Property/Constant
1429                // discrimination as the typeof pass so the helper's
1430                // (qualified_name + kind) dedup key collapses to one node
1431                // (REQ:R0001, R0003, R0004, R0005, R0018, R0019).
1432                if should_export(child, content) {
1433                    let is_property = child.kind() == "property_declaration";
1434                    let is_const = !is_property && has_modifier(child, content, "const");
1435                    let is_readonly = !is_property && has_modifier(child, content, "readonly");
1436                    let is_static = is_const || has_modifier(child, content, "static");
1437                    let visibility = extract_field_visibility(child, content);
1438                    let get_only = is_property && is_get_only_property(child);
1439                    let emit_constant = is_const || is_readonly || get_only;
1440
1441                    // Fields and properties can have multiple declarators
1442                    let mut field_cursor = child.walk();
1443                    for field_child in child.children(&mut field_cursor) {
1444                        if field_child.kind() == "variable_declarator"
1445                            && let Some(name_node) = field_child.child_by_field_name("name")
1446                            && let Ok(field_name) = name_node.utf8_text(content)
1447                        {
1448                            let qualified_name = format!("{class_qualified_name}.{field_name}");
1449                            let span =
1450                                Span::from_bytes(field_child.start_byte(), field_child.end_byte());
1451
1452                            let field_id = if emit_constant {
1453                                helper.add_constant_with_static_and_visibility(
1454                                    &qualified_name,
1455                                    Some(span),
1456                                    is_static,
1457                                    Some(visibility),
1458                                )
1459                            } else {
1460                                helper.add_property_with_static_and_visibility(
1461                                    &qualified_name,
1462                                    Some(span),
1463                                    is_static,
1464                                    Some(visibility),
1465                                )
1466                            };
1467                            export_from_file_module(helper, field_id);
1468                        } else if field_child.kind() == "identifier"
1469                            && let Ok(prop_name) = field_child.utf8_text(content)
1470                        {
1471                            // Property name is directly an identifier
1472                            let qualified_name = format!("{class_qualified_name}.{prop_name}");
1473                            let span = Span::from_bytes(child.start_byte(), child.end_byte());
1474
1475                            let prop_id = if emit_constant {
1476                                helper.add_constant_with_static_and_visibility(
1477                                    &qualified_name,
1478                                    Some(span),
1479                                    is_static,
1480                                    Some(visibility),
1481                                )
1482                            } else {
1483                                helper.add_property_with_static_and_visibility(
1484                                    &qualified_name,
1485                                    Some(span),
1486                                    is_static,
1487                                    Some(visibility),
1488                                )
1489                            };
1490                            export_from_file_module(helper, prop_id);
1491                        }
1492                    }
1493                }
1494            }
1495            _ => {}
1496        }
1497    }
1498}
1499
1500/// Process interface method exports.
1501/// In C#, interface methods are implicitly public unless explicitly marked private (C# 8.0+).
1502fn process_interface_member_exports(
1503    body_node: Node,
1504    content: &[u8],
1505    interface_qualified_name: &str,
1506    helper: &mut GraphBuildHelper,
1507    node_map: &mut HashMap<String, NodeId>,
1508) {
1509    let mut cursor = body_node.walk();
1510    for child in body_node.children(&mut cursor) {
1511        if child.kind() == "method_declaration"
1512            && !is_private(child, content)
1513            && let Some(name_node) = child.child_by_field_name("name")
1514            && let Ok(method_name) = name_node.utf8_text(content)
1515        {
1516            // Interface methods are implicitly public unless explicitly private
1517            let qualified_name = format!("{interface_qualified_name}.{method_name}");
1518            // Get the method from node_map if it exists
1519            if let Some(method_id) = node_map.get(&qualified_name) {
1520                export_from_file_module(helper, *method_id);
1521            }
1522        }
1523    }
1524}
1525
1526// ============================================================================
1527// P/Invoke (FFI) Processing
1528// ============================================================================
1529
1530/// Process method declaration to detect P/Invoke (Platform Invocation Services).
1531///
1532/// P/Invoke pattern in C#:
1533/// ```csharp
1534/// [DllImport("user32.dll")]
1535/// static extern int MessageBox(IntPtr hWnd, string text, string caption, uint type);
1536///
1537/// [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
1538/// static extern bool Beep(uint frequency, uint duration);
1539/// ```
1540///
1541/// tree-sitter-c-sharp structure:
1542/// ```text
1543/// method_declaration
1544///   attribute_list
1545///     attribute
1546///       name: identifier ("DllImport")
1547///       argument_list
1548///         attribute_argument
1549///           expression: string_literal ("user32.dll")
1550///   modifier: static
1551///   modifier: extern
1552///   return_type: predefined_type
1553///   name: identifier
1554///   parameter_list
1555/// ```
1556fn process_pinvoke_method(
1557    node: Node,
1558    content: &[u8],
1559    helper: &mut GraphBuildHelper,
1560    node_map: &mut HashMap<String, NodeId>,
1561    namespace_stack: &[String],
1562) {
1563    // Check for extern modifier
1564    let has_extern = node
1565        .children(&mut node.walk())
1566        .any(|child| child.kind() == "extern");
1567
1568    if !has_extern {
1569        return;
1570    }
1571
1572    // Look for DllImport attribute
1573    let mut cursor = node.walk();
1574    let attribute_list = node
1575        .children(&mut cursor)
1576        .find(|child| child.kind() == "attribute_list");
1577
1578    let Some(attribute_list) = attribute_list else {
1579        return;
1580    };
1581
1582    // Find DllImport attribute and extract library name
1583    let (dll_name, calling_convention) = extract_dllimport_info(attribute_list, content);
1584
1585    let Some(dll_name) = dll_name else {
1586        return;
1587    };
1588
1589    // Extract method name
1590    let method_name = node
1591        .child_by_field_name("name")
1592        .and_then(|n| n.utf8_text(content).ok())
1593        .map(std::string::ToString::to_string);
1594
1595    let Some(method_name) = method_name else {
1596        return;
1597    };
1598
1599    // Build qualified method name
1600    let qualified_method = if namespace_stack.is_empty() {
1601        method_name.clone()
1602    } else {
1603        format!("{}.{}", namespace_stack.join("."), method_name)
1604    };
1605
1606    // Get or create method node (caller - the C# method declaration)
1607    let method_span = Span::from_bytes(node.start_byte(), node.end_byte());
1608    let method_id = *node_map
1609        .entry(qualified_method.clone())
1610        .or_insert_with(|| helper.add_method(&qualified_method, Some(method_span), false, true));
1611
1612    // Create FFI function node (the native function in the DLL)
1613    let ffi_func_name = format!("ffi::{dll_name}::{method_name}");
1614    let ffi_func_id = *node_map
1615        .entry(ffi_func_name.clone())
1616        .or_insert_with(|| helper.add_function(&ffi_func_name, None, false, false));
1617
1618    // Determine FFI convention from CallingConvention parameter
1619    let convention = match calling_convention.as_deref() {
1620        Some("CallingConvention.Cdecl" | "Cdecl") => FfiConvention::Cdecl,
1621        Some("CallingConvention.FastCall" | "FastCall") => FfiConvention::Fastcall,
1622        // Default Windows convention is StdCall for P/Invoke
1623        _ => FfiConvention::Stdcall,
1624    };
1625
1626    // Add FfiCall edge
1627    helper.add_ffi_edge(method_id, ffi_func_id, convention);
1628}
1629
1630/// Extract `DllImport` attribute information (library name and calling convention).
1631///
1632/// Returns (`dll_name`, `calling_convention`) tuple.
1633fn extract_dllimport_info(
1634    attribute_list: Node,
1635    content: &[u8],
1636) -> (Option<String>, Option<String>) {
1637    let mut dll_name = None;
1638    let mut calling_convention = None;
1639
1640    let mut list_cursor = attribute_list.walk();
1641    for attr_child in attribute_list.children(&mut list_cursor) {
1642        if attr_child.kind() != "attribute" {
1643            continue;
1644        }
1645
1646        // Check if this is DllImport attribute
1647        let attr_name = attr_child
1648            .child_by_field_name("name")
1649            .and_then(|n| n.utf8_text(content).ok());
1650
1651        let is_dllimport = attr_name.is_some_and(|name| {
1652            name == "DllImport" || name == "System.Runtime.InteropServices.DllImport"
1653        });
1654
1655        if !is_dllimport {
1656            continue;
1657        }
1658
1659        // Extract arguments
1660        let mut attr_cursor = attr_child.walk();
1661        let arg_list = attr_child
1662            .children(&mut attr_cursor)
1663            .find(|child| child.kind() == "attribute_argument_list");
1664
1665        let Some(arg_list) = arg_list else {
1666            continue;
1667        };
1668
1669        let mut arg_cursor = arg_list.walk();
1670        for arg in arg_list.children(&mut arg_cursor) {
1671            if arg.kind() != "attribute_argument" {
1672                continue;
1673            }
1674
1675            // Check for named argument (CallingConvention = ...)
1676            if let Some(name_node) = arg.child_by_field_name("name")
1677                && let Ok(name) = name_node.utf8_text(content)
1678            {
1679                if name == "CallingConvention"
1680                    && let Some(expr) = arg.child_by_field_name("expression")
1681                    && let Ok(value) = expr.utf8_text(content)
1682                {
1683                    calling_convention = Some(value.to_string());
1684                }
1685                continue;
1686            }
1687
1688            // Positional argument - first one is the DLL name
1689            if dll_name.is_none() {
1690                // Find the string literal
1691                let expr = arg.child_by_field_name("expression").or_else(|| {
1692                    // Sometimes the string is a direct child
1693                    let mut c = arg.walk();
1694                    arg.children(&mut c)
1695                        .find(|child| child.kind() == "string_literal")
1696                });
1697
1698                if let Some(expr) = expr
1699                    && let Ok(text) = expr.utf8_text(content)
1700                {
1701                    // Remove quotes from string literal
1702                    let trimmed = text.trim();
1703                    if (trimmed.starts_with('"') && trimmed.ends_with('"'))
1704                        || (trimmed.starts_with('@') && trimmed.len() > 2)
1705                    {
1706                        let start = if trimmed.starts_with('@') { 2 } else { 1 };
1707                        dll_name = Some(trimmed[start..trimmed.len() - 1].to_string());
1708                    } else {
1709                        dll_name = Some(trimmed.to_string());
1710                    }
1711                }
1712            }
1713        }
1714    }
1715
1716    (dll_name, calling_convention)
1717}
1718
1719// ============================================================================
1720// TypeOf and Reference Edge Processing
1721// ============================================================================
1722
1723/// Process local variable declarations to create `TypeOf` and Reference edges.
1724///
1725/// Handles patterns like:
1726/// - `int x = 5;`
1727/// - `string name = "test";`
1728/// - `int a = 1, b = 2;` (multiple declarators)
1729/// - `List<User> users = new List<User>();` (generics - extract base type)
1730/// - `int? count = null;` (nullable - extract base type)
1731/// - `int[] numbers = new int[5];` (arrays - extract base type)
1732///
1733/// tree-sitter-c-sharp structure:
1734/// ```text
1735/// local_declaration_statement
1736///   variable_declaration
1737///     type: predefined_type | identifier | generic_name | nullable_type | array_type
1738///     variable_declarator
1739///       name: identifier
1740///       initializer: [expression]
1741/// ```
1742fn process_local_variables(
1743    node: Node,
1744    content: &[u8],
1745    helper: &mut GraphBuildHelper,
1746    _class_stack: &[String],
1747) {
1748    // Get variable_declaration child
1749    let mut cursor = node.walk();
1750    let var_decl = node
1751        .children(&mut cursor)
1752        .find(|child| child.kind() == "variable_declaration");
1753
1754    let Some(var_decl) = var_decl else {
1755        return;
1756    };
1757
1758    // Extract type
1759    let type_node = var_decl.child_by_field_name("type");
1760    let Some(type_node) = type_node else {
1761        return;
1762    };
1763
1764    // Extract full type signature for TypeOf edge
1765    let type_text = extract_type_string(type_node, content);
1766    let Some(type_text) = type_text else {
1767        return;
1768    };
1769
1770    // Extract all type names for Reference edges
1771    let all_type_names = extract_all_type_names_from_annotation(type_node, content);
1772
1773    // Process all variable declarators (may be multiple: int a = 1, b = 2;)
1774    let mut var_cursor = var_decl.walk();
1775    for child in var_decl.children(&mut var_cursor) {
1776        if child.kind() == "variable_declarator"
1777            && let Some(name_node) = child.child_by_field_name("name")
1778            && let Ok(var_name) = name_node.utf8_text(content)
1779        {
1780            let span = Span::from_bytes(child.start_byte(), child.end_byte());
1781
1782            // Create variable node
1783            let var_id = helper.add_variable(var_name, Some(span));
1784            helper.mark_definition(var_id);
1785
1786            // Create TypeOf edge with full type signature and Variable context
1787            let type_id = helper.add_type(&type_text, None);
1788            helper.add_typeof_edge_with_context(
1789                var_id,
1790                type_id,
1791                Some(TypeOfContext::Variable),
1792                None,
1793                Some(var_name),
1794            );
1795
1796            // Create Reference edges for all nested types
1797            for type_name in &all_type_names {
1798                let ref_type_id = helper.add_type(type_name, None);
1799                helper.add_reference_edge(var_id, ref_type_id);
1800            }
1801        }
1802    }
1803}
1804
1805/// Process field declarations to create `TypeOf` and Reference edges.
1806///
1807/// Handles patterns like:
1808/// - `private UserRepository repository;`
1809/// - `private int age;`
1810/// - `public List<User> users;` (generics - extract base type)
1811///
1812/// tree-sitter-c-sharp structure:
1813/// ```text
1814/// field_declaration
1815///   modifiers: [...]
1816///   declaration:
1817///     type: predefined_type | identifier | generic_name
1818///     variable_declarator
1819///       name: identifier
1820///       initializer: [expression]
1821/// ```
1822fn process_field_declaration(
1823    node: Node,
1824    content: &[u8],
1825    helper: &mut GraphBuildHelper,
1826    class_stack: &[String],
1827) {
1828    // Get the declaration child (variable_declaration)
1829    let decl_node = node
1830        .children(&mut node.walk())
1831        .find(|child| child.kind() == "variable_declaration");
1832
1833    let Some(decl_node) = decl_node else {
1834        return;
1835    };
1836
1837    // Extract type
1838    let type_node = decl_node.child_by_field_name("type");
1839    let Some(type_node) = type_node else {
1840        return;
1841    };
1842
1843    // Extract full type signature for TypeOf edge
1844    let type_text = extract_type_string(type_node, content);
1845    let Some(type_text) = type_text else {
1846        return;
1847    };
1848
1849    // Extract all type names for Reference edges
1850    let all_type_names = extract_all_type_names_from_annotation(type_node, content);
1851
1852    // Get the containing class name
1853    let class_name = class_stack.last().map_or("", String::as_str);
1854
1855    // Process all variable declarators
1856    let mut var_cursor = decl_node.walk();
1857    for child in decl_node.children(&mut var_cursor) {
1858        if child.kind() == "variable_declarator"
1859            && let Some(name_node) = child.child_by_field_name("name")
1860            && let Ok(field_name) = name_node.utf8_text(content)
1861        {
1862            // Build qualified field name
1863            let qualified_name = if class_name.is_empty() {
1864                field_name.to_string()
1865            } else {
1866                format!("{class_name}.{field_name}")
1867            };
1868
1869            let span = Span::from_bytes(child.start_byte(), child.end_byte());
1870
1871            // Branch on `readonly` / `const` / `static` modifiers (REQ:R0001,
1872            // R0003, R0004, R0005, R0018, R0019). C# semantics:
1873            //   - `const` is implicitly static and immutable -> Constant.
1874            //   - `readonly` -> Constant (immutable post-construction).
1875            //   - otherwise mutable field -> Property.
1876            // Visibility comes from the accessibility modifier; the C# default
1877            // for a class member is `private` (REQ:R0023). `internal` is a
1878            // first-class accessibility level and is passed through verbatim.
1879            let is_const = has_modifier(node, content, "const");
1880            let is_readonly = has_modifier(node, content, "readonly");
1881            let is_static = is_const || has_modifier(node, content, "static");
1882            let visibility = extract_field_visibility(node, content);
1883
1884            let field_id = if is_const || is_readonly {
1885                helper.add_constant_with_static_and_visibility(
1886                    &qualified_name,
1887                    Some(span),
1888                    is_static,
1889                    Some(visibility),
1890                )
1891            } else {
1892                helper.add_property_with_static_and_visibility(
1893                    &qualified_name,
1894                    Some(span),
1895                    is_static,
1896                    Some(visibility),
1897                )
1898            };
1899
1900            // Create TypeOf edge with full type signature, Field context, and
1901            // the bare field name (REQ:R0023 / AC-4). Cross-language byte-exact
1902            // `field:<name>` planner queries depend on the unqualified form.
1903            let type_id = helper.add_type(&type_text, None);
1904            helper.add_typeof_edge_with_context(
1905                field_id,
1906                type_id,
1907                Some(TypeOfContext::Field),
1908                None,
1909                Some(field_name),
1910            );
1911
1912            // Create Reference edges for all nested types
1913            for type_name in &all_type_names {
1914                let ref_type_id = helper.add_type(type_name, None);
1915                helper.add_reference_edge(field_id, ref_type_id);
1916            }
1917        }
1918    }
1919}
1920
1921/// Process property declarations to create `TypeOf` and Reference edges.
1922///
1923/// Handles patterns like:
1924/// - `public int Age { get; set; }` (auto-property)
1925/// - `public string Name { get; }` (read-only property)
1926/// - `public bool IsAdult { get { return age >= 18; } }` (computed property)
1927/// - `public List<User> Users { get; set; }` (generic property)
1928///
1929/// tree-sitter-c-sharp structure:
1930/// ```text
1931/// property_declaration
1932///   modifiers: [...]
1933///   type: predefined_type | identifier | generic_name
1934///   name: identifier
1935///   accessor_list: { get; set; }
1936/// ```
1937fn process_property_declaration(
1938    node: Node,
1939    content: &[u8],
1940    helper: &mut GraphBuildHelper,
1941    class_stack: &[String],
1942) {
1943    // Extract type
1944    let type_node = node.child_by_field_name("type");
1945    let Some(type_node) = type_node else {
1946        return;
1947    };
1948
1949    // Extract full type signature for TypeOf edge
1950    let type_text = extract_type_string(type_node, content);
1951    let Some(type_text) = type_text else {
1952        return;
1953    };
1954
1955    // Extract all type names for Reference edges
1956    let all_type_names = extract_all_type_names_from_annotation(type_node, content);
1957
1958    // Extract property name
1959    let name_node = node.child_by_field_name("name");
1960    let Some(name_node) = name_node else {
1961        return;
1962    };
1963
1964    let Ok(prop_name) = name_node.utf8_text(content) else {
1965        return;
1966    };
1967
1968    // Get the containing class name
1969    let class_name = class_stack.last().map_or("", String::as_str);
1970
1971    // Build qualified property name
1972    let qualified_name = if class_name.is_empty() {
1973        prop_name.to_string()
1974    } else {
1975        format!("{class_name}.{prop_name}")
1976    };
1977
1978    let span = Span::from_bytes(node.start_byte(), node.end_byte());
1979
1980    // Branch on modifiers + accessor shape (REQ:R0001, R0003, R0004, R0005,
1981    // R0018, R0019). C# property semantics:
1982    //   - `const` is illegal on properties (rejected by the compiler).
1983    //   - `readonly` is illegal on properties.
1984    //   - A get-only auto-property (`{ get; }` with no setter and no body) is
1985    //     externally immutable -> Constant.
1986    //   - Computed get-only properties (`{ get { ... } }`) and
1987    //     expression-bodied properties (`=> expr`) are computed callable
1988    //     surfaces; their backing storage (if any) is mutable from the
1989    //     implementation side, so they emit Property, not Constant.
1990    //   - Property with any setter (or `init`) -> Property.
1991    // Visibility comes from the accessibility modifier; the default for a
1992    // class member is `private` (REQ:R0023). `internal` is passed through.
1993    let is_static = has_modifier(node, content, "static");
1994    let visibility = extract_field_visibility(node, content);
1995    let get_only = is_get_only_property(node);
1996
1997    let prop_id = if get_only {
1998        helper.add_constant_with_static_and_visibility(
1999            &qualified_name,
2000            Some(span),
2001            is_static,
2002            Some(visibility),
2003        )
2004    } else {
2005        helper.add_property_with_static_and_visibility(
2006            &qualified_name,
2007            Some(span),
2008            is_static,
2009            Some(visibility),
2010        )
2011    };
2012
2013    // Create TypeOf edge with full type signature, Field context, and the bare
2014    // property name (REQ:R0023 / AC-4). Cross-language byte-exact
2015    // `field:<name>` planner queries depend on the unqualified form.
2016    // (Properties are treated as fields for TypeOf context.)
2017    let type_id = helper.add_type(&type_text, None);
2018    helper.add_typeof_edge_with_context(
2019        prop_id,
2020        type_id,
2021        Some(TypeOfContext::Field),
2022        None,
2023        Some(prop_name),
2024    );
2025
2026    // Create Reference edges for all nested types
2027    for type_name in &all_type_names {
2028        let ref_type_id = helper.add_type(type_name, None);
2029        helper.add_reference_edge(prop_id, ref_type_id);
2030    }
2031}
2032
2033/// Extract the accessibility modifier from a field/property declaration.
2034///
2035/// C# class members default to `private` when no accessibility modifier is
2036/// present (REQ:R0023). `internal` is a first-class accessibility level and is
2037/// passed through verbatim. `protected internal` (more accessible than either
2038/// alone) is normalized to the canonical two-word form.
2039fn extract_field_visibility(node: Node, content: &[u8]) -> &'static str {
2040    let has_protected = has_modifier(node, content, "protected");
2041    let has_internal = has_modifier(node, content, "internal");
2042    if has_protected && has_internal {
2043        "protected internal"
2044    } else if has_modifier(node, content, "public") {
2045        "public"
2046    } else if has_protected {
2047        "protected"
2048    } else if has_internal {
2049        "internal"
2050    } else if has_modifier(node, content, "private") {
2051        "private"
2052    } else {
2053        // C# class-member default.
2054        "private"
2055    }
2056}
2057
2058/// Determine whether a property declaration is a true get-only
2059/// auto-implemented property (`{ get; }`), as distinct from a computed
2060/// getter or an expression-bodied property.
2061///
2062/// Returns `true` only for:
2063///   - An `accessor_list` containing exactly one `get` accessor with NO body
2064///     (i.e. `get;`) and no `set;` / `init;` accessor. This is C#'s
2065///     auto-implemented immutable property, externally indistinguishable from
2066///     a `readonly` field.
2067///
2068/// Returns `false` for:
2069///   - `{ get { ... } }` — computed body. The backing storage (if any) is
2070///     mutable from the implementation's perspective; the surface is a
2071///     callable, not an immutable value.
2072///   - `T X => expr;` (`arrow_expression_clause`) — expression-bodied
2073///     properties are computed getters, same rationale as above.
2074///   - Any property with a `set` or `init` accessor.
2075///   - Any property without an `accessor_list` (defensive default).
2076fn is_get_only_property(node: Node) -> bool {
2077    // Expression-bodied property: `T X => expr;` is a computed getter, NOT
2078    // an auto-implemented immutable property. Reject before scanning the
2079    // accessor list.
2080    if node
2081        .children(&mut node.walk())
2082        .any(|child| child.kind() == "arrow_expression_clause")
2083    {
2084        return false;
2085    }
2086
2087    let Some(accessor_list) = node
2088        .children(&mut node.walk())
2089        .find(|child| child.kind() == "accessor_list")
2090    else {
2091        // No accessor list and no expression body -> not classifiable as a
2092        // get-only auto-property; default to mutable.
2093        return false;
2094    };
2095
2096    let mut has_auto_get = false;
2097    let mut has_set_like = false;
2098    for accessor in accessor_list.children(&mut accessor_list.walk()) {
2099        if accessor.kind() != "accessor_declaration" {
2100            continue;
2101        }
2102
2103        // Determine the accessor keyword and whether it has a body. An
2104        // auto-implemented accessor terminates immediately with `;` (no
2105        // `block` and no `arrow_expression_clause` child). A computed
2106        // accessor carries a `block` (`get { ... }`) or
2107        // `arrow_expression_clause` (`get => expr`).
2108        let mut keyword: Option<&str> = None;
2109        let mut has_body = false;
2110        for tok in accessor.children(&mut accessor.walk()) {
2111            match tok.kind() {
2112                "get" | "set" | "init" => keyword = Some(tok.kind()),
2113                "block" | "arrow_expression_clause" => has_body = true,
2114                _ => {}
2115            }
2116        }
2117
2118        match keyword {
2119            Some("get") => {
2120                if has_body {
2121                    // Computed getter — disqualifies the property from being
2122                    // a get-only auto-property.
2123                    return false;
2124                }
2125                has_auto_get = true;
2126            }
2127            Some("set" | "init") => has_set_like = true,
2128            _ => {}
2129        }
2130    }
2131
2132    has_auto_get && !has_set_like
2133}
2134/// Process method parameters to create `TypeOf` and Reference edges.
2135///
2136/// Handles patterns like:
2137/// - `void Method(int count, string name)`
2138/// - `User Process(Repository repo, List<Item> items)`
2139///
2140/// tree-sitter-c-sharp structure:
2141/// ```text
2142/// method_declaration
2143///   return_type: predefined_type | identifier | generic_name
2144///   name: identifier
2145///   parameter_list
2146///     parameter
2147///       type: predefined_type | identifier | generic_name
2148///       name: identifier
2149/// ```
2150fn process_method_parameters(
2151    node: Node,
2152    _method_name: &str,
2153    content: &[u8],
2154    helper: &mut GraphBuildHelper,
2155) {
2156    // Get parameter_list
2157    let Some(param_list) = node.child_by_field_name("parameter_list") else {
2158        return;
2159    };
2160
2161    // Process each parameter
2162    let mut cursor = param_list.walk();
2163    let mut param_index: u16 = 0;
2164
2165    // Iterate through children - tree-sitter-c-sharp may use various node kinds for parameters
2166    for child in param_list.children(&mut cursor) {
2167        // Skip punctuation nodes (parentheses, commas)
2168        if !child.is_named() {
2169            continue;
2170        }
2171        // Most parameter-like nodes should be processed
2172        {
2173            // Extract parameter name
2174            let Some(name_node) = child.child_by_field_name("name") else {
2175                continue;
2176            };
2177            let Ok(param_name) = name_node.utf8_text(content) else {
2178                continue;
2179            };
2180
2181            // Extract parameter type
2182            let Some(type_node) = child.child_by_field_name("type") else {
2183                continue;
2184            };
2185
2186            // Extract full type signature for TypeOf edge
2187            let Some(type_text) = extract_type_string(type_node, content) else {
2188                continue;
2189            };
2190
2191            // Extract all type names for Reference edges
2192            let all_type_names = extract_all_type_names_from_annotation(type_node, content);
2193
2194            // Create parameter variable node
2195            let param_span = Span::from_bytes(child.start_byte(), child.end_byte());
2196            let param_id = helper.add_variable(param_name, Some(param_span));
2197            helper.mark_definition(param_id);
2198
2199            // Create TypeOf edge with full type signature and Parameter context
2200            let type_id = helper.add_type(&type_text, None);
2201            helper.add_typeof_edge_with_context(
2202                param_id,
2203                type_id,
2204                Some(TypeOfContext::Parameter),
2205                Some(param_index),
2206                Some(param_name),
2207            );
2208
2209            // Create Reference edges for all nested types
2210            for type_name in &all_type_names {
2211                let ref_type_id = helper.add_type(type_name, None);
2212                helper.add_reference_edge(param_id, ref_type_id);
2213            }
2214
2215            param_index += 1;
2216        }
2217    }
2218}
2219
2220/// Process method return type to create `TypeOf` and Reference edges.
2221///
2222/// Handles patterns like:
2223/// - `string GetName()`
2224/// - `List<User> GetUsers()`
2225/// - `Task<Result> ProcessAsync()`
2226///
2227/// tree-sitter-c-sharp structure:
2228/// ```text
2229/// method_declaration
2230///   return_type: predefined_type | identifier | generic_name
2231///   name: identifier
2232///   parameter_list
2233/// ```
2234fn process_method_return_type(
2235    node: Node,
2236    method_name: &str,
2237    content: &[u8],
2238    helper: &mut GraphBuildHelper,
2239) {
2240    // Get return_type - C# uses "return_type", "returns", or "type" field
2241    let return_type_node = node
2242        .child_by_field_name("return_type")
2243        .or_else(|| node.child_by_field_name("returns"))
2244        .or_else(|| node.child_by_field_name("type"));
2245
2246    let Some(return_type_node) = return_type_node else {
2247        return;
2248    };
2249
2250    // Skip void return types
2251    if let Ok(type_text) = return_type_node.utf8_text(content)
2252        && type_text.trim() == "void"
2253    {
2254        return;
2255    }
2256
2257    // Extract full type signature for TypeOf edge
2258    let Some(type_text) = extract_type_string(return_type_node, content) else {
2259        return;
2260    };
2261
2262    // Extract all type names for Reference edges
2263    let all_type_names = extract_all_type_names_from_annotation(return_type_node, content);
2264
2265    // Get or find the method node ID
2266    let method_span = Span::from_bytes(node.start_byte(), node.end_byte());
2267    let method_id = helper.add_method(method_name, Some(method_span), false, false);
2268    helper.mark_definition(method_id);
2269
2270    // Create TypeOf edge with full type signature and Return context
2271    let type_id = helper.add_type(&type_text, None);
2272    helper.add_typeof_edge_with_context(
2273        method_id,
2274        type_id,
2275        Some(TypeOfContext::Return),
2276        Some(0), // Return always has index 0
2277        Some(method_name),
2278    );
2279
2280    // Create Reference edges for all nested types
2281    for type_name in &all_type_names {
2282        let ref_type_id = helper.add_type(type_name, None);
2283        helper.add_reference_edge(method_id, ref_type_id);
2284    }
2285}
2286
2287// ============================================================================
2288// Generic Type Parameter Emission (REQ:R0028 / U19 — C2_GEN_TP_CSHARP)
2289// ============================================================================
2290
2291/// Emit per-type-parameter `Type` nodes and `TypeOf{Constraint}` edges
2292/// for a C# generic declaration (class, interface, or method).
2293///
2294/// Handles all three shapes that carry a `type_parameter_list` child on
2295/// tree-sitter-c-sharp declarations:
2296///
2297/// 1. `class_declaration`     → `<namespace>.<ClassName>.<ParamName>`
2298/// 2. `interface_declaration` → `<namespace>.<InterfaceName>.<ParamName>`
2299/// 3. `method_declaration`    → `<namespace>.<ClassName>.<MethodName>.<ParamName>`
2300///
2301/// Tree-sitter-c-sharp grammar shape:
2302///
2303/// ```text
2304/// type_parameter_list:
2305///   '<' commaSep1(type_parameter) '>'
2306/// type_parameter:
2307///   repeat(attribute_list)
2308///   optional('in' | 'out')      // variance — emit base node, attribute deferred
2309///   name: identifier
2310/// type_parameter_constraints_clause:
2311///   'where' identifier ':' commaSep1(type_parameter_constraint)
2312/// type_parameter_constraint:
2313///   class_constraint  ('class')
2314///   | struct_constraint ('struct')
2315///   | unmanaged ('unmanaged')
2316///   | notnull ('notnull')
2317///   | constructor_constraint ('new()')
2318///   | type: <named-type>
2319/// ```
2320///
2321/// In the live tree-sitter-c-sharp 0.23.x grammar `class`/`struct`/
2322/// `unmanaged`/`notnull` arrive as **unnamed** keyword children of the
2323/// `type_parameter_constraint` node (not as separate named rules).
2324/// `constructor_constraint` is named. The bound type itself comes
2325/// through the `type` field on `type_parameter_constraint` and may be
2326/// an `identifier`, `qualified_name`, `generic_name`, or
2327/// `nullable_type` etc. — handled by `extract_constraint_base_type_name`.
2328///
2329/// AC-5: variance modifiers (`in`/`out`) are recorded on the parameter
2330/// only via the base Type-node emission. The variance attribute itself
2331/// is deferred to a future `EdgeKind` extension.
2332fn process_type_parameter_declarations(
2333    decl_node: Node,
2334    content: &[u8],
2335    parent_qualified_name: &str,
2336    helper: &mut GraphBuildHelper,
2337) {
2338    // type_parameter_list is an unnamed-position child of the
2339    // declaration (no `type_parameters` field name in c-sharp grammar).
2340    let mut decl_cursor = decl_node.walk();
2341    let Some(params_node) = decl_node
2342        .children(&mut decl_cursor)
2343        .find(|c| c.kind() == "type_parameter_list")
2344    else {
2345        return;
2346    };
2347
2348    // Map parameter-name -> NodeId so where-clauses can target the
2349    // right parameter Type node by identifier match.
2350    let mut param_ids: HashMap<String, sqry_core::graph::unified::node::NodeId> = HashMap::new();
2351
2352    let mut params_cursor = params_node.walk();
2353    for param_node in params_node.children(&mut params_cursor) {
2354        if param_node.kind() != "type_parameter" {
2355            continue;
2356        }
2357
2358        // Parameter name lives under the `name` field.
2359        let Some(name_node) = param_node.child_by_field_name("name") else {
2360            continue;
2361        };
2362        let Ok(param_name) = name_node.utf8_text(content) else {
2363            continue;
2364        };
2365
2366        let qualified_param = format!("{parent_qualified_name}.{param_name}");
2367        let span = Span::from_bytes(name_node.start_byte(), name_node.end_byte());
2368        // AC-2: span anchored on the parameter identifier so
2369        // "Find Definition" / hover navigation lands on the declaration
2370        // site rather than the synthetic `(0, 0)` sentinel.
2371        let param_id = helper.add_type(&qualified_param, Some(span));
2372        helper.mark_definition(param_id);
2373        param_ids.insert(param_name.to_string(), param_id);
2374    }
2375
2376    // AC-3 / AC-4: walk every `type_parameter_constraints_clause`
2377    // sibling on the same declaration node and emit one
2378    // `TypeOf{Constraint}` edge per constraint entry.
2379    let mut clause_cursor = decl_node.walk();
2380    for clause_node in decl_node.children(&mut clause_cursor) {
2381        if clause_node.kind() != "type_parameter_constraints_clause" {
2382            continue;
2383        }
2384        emit_type_parameter_constraint_clause(clause_node, content, &param_ids, helper);
2385    }
2386}
2387
2388/// Emit `TypeOf{Constraint}` edges for one
2389/// `type_parameter_constraints_clause`.
2390///
2391/// The clause shape is:
2392/// ```text
2393/// type_parameter_constraints_clause:
2394///   'where' identifier ':' commaSep1(type_parameter_constraint)
2395/// ```
2396///
2397/// The leading `identifier` child (after the `where` keyword) names the
2398/// type parameter being constrained. We match it against the parameter
2399/// IDs collected during the `type_parameter_list` walk; an unknown
2400/// parameter is skipped silently (defensive — the grammar guarantees
2401/// it matches one of the declared parameters).
2402fn emit_type_parameter_constraint_clause(
2403    clause_node: Node,
2404    content: &[u8],
2405    param_ids: &HashMap<String, sqry_core::graph::unified::node::NodeId>,
2406    helper: &mut GraphBuildHelper,
2407) {
2408    // The first named child is the parameter identifier (the `where`
2409    // keyword and `:` are unnamed). Subsequent named children are
2410    // `type_parameter_constraint` nodes.
2411    let mut cursor = clause_node.walk();
2412    let mut named_children = clause_node
2413        .children(&mut cursor)
2414        .filter(tree_sitter::Node::is_named);
2415
2416    let Some(param_id_node) = named_children.next() else {
2417        return;
2418    };
2419    if param_id_node.kind() != "identifier" {
2420        return;
2421    }
2422    let Ok(param_name) = param_id_node.utf8_text(content) else {
2423        return;
2424    };
2425    let Some(&param_id) = param_ids.get(param_name) else {
2426        // Constraint clause references a parameter we did not emit —
2427        // skip silently. (Should not happen for well-formed C#.)
2428        return;
2429    };
2430
2431    for constraint_node in named_children {
2432        if constraint_node.kind() != "type_parameter_constraint" {
2433            continue;
2434        }
2435        let Some(constraint_target_name) = extract_constraint_target_name(constraint_node, content)
2436        else {
2437            continue;
2438        };
2439        let constraint_id = helper.add_type(&constraint_target_name, None);
2440        helper.add_typeof_edge_with_context(
2441            param_id,
2442            constraint_id,
2443            Some(TypeOfContext::Constraint),
2444            None,
2445            None,
2446        );
2447    }
2448}
2449
2450/// Extract the constraint target identifier from a single
2451/// `type_parameter_constraint` node.
2452///
2453/// Constraint shapes recognised:
2454///
2455/// - **Synthetic keywords**: `class`, `struct`, `unmanaged`, `notnull`
2456///   appear as **unnamed** keyword children → return the keyword text
2457///   verbatim. These become interned synthetic Type nodes named
2458///   exactly `class` / `struct` / `unmanaged` / `notnull`.
2459/// - **`constructor_constraint`** (`new()`) is a named child → return
2460///   the literal string `"new()"` as the synthetic target name.
2461/// - **Named-type bound** (`where T : IFoo`, `where T : Comparable<T>`):
2462///   the `type` field carries an `identifier`, `qualified_name`,
2463///   `generic_name`, or `nullable_type` — return the base name with
2464///   any generic argument list stripped (so `Comparable<T>` becomes
2465///   `Comparable`). Cross-file unification (Phase 4c-prime) collapses
2466///   the synthetic stub into the canonical declaration when one exists.
2467fn extract_constraint_target_name(constraint_node: Node, content: &[u8]) -> Option<String> {
2468    // Iterate all children: first check for the named `constructor_constraint`,
2469    // then the `type` field, then fall back to unnamed keyword tokens.
2470    let mut cursor = constraint_node.walk();
2471    let mut had_named_constructor = false;
2472    let mut keyword_token: Option<&'static str> = None;
2473
2474    for child in constraint_node.children(&mut cursor) {
2475        match child.kind() {
2476            "constructor_constraint" => {
2477                had_named_constructor = true;
2478            }
2479            "class" if !child.is_named() => {
2480                keyword_token = Some("class");
2481            }
2482            "struct" if !child.is_named() => {
2483                keyword_token = Some("struct");
2484            }
2485            "unmanaged" if !child.is_named() => {
2486                keyword_token = Some("unmanaged");
2487            }
2488            "notnull" if !child.is_named() => {
2489                keyword_token = Some("notnull");
2490            }
2491            _ => {}
2492        }
2493    }
2494
2495    if had_named_constructor {
2496        return Some("new()".to_string());
2497    }
2498    if let Some(kw) = keyword_token {
2499        return Some(kw.to_string());
2500    }
2501
2502    // Otherwise the bound is a named type carried by the `type` field
2503    // (identifier, qualified_name, generic_name, nullable_type, ...).
2504    let type_node = constraint_node.child_by_field_name("type")?;
2505    Some(extract_constraint_base_type_name(type_node, content))
2506}
2507
2508/// Extract the base name from a constraint bound type, stripping any
2509/// generic type-argument list.
2510///
2511/// `where T : Comparable<T>` should produce a `Comparable` Type node
2512/// (not `Comparable<T>`), matching the Java implementation's behaviour
2513/// in `extract_bound_type_base_name`. This lets cross-file unification
2514/// collapse the synthetic stub into the canonical `Comparable`
2515/// declaration when one exists in the same workspace.
2516fn extract_constraint_base_type_name(type_node: Node, content: &[u8]) -> String {
2517    match type_node.kind() {
2518        "generic_name" => {
2519            // `generic_name` has shape: identifier type_argument_list.
2520            // Take the leading identifier as the base name.
2521            let mut cursor = type_node.walk();
2522            for child in type_node.children(&mut cursor) {
2523                if matches!(child.kind(), "identifier" | "qualified_name") {
2524                    return extract_constraint_base_type_name(child, content);
2525                }
2526            }
2527            type_node.utf8_text(content).unwrap_or_default().to_string()
2528        }
2529        "qualified_name" => {
2530            // `qualified_name` may end in a `generic_name` (e.g.
2531            // `System.Collections.Generic.IEnumerable<T>`). Recurse into
2532            // the trailing child to strip the type-argument list while
2533            // preserving the dotted prefix.
2534            let mut cursor = type_node.walk();
2535            let children: Vec<_> = type_node
2536                .children(&mut cursor)
2537                .filter(tree_sitter::Node::is_named)
2538                .collect();
2539            // Last named child is the leaf: identifier or generic_name.
2540            if let Some(last) = children.last()
2541                && last.kind() == "generic_name"
2542            {
2543                // Reconstruct dotted prefix + stripped leaf base.
2544                let prefix_segments: Vec<String> = children
2545                    .iter()
2546                    .take(children.len() - 1)
2547                    .filter_map(|c| c.utf8_text(content).ok().map(str::to_string))
2548                    .collect();
2549                let leaf_base = extract_constraint_base_type_name(*last, content);
2550                if prefix_segments.is_empty() {
2551                    return leaf_base;
2552                }
2553                return format!("{}.{}", prefix_segments.join("."), leaf_base);
2554            }
2555            type_node.utf8_text(content).unwrap_or_default().to_string()
2556        }
2557        "nullable_type" => {
2558            // `T?` — strip the trailing `?` by recursing into the inner
2559            // type (first named child).
2560            let mut cursor = type_node.walk();
2561            for child in type_node.children(&mut cursor) {
2562                if child.is_named() {
2563                    return extract_constraint_base_type_name(child, content);
2564                }
2565            }
2566            type_node.utf8_text(content).unwrap_or_default().to_string()
2567        }
2568        _ => type_node.utf8_text(content).unwrap_or_default().to_string(),
2569    }
2570}
2571
2572#[cfg(test)]
2573mod shape_tests {
2574    use super::*;
2575    use sqry_core::graph::unified::build::shape::{
2576        CfBucket, ShapeBudget, compute_shape_descriptor,
2577    };
2578
2579    const SAMPLE: &str = include_str!(concat!(
2580        env!("CARGO_MANIFEST_DIR"),
2581        "/../test-fixtures/shape/systems/sample.cs"
2582    ));
2583
2584    fn parse(src: &str) -> Tree {
2585        let mut parser = tree_sitter::Parser::new();
2586        parser
2587            .set_language(&tree_sitter_c_sharp::LANGUAGE.into())
2588            .expect("load C# grammar");
2589        parser.parse(src, None).expect("parse C# sample")
2590    }
2591
2592    fn first_of_kind<'a>(node: Node<'a>, kind: &str) -> Option<Node<'a>> {
2593        if node.kind() == kind {
2594            return Some(node);
2595        }
2596        let mut cursor = node.walk();
2597        for child in node.children(&mut cursor) {
2598            if let Some(found) = first_of_kind(child, kind) {
2599                return Some(found);
2600            }
2601        }
2602        None
2603    }
2604
2605    #[test]
2606    fn csharp_mapping_is_non_empty() {
2607        let mapping = csharp_shape_mapping();
2608        let lang: tree_sitter::Language = tree_sitter_c_sharp::LANGUAGE.into();
2609        let count = (0..lang.node_kind_count())
2610            .filter_map(|id| u16::try_from(id).ok())
2611            .filter(|id| mapping.cf_bucket(*id).is_some())
2612            .count();
2613        assert!(
2614            count > 0,
2615            "C# cf_bucket map should cover real control-flow kinds"
2616        );
2617    }
2618
2619    #[test]
2620    fn csharp_histogram_covers_control_flow() {
2621        let tree = parse(SAMPLE);
2622        let func = first_of_kind(tree.root_node(), "method_declaration")
2623            .expect("sample has a method_declaration");
2624        let desc = compute_shape_descriptor(
2625            func,
2626            SAMPLE.as_bytes(),
2627            csharp_shape_mapping(),
2628            &ShapeBudget::default(),
2629        );
2630        let h = &desc.cf_histogram;
2631        assert!(h[CfBucket::Branch.index()] >= 1, "branch present");
2632        assert!(h[CfBucket::Loop.index()] >= 1, "loop present");
2633        assert!(h[CfBucket::Match.index()] >= 1, "switch present");
2634        assert!(h[CfBucket::Call.index()] >= 1, "call present");
2635        assert!(h[CfBucket::Return.index()] >= 1, "return present");
2636        assert!(
2637            h[CfBucket::BreakContinue.index()] >= 1,
2638            "break/continue present"
2639        );
2640        assert!(h[CfBucket::Try.index()] >= 1, "try present");
2641        assert!(h[CfBucket::Catch.index()] >= 1, "catch present");
2642        assert!(h[CfBucket::Throw.index()] >= 1, "throw present");
2643        assert!(h[CfBucket::Closure.index()] >= 1, "lambda present");
2644    }
2645
2646    #[test]
2647    fn csharp_signature_shape_reads_params() {
2648        let tree = parse(SAMPLE);
2649        let func = first_of_kind(tree.root_node(), "method_declaration")
2650            .expect("sample has a method_declaration");
2651        let shape = csharp_shape_mapping().signature_shape(func, SAMPLE.as_bytes());
2652        // Classify(int n, string label = "default"): two positional params,
2653        // one carrying a default value.
2654        assert_eq!(shape.arity_positional, 2, "two positional params");
2655        assert!(shape.has_defaults, "default value detected");
2656        assert!(shape.has_return_annotation, "C# return type slot present");
2657    }
2658}