Skip to main content

sqry_lang_cpp/relations/
graph_builder.rs

1//! Cpp `GraphBuilder` implementation for code graph construction.
2//!
3//! Extracts Cpp-specific relationships:
4//! - Class definitions (regular, template, sealed, objects, companion objects)
5//! - Function definitions (regular, virtual, inline, extension functions)
6//! - Call expressions (regular calls, method calls, extension calls)
7//! - Inheritance (class/struct inheritance via Inherits edges)
8//! - Interface implementation (Implements edges for classes implementing pure virtual interfaces)
9//! - FFI declarations (extern "C" blocks via `FfiCall` edges)
10//!
11//! # Multi-Pass Strategy
12//!
13//! 1. **Pass 1**: Extract class/object definitions → Create Class nodes
14//! 2. **Pass 2**: Extract function/property definitions → Create Function nodes
15//! 3. **Pass 3**: Extract call expressions → Create Call edges
16//! 4. **Pass 4**: Extract FFI declarations → Create FFI function nodes
17
18use sqry_core::graph::unified::build::helper::CalleeKindHint;
19use sqry_core::graph::unified::{FfiConvention, GraphBuildHelper, StagingGraph};
20use sqry_core::graph::{GraphBuilder, GraphBuilderError, GraphResult, Language, Position, Span};
21use std::{
22    collections::{HashMap, HashSet},
23    path::{Path, PathBuf},
24    time::{Duration, Instant},
25};
26use tree_sitter::{Node, Tree};
27
28/// File-level module name for exports.
29/// In C++, symbols at file/namespace scope with external linkage are exported.
30const FILE_MODULE_NAME: &str = "<file_module>";
31
32/// Type alias for mapping (qualifier, name) tuples to fully-qualified names
33/// Used for both field types and type mappings in C++ AST analysis
34type QualifiedNameMap = HashMap<(String, String), String>;
35
36/// Registry of FFI declarations discovered during graph building.
37///
38/// Maps simple function names (e.g., `printf`) to their qualified FFI name
39/// (e.g., `extern::C::printf`) and calling convention. This allows call edge
40/// construction to detect when a call targets an FFI function and create
41/// `FfiCall` edges instead of regular `Call` edges.
42type FfiRegistry = HashMap<String, (String, FfiConvention)>;
43
44/// Registry of pure virtual interfaces (abstract classes with only pure virtual methods).
45///
46/// Maps interface name to their qualified names for Implements edge creation.
47type PureVirtualRegistry = HashSet<String>;
48
49const DEFAULT_GRAPH_BUILD_TIMEOUT_MS: u64 = 10_000;
50const MIN_GRAPH_BUILD_TIMEOUT_MS: u64 = 1_000;
51const MAX_GRAPH_BUILD_TIMEOUT_MS: u64 = 60_000;
52const BUDGET_CHECK_INTERVAL: u32 = 1024;
53
54fn cpp_graph_build_timeout() -> Duration {
55    let timeout_ms = std::env::var("SQRY_CPP_GRAPH_BUILD_TIMEOUT_MS")
56        .ok()
57        .and_then(|value| value.parse::<u64>().ok())
58        .unwrap_or(DEFAULT_GRAPH_BUILD_TIMEOUT_MS)
59        .clamp(MIN_GRAPH_BUILD_TIMEOUT_MS, MAX_GRAPH_BUILD_TIMEOUT_MS);
60    Duration::from_millis(timeout_ms)
61}
62
63struct BuildBudget {
64    file: PathBuf,
65    phase_timeout: Duration,
66    started_at: Instant,
67    checkpoints: u32,
68}
69
70impl BuildBudget {
71    fn new(file: &Path) -> Self {
72        Self {
73            file: file.to_path_buf(),
74            phase_timeout: cpp_graph_build_timeout(),
75            started_at: Instant::now(),
76            checkpoints: 0,
77        }
78    }
79
80    #[cfg(test)]
81    fn already_expired(file: &Path) -> Self {
82        Self {
83            file: file.to_path_buf(),
84            phase_timeout: Duration::from_secs(1),
85            started_at: Instant::now().checked_sub(Duration::from_secs(60)).unwrap(),
86            checkpoints: BUDGET_CHECK_INTERVAL - 1,
87        }
88    }
89
90    fn checkpoint(&mut self, phase: &'static str) -> GraphResult<()> {
91        self.checkpoints = self.checkpoints.wrapping_add(1);
92        if self.checkpoints.is_multiple_of(BUDGET_CHECK_INTERVAL)
93            && self.started_at.elapsed() > self.phase_timeout
94        {
95            return Err(GraphBuilderError::BuildTimedOut {
96                file: self.file.clone(),
97                phase,
98                #[allow(clippy::cast_possible_truncation)] // Graph storage: node/edge index counts fit in u32
99                timeout_ms: self.phase_timeout.as_millis() as u64,
100            });
101        }
102        Ok(())
103    }
104}
105
106// Helper extension trait for Span creation
107#[allow(dead_code)] // Reserved for future span-based analysis
108trait SpanExt {
109    fn from_node(node: &tree_sitter::Node) -> Self;
110}
111
112impl SpanExt for Span {
113    fn from_node(node: &tree_sitter::Node) -> Self {
114        Span::new(
115            Position::new(node.start_position().row, node.start_position().column),
116            Position::new(node.end_position().row, node.end_position().column),
117        )
118    }
119}
120
121// ================================
122// ASTGraph: In-memory function context index
123// ================================
124
125/// In-memory index of C++ function contexts for O(1) lookups during call edge extraction.
126///
127/// This structure is built in a first pass over the AST and provides:
128/// - Fast lookup of the enclosing function for any byte position
129/// - Qualified names for all functions/methods
130/// - Field type resolution for member variable method calls
131/// - Type name resolution via includes and using declarations
132#[derive(Debug)]
133struct ASTGraph {
134    /// All function/method contexts with their qualified names and byte spans
135    contexts: Vec<FunctionContext>,
136    /// Maps function definition start byte to its context index.
137    context_start_index: HashMap<usize, usize>,
138
139    /// Maps (`class_fqn`, `field_name`) to field's FQN type.
140    /// Example: ("`demo::Service`", "repo") -> "`demo::Repository`"
141    /// This avoids collisions when multiple classes have fields with the same name.
142    /// Used to resolve method calls on member variables (e.g., repo.save -> `demo::Repository::save`)
143    /// Reserved for future call resolution enhancements
144    #[allow(dead_code)]
145    field_types: QualifiedNameMap,
146
147    /// Maps (`namespace_context`, `simple_type_name`) to FQN.
148    /// Example: ("demo", "Repository") -> "`demo::Repository`"
149    /// This handles the fact that the same simple type name can resolve differently
150    /// in different namespaces and using-directive scopes.
151    /// Used to resolve static method calls (e.g., `Repository::save` -> `demo::Repository::save`)
152    /// Reserved for future call resolution enhancements
153    #[allow(dead_code)]
154    type_map: QualifiedNameMap,
155
156    /// Maps byte ranges to namespace prefixes (e.g., range -> "`demo::`")
157    /// Used to determine which namespace context a symbol is defined in
158    /// Reserved for future namespace-aware resolution
159    #[allow(dead_code)]
160    namespace_map: HashMap<std::ops::Range<usize>, String>,
161}
162
163impl ASTGraph {
164    /// Build `ASTGraph` from tree-sitter AST
165    fn from_tree(root: Node, content: &[u8], budget: &mut BuildBudget) -> GraphResult<Self> {
166        // Extract namespace context
167        let namespace_map = extract_namespace_map(root, content, budget)?;
168
169        // Extract function contexts
170        let mut contexts = extract_cpp_contexts(root, content, &namespace_map, budget)?;
171        contexts.sort_by_key(|ctx| ctx.span.0);
172        let context_start_index = contexts
173            .iter()
174            .enumerate()
175            .map(|(idx, ctx)| (ctx.span.0, idx))
176            .collect();
177
178        // Extract field declarations and type mappings
179        let (field_types, type_map) =
180            extract_field_and_type_info(root, content, &namespace_map, budget)?;
181
182        Ok(Self {
183            contexts,
184            context_start_index,
185            field_types,
186            type_map,
187            namespace_map,
188        })
189    }
190
191    /// Find the enclosing function context for a given byte position.
192    ///
193    /// C++ has no nested function definitions, so at most one function span can
194    /// contain any byte offset. With contexts sorted by start byte we can use a
195    /// binary search instead of scanning every function for every call site.
196    fn find_enclosing(&self, byte_pos: usize) -> Option<&FunctionContext> {
197        let insertion_point = self.contexts.partition_point(|ctx| ctx.span.0 <= byte_pos);
198        if insertion_point == 0 {
199            return None;
200        }
201
202        let candidate = &self.contexts[insertion_point - 1];
203        (byte_pos < candidate.span.1).then_some(candidate)
204    }
205
206    fn context_for_start(&self, start_byte: usize) -> Option<&FunctionContext> {
207        self.context_start_index
208            .get(&start_byte)
209            .and_then(|idx| self.contexts.get(*idx))
210    }
211}
212
213/// Represents a C++ function or method with its qualified name and metadata
214#[derive(Debug, Clone)]
215struct FunctionContext {
216    /// Fully qualified name: "`demo::Service::process`" or "`demo::helper`"
217    qualified_name: String,
218    /// Byte span of the function body
219    span: (usize, usize),
220    /// Whether this is a static method
221    /// Reserved for future method resolution enhancements
222    is_static: bool,
223    /// Whether this is a virtual method
224    /// Reserved for future polymorphic call analysis
225    #[allow(dead_code)]
226    is_virtual: bool,
227    /// Whether this is inline
228    /// Reserved for future optimization hints
229    #[allow(dead_code)]
230    is_inline: bool,
231    /// Namespace stack for use in call resolution (e.g., [`demo`])
232    namespace_stack: Vec<String>,
233    /// Class stack for use in call resolution (e.g., [`Service`])
234    /// Reserved for future method resolution enhancements
235    #[allow(dead_code)] // Used in tests and reserved for future call resolution
236    class_stack: Vec<String>,
237    /// Return type of the function (e.g., `int`, `std::string`)
238    return_type: Option<String>,
239}
240
241impl FunctionContext {
242    #[allow(dead_code)] // Reserved for future context queries
243    fn qualified_name(&self) -> &str {
244        &self.qualified_name
245    }
246}
247
248/// Cpp-specific `GraphBuilder` implementation.
249///
250/// Performs multi-pass analysis:
251/// 1. Extract class and object definitions
252/// 2. Extract function and property definitions
253/// 3. Extract call expressions
254///
255/// # Example
256///
257/// ```no_run
258/// use sqry_lang_cpp::relations::CppGraphBuilder;
259/// use sqry_core::graph::GraphBuilder;
260/// use sqry_core::graph::unified::StagingGraph;
261/// use tree_sitter::Parser;
262///
263/// let mut parser = Parser::new();
264/// parser.set_language(&tree_sitter_cpp::LANGUAGE.into()).unwrap();
265/// let tree = parser.parse(b"class User { public: std::string getName() { return \"Alice\"; } };", None).unwrap();
266/// let mut staging = StagingGraph::new();
267/// let builder = CppGraphBuilder::new();
268/// builder.build_graph(&tree, b"class User { public: std::string getName() { return \"Alice\"; } };",
269///                      std::path::Path::new("test.cpp"), &mut staging).unwrap();
270/// ```
271#[derive(Debug, Default, Clone, Copy)]
272pub struct CppGraphBuilder;
273
274impl CppGraphBuilder {
275    /// Create a new Cpp `GraphBuilder`.
276    #[must_use]
277    pub fn new() -> Self {
278        Self
279    }
280
281    #[allow(clippy::unused_self)] // Method uses self for API consistency
282    #[allow(clippy::trivially_copy_pass_by_ref)] // Intentional
283    fn build_graph_with_budget(
284        #[allow(clippy::trivially_copy_pass_by_ref)] // API consistency with other methods
285        &self,
286        tree: &Tree,
287        content: &[u8],
288        file: &Path,
289        staging: &mut StagingGraph,
290        budget: &mut BuildBudget,
291    ) -> GraphResult<()> {
292        // Create helper for staging graph population
293        let mut helper = GraphBuildHelper::new(staging, file, Language::Cpp);
294
295        // Build AST graph for call context tracking
296        let ast_graph = ASTGraph::from_tree(tree.root_node(), content, budget)?;
297
298        // Track seen includes for deduplication
299        let mut seen_includes: HashSet<String> = HashSet::new();
300
301        // Track namespace and class context for qualified naming
302        let mut namespace_stack: Vec<String> = Vec::new();
303        let mut class_stack: Vec<String> = Vec::new();
304
305        // Two-pass approach for FFI call linking:
306        // Pass 1: Collect FFI declarations so calls can be resolved regardless of source order
307        let mut ffi_registry = FfiRegistry::new();
308        collect_ffi_declarations(tree.root_node(), content, &mut ffi_registry, budget)?;
309
310        // Pass 1b: Collect pure virtual interfaces for Implements edge detection
311        let mut pure_virtual_registry = PureVirtualRegistry::new();
312        collect_pure_virtual_interfaces(
313            tree.root_node(),
314            content,
315            &mut pure_virtual_registry,
316            budget,
317        )?;
318
319        // Walk tree to find classes, functions, methods, and calls
320        walk_tree_for_graph(
321            tree.root_node(),
322            content,
323            &ast_graph,
324            &mut helper,
325            &mut seen_includes,
326            &mut namespace_stack,
327            &mut class_stack,
328            &ffi_registry,
329            &pure_virtual_registry,
330            budget,
331        )?;
332
333        Ok(())
334    }
335
336    /// Extract class attributes from modifiers.
337    #[allow(dead_code)] // Scaffolding for class attribute analysis
338    fn extract_class_attributes(node: &tree_sitter::Node, content: &[u8]) -> Vec<String> {
339        let mut attributes = Vec::new();
340        let mut cursor = node.walk();
341        for child in node.children(&mut cursor) {
342            if child.kind() == "modifiers" {
343                let mut mod_cursor = child.walk();
344                for modifier in child.children(&mut mod_cursor) {
345                    if let Ok(mod_text) = modifier.utf8_text(content) {
346                        match mod_text {
347                            "template" => attributes.push("template".to_string()),
348                            "sealed" => attributes.push("sealed".to_string()),
349                            "abstract" => attributes.push("abstract".to_string()),
350                            "open" => attributes.push("open".to_string()),
351                            "final" => attributes.push("final".to_string()),
352                            "inner" => attributes.push("inner".to_string()),
353                            "value" => attributes.push("value".to_string()),
354                            _ => {}
355                        }
356                    }
357                }
358            }
359        }
360        attributes
361    }
362
363    /// Check if a function is virtual (async).
364    #[allow(dead_code)] // Scaffolding for virtual method detection
365    fn extract_is_virtual(node: &tree_sitter::Node, content: &[u8]) -> bool {
366        if let Some(spec) = node.child_by_field_name("declaration_specifiers")
367            && let Ok(text) = spec.utf8_text(content)
368            && text.contains("virtual")
369        {
370            return true;
371        }
372
373        if let Ok(text) = node.utf8_text(content)
374            && text.contains("virtual")
375        {
376            return true;
377        }
378
379        if let Some(parent) = node.parent()
380            && (parent.kind() == "field_declaration" || parent.kind() == "declaration")
381            && let Ok(text) = parent.utf8_text(content)
382            && text.contains("virtual")
383        {
384            return true;
385        }
386
387        false
388    }
389
390    /// Extract function attributes from modifiers.
391    #[allow(dead_code)] // Scaffolding for function attribute analysis
392    fn extract_function_attributes(node: &tree_sitter::Node, content: &[u8]) -> Vec<String> {
393        let mut attributes = Vec::new();
394        for node_ref in [
395            node.child_by_field_name("declaration_specifiers"),
396            node.parent(),
397        ]
398        .into_iter()
399        .flatten()
400        {
401            if let Ok(text) = node_ref.utf8_text(content) {
402                for keyword in [
403                    "virtual",
404                    "inline",
405                    "constexpr",
406                    "operator",
407                    "override",
408                    "static",
409                ] {
410                    if text.contains(keyword) && !attributes.contains(&keyword.to_string()) {
411                        attributes.push(keyword.to_string());
412                    }
413                }
414            }
415        }
416
417        if let Ok(text) = node.utf8_text(content) {
418            for keyword in [
419                "virtual",
420                "inline",
421                "constexpr",
422                "operator",
423                "override",
424                "static",
425            ] {
426                if text.contains(keyword) && !attributes.contains(&keyword.to_string()) {
427                    attributes.push(keyword.to_string());
428                }
429            }
430        }
431
432        attributes
433    }
434}
435
436impl GraphBuilder for CppGraphBuilder {
437    fn language(&self) -> Language {
438        Language::Cpp
439    }
440
441    fn build_graph(
442        &self,
443        tree: &Tree,
444        content: &[u8],
445        file: &Path,
446        staging: &mut StagingGraph,
447    ) -> GraphResult<()> {
448        let mut budget = BuildBudget::new(file);
449        self.build_graph_with_budget(tree, content, file, staging, &mut budget)
450    }
451}
452
453// ================================
454// Context Extraction (Stub Implementations)
455// ================================
456
457/// Extract namespace declarations and build a map from byte ranges to namespace names.
458///
459/// This function recursively traverses the AST and builds a map from byte ranges to namespace
460/// prefixes. For example, if a node is inside `namespace demo { ... }`, its byte range will
461/// map to "`demo::`".
462///
463/// Returns: `HashMap`<Range<usize>, String> mapping byte ranges to namespace prefixes
464fn extract_namespace_map(
465    node: Node,
466    content: &[u8],
467    budget: &mut BuildBudget,
468) -> GraphResult<HashMap<std::ops::Range<usize>, String>> {
469    let mut map = HashMap::new();
470
471    // Create recursion guard with configured limit
472    let recursion_limits = sqry_core::config::RecursionLimits::load_or_default()
473        .expect("Failed to load recursion limits");
474    let file_ops_depth = recursion_limits
475        .effective_file_ops_depth()
476        .expect("Invalid file_ops_depth configuration");
477    let mut guard = sqry_core::query::security::RecursionGuard::new(file_ops_depth)
478        .expect("Failed to create recursion guard");
479
480    extract_namespaces_recursive(node, content, "", &mut map, &mut guard, budget).map_err(|e| {
481        match e {
482            timeout @ GraphBuilderError::BuildTimedOut { .. } => timeout,
483            other => GraphBuilderError::ParseError {
484                span: span_from_node(node),
485                reason: format!("C++ namespace extraction failed: {other}"),
486            },
487        }
488    })?;
489
490    Ok(map)
491}
492
493/// Recursive helper for namespace extraction
494///
495/// # Errors
496///
497/// Returns [`RecursionError::DepthLimitExceeded`] if recursion depth exceeds the guard's limit.
498fn extract_namespaces_recursive(
499    node: Node,
500    content: &[u8],
501    current_ns: &str,
502    map: &mut HashMap<std::ops::Range<usize>, String>,
503    guard: &mut sqry_core::query::security::RecursionGuard,
504    budget: &mut BuildBudget,
505) -> GraphResult<()> {
506    budget.checkpoint("cpp:extract_namespace_map")?;
507    guard.enter().map_err(|e| GraphBuilderError::ParseError {
508        span: span_from_node(node),
509        reason: format!("C++ namespace extraction hit recursion limit: {e}"),
510    })?;
511
512    if node.kind() == "namespace_definition" {
513        // Extract namespace name from the namespace_identifier or identifier child
514        let ns_name = if let Some(name_node) = node.child_by_field_name("name") {
515            extract_identifier(name_node, content)
516        } else {
517            // Anonymous namespace
518            String::from("anonymous")
519        };
520
521        // Build new namespace prefix
522        let new_ns = if current_ns.is_empty() {
523            format!("{ns_name}::")
524        } else {
525            format!("{current_ns}{ns_name}::")
526        };
527
528        // Map the body's byte range to this namespace
529        if let Some(body) = node.child_by_field_name("body") {
530            let range = body.start_byte()..body.end_byte();
531            map.insert(range, new_ns.clone());
532
533            // Recurse into nested namespaces within the body
534            let mut cursor = body.walk();
535            for child in body.children(&mut cursor) {
536                extract_namespaces_recursive(child, content, &new_ns, map, guard, budget)?;
537            }
538        }
539    } else {
540        // Recurse with current namespace
541        let mut cursor = node.walk();
542        for child in node.children(&mut cursor) {
543            extract_namespaces_recursive(child, content, current_ns, map, guard, budget)?;
544        }
545    }
546
547    guard.exit();
548    Ok(())
549}
550
551/// Extract identifier from a node (handles simple identifiers and qualified names)
552fn extract_identifier(node: Node, content: &[u8]) -> String {
553    node.utf8_text(content).unwrap_or("").to_string()
554}
555
556/// Find the namespace prefix for a given byte offset
557fn find_namespace_for_offset(
558    byte_offset: usize,
559    namespace_map: &HashMap<std::ops::Range<usize>, String>,
560) -> String {
561    // Find all ranges that contain this offset
562    let mut matching_ranges: Vec<_> = namespace_map
563        .iter()
564        .filter(|(range, _)| range.contains(&byte_offset))
565        .collect();
566
567    // Sort by range size (smaller ranges are more specific/nested)
568    matching_ranges.sort_by_key(|(range, _)| range.end - range.start);
569
570    // Return the most specific (smallest) range's namespace
571    matching_ranges
572        .first()
573        .map_or("", |(_, ns)| ns.as_str())
574        .to_string()
575}
576
577/// Extract all function/method contexts with their qualified names.
578///
579/// This function traverses the AST and builds a complete list of all functions/methods
580/// with their fully qualified names (including namespace and class context).
581///
582/// Returns: Vec<FunctionContext> with all function/method contexts
583fn extract_cpp_contexts(
584    node: Node,
585    content: &[u8],
586    namespace_map: &HashMap<std::ops::Range<usize>, String>,
587    budget: &mut BuildBudget,
588) -> GraphResult<Vec<FunctionContext>> {
589    let mut contexts = Vec::new();
590    let mut class_stack = Vec::new();
591
592    // Create recursion guard with configured limit
593    let recursion_limits = sqry_core::config::RecursionLimits::load_or_default()
594        .expect("Failed to load recursion limits");
595    let file_ops_depth = recursion_limits
596        .effective_file_ops_depth()
597        .expect("Invalid file_ops_depth configuration");
598    let mut guard = sqry_core::query::security::RecursionGuard::new(file_ops_depth)
599        .expect("Failed to create recursion guard");
600
601    extract_contexts_recursive(
602        node,
603        content,
604        namespace_map,
605        &mut contexts,
606        &mut class_stack,
607        &mut guard,
608        budget,
609    )
610    .map_err(|e| match e {
611        timeout @ GraphBuilderError::BuildTimedOut { .. } => timeout,
612        other => GraphBuilderError::ParseError {
613            span: span_from_node(node),
614            reason: format!("C++ context extraction failed: {other}"),
615        },
616    })?;
617
618    Ok(contexts)
619}
620
621/// Recursive helper for function context extraction
622/// # Errors
623///
624/// Returns [`RecursionError::DepthLimitExceeded`] if recursion depth exceeds the guard's limit.
625fn extract_contexts_recursive(
626    node: Node,
627    content: &[u8],
628    namespace_map: &HashMap<std::ops::Range<usize>, String>,
629    contexts: &mut Vec<FunctionContext>,
630    class_stack: &mut Vec<String>,
631    guard: &mut sqry_core::query::security::RecursionGuard,
632    budget: &mut BuildBudget,
633) -> GraphResult<()> {
634    budget.checkpoint("cpp:extract_contexts")?;
635    guard.enter().map_err(|e| GraphBuilderError::ParseError {
636        span: span_from_node(node),
637        reason: format!("C++ context extraction hit recursion limit: {e}"),
638    })?;
639
640    match node.kind() {
641        "class_specifier" | "struct_specifier" => {
642            // Extract class/struct name
643            if let Some(name_node) = node.child_by_field_name("name") {
644                let class_name = extract_identifier(name_node, content);
645                class_stack.push(class_name);
646
647                // Recurse into class body
648                if let Some(body) = node.child_by_field_name("body") {
649                    let mut cursor = body.walk();
650                    for child in body.children(&mut cursor) {
651                        extract_contexts_recursive(
652                            child,
653                            content,
654                            namespace_map,
655                            contexts,
656                            class_stack,
657                            guard,
658                            budget,
659                        )?;
660                    }
661                }
662
663                class_stack.pop();
664            }
665        }
666
667        "function_definition" => {
668            // Extract function name and build qualified name
669            if let Some(declarator) = node.child_by_field_name("declarator") {
670                let (func_name, class_prefix) =
671                    extract_function_name_with_class(declarator, content);
672
673                // Find enclosing namespace and convert to stack
674                let namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
675                let namespace_stack: Vec<String> = if namespace.is_empty() {
676                    Vec::new()
677                } else {
678                    namespace
679                        .trim_end_matches("::")
680                        .split("::")
681                        .map(String::from)
682                        .collect()
683                };
684
685                // Build the effective class stack:
686                // - If we're inside a class body, use that class stack
687                // - If this is an out-of-class method (e.g., Service::process), use the class prefix
688                let effective_class_stack: Vec<String> = if !class_stack.is_empty() {
689                    class_stack.clone()
690                } else if let Some(ref prefix) = class_prefix {
691                    vec![prefix.clone()]
692                } else {
693                    Vec::new()
694                };
695
696                // Build qualified name
697                let qualified_name =
698                    build_qualified_name(&namespace_stack, &effective_class_stack, &func_name);
699
700                // Extract metadata
701                let is_static = is_static_function(node, content);
702                let is_virtual = is_virtual_function(node, content);
703                let is_inline = is_inline_function(node, content);
704
705                // Extract return type from function definition
706                let return_type = node
707                    .child_by_field_name("type")
708                    .and_then(|type_node| type_node.utf8_text(content).ok())
709                    .map(std::string::ToString::to_string);
710
711                // Get function definition's full span for matching during graph building
712                let span = (node.start_byte(), node.end_byte());
713
714                contexts.push(FunctionContext {
715                    qualified_name,
716                    span,
717                    is_static,
718                    is_virtual,
719                    is_inline,
720                    namespace_stack,
721                    class_stack: effective_class_stack,
722                    return_type,
723                });
724            }
725
726            // Don't recurse into function body - C++ doesn't have nested functions
727        }
728
729        _ => {
730            // Recurse into children
731            let mut cursor = node.walk();
732            for child in node.children(&mut cursor) {
733                extract_contexts_recursive(
734                    child,
735                    content,
736                    namespace_map,
737                    contexts,
738                    class_stack,
739                    guard,
740                    budget,
741                )?;
742            }
743        }
744    }
745
746    guard.exit();
747    Ok(())
748}
749
750/// Build a fully qualified name from namespace stack, class stack, and name.
751///
752/// This function combines namespace context, class hierarchy, and the final name
753/// into a C++-style qualified name (e.g., `namespace::ClassName::methodName`).
754fn build_qualified_name(namespace_stack: &[String], class_stack: &[String], name: &str) -> String {
755    let mut parts = Vec::new();
756
757    // Add namespace stack
758    parts.extend(namespace_stack.iter().cloned());
759
760    // Add class stack
761    for class_name in class_stack {
762        parts.push(class_name.clone());
763    }
764
765    // Add name
766    parts.push(name.to_string());
767
768    parts.join("::")
769}
770
771/// Extract function name and optional class prefix from a function declarator node.
772/// Returns (`function_name`, `optional_class_prefix`).
773/// For `Service::process`, returns ("process", Some("Service")).
774/// For `process`, returns ("process", None).
775fn extract_function_name_with_class(declarator: Node, content: &[u8]) -> (String, Option<String>) {
776    // The declarator can be:
777    // - function_declarator (simple function)
778    // - qualified_identifier (Class::method)
779    // - field_identifier (method)
780    // - destructor_name (~Class)
781    // - operator_name (operator+)
782
783    match declarator.kind() {
784        "function_declarator" => {
785            // Recurse to find the actual name
786            if let Some(declarator_inner) = declarator.child_by_field_name("declarator") {
787                extract_function_name_with_class(declarator_inner, content)
788            } else {
789                (extract_identifier(declarator, content), None)
790            }
791        }
792        "qualified_identifier" => {
793            // For qualified names like Service::process, extract both parts
794            let name = if let Some(name_node) = declarator.child_by_field_name("name") {
795                extract_identifier(name_node, content)
796            } else {
797                extract_identifier(declarator, content)
798            };
799
800            // Extract the scope (class/namespace prefix)
801            let class_prefix = declarator
802                .child_by_field_name("scope")
803                .map(|scope_node| extract_identifier(scope_node, content));
804
805            (name, class_prefix)
806        }
807        "field_identifier" | "identifier" | "destructor_name" | "operator_name" => {
808            (extract_identifier(declarator, content), None)
809        }
810        _ => {
811            // For other cases, try to extract text directly
812            (extract_identifier(declarator, content), None)
813        }
814    }
815}
816
817/// Extract function name from a function declarator node (convenience wrapper)
818#[allow(dead_code)]
819fn extract_function_name(declarator: Node, content: &[u8]) -> String {
820    extract_function_name_with_class(declarator, content).0
821}
822
823/// Check if a function is static
824fn is_static_function(node: Node, content: &[u8]) -> bool {
825    has_specifier(node, "static", content)
826}
827
828/// Check if a function is virtual
829fn is_virtual_function(node: Node, content: &[u8]) -> bool {
830    has_specifier(node, "virtual", content)
831}
832
833/// Check if a function is inline
834fn is_inline_function(node: Node, content: &[u8]) -> bool {
835    has_specifier(node, "inline", content)
836}
837
838/// Check if a function has a specific specifier (static, virtual, inline, etc.)
839fn has_specifier(node: Node, specifier: &str, content: &[u8]) -> bool {
840    // Check declaration specifiers
841    let mut cursor = node.walk();
842    for child in node.children(&mut cursor) {
843        if (child.kind() == "storage_class_specifier"
844            || child.kind() == "type_qualifier"
845            || child.kind() == "virtual"
846            || child.kind() == "inline")
847            && let Ok(text) = child.utf8_text(content)
848            && text == specifier
849        {
850            return true;
851        }
852    }
853    false
854}
855
856/// Extract field declarations and type mappings.
857///
858/// This function traverses the AST and extracts:
859/// 1. Field types: Maps (`class_fqn`, `field_name`) to field's FQN type
860/// 2. Type map: Maps (`namespace_context`, `simple_type_name`) to FQN from using directives
861///
862/// Returns:
863/// - `field_types`: Maps (`class_fqn`, `field_name`) to field's FQN type
864/// - `type_map`: Maps (`namespace_context`, `simple_type_name`) to FQN
865fn extract_field_and_type_info(
866    node: Node,
867    content: &[u8],
868    namespace_map: &HashMap<std::ops::Range<usize>, String>,
869    budget: &mut BuildBudget,
870) -> GraphResult<(QualifiedNameMap, QualifiedNameMap)> {
871    let mut field_types = HashMap::new();
872    let mut type_map = HashMap::new();
873    let mut class_stack = Vec::new();
874
875    extract_fields_recursive(
876        node,
877        content,
878        namespace_map,
879        &mut field_types,
880        &mut type_map,
881        &mut class_stack,
882        budget,
883    )?;
884
885    Ok((field_types, type_map))
886}
887
888/// Recursive helper for field and type extraction
889fn extract_fields_recursive(
890    node: Node,
891    content: &[u8],
892    namespace_map: &HashMap<std::ops::Range<usize>, String>,
893    field_types: &mut HashMap<(String, String), String>,
894    type_map: &mut HashMap<(String, String), String>,
895    class_stack: &mut Vec<String>,
896    budget: &mut BuildBudget,
897) -> GraphResult<()> {
898    budget.checkpoint("cpp:extract_fields")?;
899    match node.kind() {
900        "class_specifier" | "struct_specifier" => {
901            // Extract class name and build FQN
902            if let Some(name_node) = node.child_by_field_name("name") {
903                let class_name = extract_identifier(name_node, content);
904                let namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
905
906                // Build FQN including parent classes from class_stack
907                let class_fqn = if class_stack.is_empty() {
908                    // Top-level class: just namespace + class name
909                    if namespace.is_empty() {
910                        class_name.clone()
911                    } else {
912                        format!("{}::{}", namespace.trim_end_matches("::"), class_name)
913                    }
914                } else {
915                    // Nested class: parent_fqn + class name
916                    format!("{}::{}", class_stack.last().unwrap(), class_name)
917                };
918
919                class_stack.push(class_fqn.clone());
920
921                // Process all children to find field_declaration_list or direct field_declaration
922                let mut cursor = node.walk();
923                for child in node.children(&mut cursor) {
924                    extract_fields_recursive(
925                        child,
926                        content,
927                        namespace_map,
928                        field_types,
929                        type_map,
930                        class_stack,
931                        budget,
932                    )?;
933                }
934
935                class_stack.pop();
936            }
937        }
938
939        "field_declaration" => {
940            // Extract field declaration if we're inside a class
941            if let Some(class_fqn) = class_stack.last() {
942                extract_field_declaration(
943                    node,
944                    content,
945                    class_fqn,
946                    namespace_map,
947                    field_types,
948                    type_map,
949                );
950            }
951        }
952
953        "using_directive" => {
954            // Extract using directive: using namespace std;
955            extract_using_directive(node, content, namespace_map, type_map);
956        }
957
958        "using_declaration" => {
959            // Extract using declaration: using std::vector;
960            extract_using_declaration(node, content, namespace_map, type_map);
961        }
962
963        _ => {
964            // Recurse into children
965            let mut cursor = node.walk();
966            for child in node.children(&mut cursor) {
967                extract_fields_recursive(
968                    child,
969                    content,
970                    namespace_map,
971                    field_types,
972                    type_map,
973                    class_stack,
974                    budget,
975                )?;
976            }
977        }
978    }
979
980    Ok(())
981}
982
983/// Extract a field declaration and store its type
984fn extract_field_declaration(
985    node: Node,
986    content: &[u8],
987    class_fqn: &str,
988    namespace_map: &HashMap<std::ops::Range<usize>, String>,
989    field_types: &mut HashMap<(String, String), String>,
990    type_map: &HashMap<(String, String), String>,
991) {
992    // In tree-sitter-cpp, field_declaration children are:
993    // type_identifier, field_identifier, ;
994    // OR for multiple declarators: type_identifier, declarator1, ',', declarator2, ;
995
996    let mut field_type = None;
997    let mut field_names = Vec::new();
998
999    let mut cursor = node.walk();
1000    for child in node.children(&mut cursor) {
1001        match child.kind() {
1002            "type_identifier" | "primitive_type" | "qualified_identifier" | "template_type" => {
1003                field_type = Some(extract_type_name(child, content));
1004            }
1005            "field_identifier" => {
1006                // Direct field identifier (simple case: Type name;)
1007                field_names.push(extract_identifier(child, content));
1008            }
1009            "field_declarator"
1010            | "init_declarator"
1011            | "pointer_declarator"
1012            | "reference_declarator"
1013            | "array_declarator" => {
1014                // Declarator (with modifiers: Type* name; or Type name = init;)
1015                if let Some(name) = extract_field_name(child, content) {
1016                    field_names.push(name);
1017                }
1018            }
1019            _ => {}
1020        }
1021    }
1022
1023    // Resolve field type to FQN using namespace/type_map
1024    if let Some(ftype) = field_type {
1025        let namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
1026        let field_type_fqn = resolve_type_to_fqn(&ftype, &namespace, type_map);
1027
1028        // Store each field name with the same type
1029        for fname in field_names {
1030            field_types.insert((class_fqn.to_string(), fname), field_type_fqn.clone());
1031        }
1032    }
1033}
1034
1035/// Extract type name from a type node
1036fn extract_type_name(type_node: Node, content: &[u8]) -> String {
1037    match type_node.kind() {
1038        "type_identifier" | "primitive_type" => extract_identifier(type_node, content),
1039        "qualified_identifier" => {
1040            // For qualified types like std::vector, we want the full name
1041            extract_identifier(type_node, content)
1042        }
1043        "template_type" => {
1044            // For template types like vector<int>, extract the base type
1045            if let Some(name) = type_node.child_by_field_name("name") {
1046                extract_identifier(name, content)
1047            } else {
1048                extract_identifier(type_node, content)
1049            }
1050        }
1051        _ => {
1052            // For other cases, try to extract text directly
1053            extract_identifier(type_node, content)
1054        }
1055    }
1056}
1057
1058/// Extract field name from a declarator
1059fn extract_field_name(declarator: Node, content: &[u8]) -> Option<String> {
1060    match declarator.kind() {
1061        "field_declarator" => {
1062            // Recurse to find the actual name
1063            if let Some(declarator_inner) = declarator.child_by_field_name("declarator") {
1064                extract_field_name(declarator_inner, content)
1065            } else {
1066                Some(extract_identifier(declarator, content))
1067            }
1068        }
1069        "field_identifier" | "identifier" => Some(extract_identifier(declarator, content)),
1070        "pointer_declarator" | "reference_declarator" | "array_declarator" => {
1071            // For pointer/reference/array types, recurse to find the name
1072            if let Some(declarator_inner) = declarator.child_by_field_name("declarator") {
1073                extract_field_name(declarator_inner, content)
1074            } else {
1075                None
1076            }
1077        }
1078        "init_declarator" => {
1079            // For initialized fields, extract the declarator
1080            if let Some(declarator_inner) = declarator.child_by_field_name("declarator") {
1081                extract_field_name(declarator_inner, content)
1082            } else {
1083                None
1084            }
1085        }
1086        _ => None,
1087    }
1088}
1089
1090/// Resolve a simple type name to its FQN using namespace context and `type_map`
1091fn resolve_type_to_fqn(
1092    type_name: &str,
1093    namespace: &str,
1094    type_map: &HashMap<(String, String), String>,
1095) -> String {
1096    // If already qualified (contains ::), return as-is
1097    if type_name.contains("::") {
1098        return type_name.to_string();
1099    }
1100
1101    // Try to resolve using type_map with current namespace
1102    let namespace_key = namespace.trim_end_matches("::").to_string();
1103    if let Some(fqn) = type_map.get(&(namespace_key.clone(), type_name.to_string())) {
1104        return fqn.clone();
1105    }
1106
1107    // Try global namespace
1108    if let Some(fqn) = type_map.get(&(String::new(), type_name.to_string())) {
1109        return fqn.clone();
1110    }
1111
1112    // If no mapping found, return as-is
1113    type_name.to_string()
1114}
1115
1116/// Extract using directive (using namespace X;)
1117fn extract_using_directive(
1118    node: Node,
1119    content: &[u8],
1120    namespace_map: &HashMap<std::ops::Range<usize>, String>,
1121    _type_map: &mut HashMap<(String, String), String>,
1122) {
1123    // For now, we don't store using directives in type_map
1124    // because they affect all types in a namespace, not just specific ones
1125    // This is a simplification - full implementation would track these
1126    let _namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
1127
1128    // Extract the namespace being used
1129    if let Some(name_node) = node.child_by_field_name("name") {
1130        let _using_ns = extract_identifier(name_node, content);
1131        // Using directives (`using namespace std;`) import all names from a namespace,
1132        // requiring scoped directive tracking to resolve unqualified types. Using
1133        // declarations (`using std::vector;`) are handled by extract_using_declaration().
1134    }
1135}
1136
1137/// Extract using declaration (using `X::Y`;)
1138///
1139/// Maps simple names to their fully qualified names for type resolution.
1140/// Example: `using std::vector;` stores `("", "vector") -> "std::vector"`.
1141fn extract_using_declaration(
1142    node: Node,
1143    content: &[u8],
1144    namespace_map: &HashMap<std::ops::Range<usize>, String>,
1145    type_map: &mut HashMap<(String, String), String>,
1146) {
1147    let namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
1148    let namespace_key = namespace.trim_end_matches("::").to_string();
1149
1150    // Find the qualified_identifier child (tree-sitter-cpp doesn't expose a "name" field)
1151    let mut cursor = node.walk();
1152    for child in node.children(&mut cursor) {
1153        if child.kind() == "qualified_identifier" || child.kind() == "identifier" {
1154            let fqn = extract_identifier(child, content);
1155
1156            // Extract the simple name (last part after ::)
1157            if let Some(simple_name) = fqn.split("::").last() {
1158                // Store: (namespace_context, simple_name) -> fqn
1159                type_map.insert((namespace_key, simple_name.to_string()), fqn);
1160            }
1161            break;
1162        }
1163    }
1164}
1165
1166// ================================
1167// Call Resolution
1168// ================================
1169
1170/// Resolve a callee name to its fully qualified name using `ASTGraph` context.
1171///
1172/// This function handles:
1173/// - Simple names: "helper" -> "`demo::helper`" (using namespace context)
1174/// - Qualified names: "`Service::process`" -> "`demo::Service::process`" (adding namespace)
1175/// - Static method calls: "`Repository::save`" -> "`demo::Repository::save`"
1176/// - Member calls would require parsing the AST node further (future work)
1177fn resolve_callee_name(
1178    callee_name: &str,
1179    caller_ctx: &FunctionContext,
1180    _ast_graph: &ASTGraph,
1181) -> String {
1182    // If already fully qualified (starts with ::), return as-is
1183    if callee_name.starts_with("::") {
1184        return callee_name.trim_start_matches("::").to_string();
1185    }
1186
1187    // If contains ::, it might be partially qualified (e.g., "Service::process")
1188    if callee_name.contains("::") {
1189        // Add namespace prefix if not already qualified
1190        if !caller_ctx.namespace_stack.is_empty() {
1191            let namespace_prefix = caller_ctx.namespace_stack.join("::");
1192            return format!("{namespace_prefix}::{callee_name}");
1193        }
1194        return callee_name.to_string();
1195    }
1196
1197    // Simple name: build FQN from caller's namespace and class context
1198    let mut parts = Vec::new();
1199
1200    // Add namespace
1201    if !caller_ctx.namespace_stack.is_empty() {
1202        parts.extend(caller_ctx.namespace_stack.iter().cloned());
1203    }
1204
1205    // For simple names within a class, don't add class context automatically
1206    // (the call might be to a free function or static method from another class)
1207    // Future work: Parse the call expression to determine if it's a member call
1208
1209    // Add function name
1210    parts.push(callee_name.to_string());
1211
1212    parts.join("::")
1213}
1214
1215/// Strip type qualifiers (const, volatile, *, &) to extract the base type name.
1216/// Examples:
1217/// - "const int*" -> "int"
1218/// - "int const*" -> "int"  (postfix const)
1219/// - "`std::string`&" -> "string"
1220/// - "vector<int>" -> "vector"
1221fn strip_type_qualifiers(type_text: &str) -> String {
1222    let mut result = type_text.trim().to_string();
1223
1224    // Remove prefix qualifiers (with trailing space)
1225    result = result.replace("const ", "");
1226    result = result.replace("volatile ", "");
1227    result = result.replace("mutable ", "");
1228    result = result.replace("constexpr ", "");
1229
1230    // Remove postfix qualifiers (with leading space)
1231    result = result.replace(" const", "");
1232    result = result.replace(" volatile", "");
1233    result = result.replace(" mutable", "");
1234    result = result.replace(" constexpr", "");
1235
1236    // Remove pointer and reference markers
1237    result = result.replace(['*', '&'], "");
1238
1239    // Trim any extra whitespace
1240    result = result.trim().to_string();
1241
1242    // Extract the simple name from qualified names (std::string -> string)
1243    if let Some(last_part) = result.split("::").last() {
1244        result = last_part.to_string();
1245    }
1246
1247    // Extract base type from templates (vector<int> -> vector)
1248    if let Some(open_bracket) = result.find('<') {
1249        result = result[..open_bracket].to_string();
1250    }
1251
1252    result.trim().to_string()
1253}
1254
1255/// Process a field declaration inside a class/struct, creating `Property` /
1256/// `Constant` nodes plus `TypeOf` (with `TypeOfContext::Field` + bare-name)
1257/// and `Reference` edges.
1258///
1259/// Per cross-language-field-emission/02_DESIGN §3.1.1 + §4.1:
1260/// - Qualified-name format: `Class.field`. Only the LAST separator is `.`;
1261///   the class chain itself keeps `::` (e.g. `demo::Outer::Inner.field`).
1262/// - `const` and `constexpr` declarations emit `NodeKind::Constant`; everything
1263///   else emits `NodeKind::Property`.
1264/// - The `static` keyword sets `is_static = true`. Per design §3.4 only the
1265///   `static` keyword controls this — bare `constexpr` does NOT imply static.
1266/// - Visibility flows in from `walk_class_body` (defaults: `class` →
1267///   `"private"`, `struct` → `"public"`).
1268/// - The `TypeOf` edge uses `TypeOfContext::Field` and stores the **bare**
1269///   field name in its `name` metadata (not the qualified form).
1270#[allow(clippy::unnecessary_wraps, clippy::too_many_lines)]
1271fn process_field_declaration(
1272    node: Node,
1273    content: &[u8],
1274    class_qualified_name: &str,
1275    visibility: &str,
1276    helper: &mut GraphBuildHelper,
1277) -> GraphResult<()> {
1278    // Extract type and field names from the field_declaration
1279    let mut field_type_text = None;
1280    let mut field_names = Vec::new();
1281    // Modifiers on the declaration itself — used to pick Property vs Constant
1282    // and to compute is_static.
1283    let mut is_static_kw = false;
1284    let mut is_const = false;
1285    let mut is_constexpr = false;
1286
1287    let mut cursor = node.walk();
1288    for child in node.children(&mut cursor) {
1289        match child.kind() {
1290            "type_identifier" | "primitive_type" => {
1291                if let Ok(text) = child.utf8_text(content) {
1292                    field_type_text = Some(text.to_string());
1293                }
1294            }
1295            "qualified_identifier" => {
1296                // Handle qualified types like std::string
1297                if let Ok(text) = child.utf8_text(content) {
1298                    field_type_text = Some(text.to_string());
1299                }
1300            }
1301            "template_type" => {
1302                // Handle template types like std::vector<int>
1303                if let Ok(text) = child.utf8_text(content) {
1304                    field_type_text = Some(text.to_string());
1305                }
1306            }
1307            "sized_type_specifier" => {
1308                // Handle sized types like unsigned long, long long
1309                if let Ok(text) = child.utf8_text(content) {
1310                    field_type_text = Some(text.to_string());
1311                }
1312            }
1313            "type_qualifier" => {
1314                // Type qualifiers carry semantic information (`const`,
1315                // `volatile`, ...) and — for older tree-sitter-cpp grammars —
1316                // also `constexpr`. We always inspect the text so the
1317                // const/constexpr classification is accurate, and we only
1318                // promote it to `field_type_text` as a fallback when no
1319                // explicit type child was seen.
1320                if let Ok(text) = child.utf8_text(content) {
1321                    let trimmed = text.trim();
1322                    if trimmed == "const" {
1323                        is_const = true;
1324                    } else if trimmed == "constexpr" {
1325                        is_constexpr = true;
1326                    }
1327                    if field_type_text.is_none() {
1328                        field_type_text = Some(text.to_string());
1329                    }
1330                }
1331            }
1332            "storage_class_specifier" => {
1333                // `static`, `extern`, `register`, `mutable`, `thread_local`,
1334                // and (in newer grammars) `constexpr`.
1335                if let Ok(text) = child.utf8_text(content) {
1336                    let trimmed = text.trim();
1337                    if trimmed == "static" {
1338                        is_static_kw = true;
1339                    } else if trimmed == "constexpr" {
1340                        is_constexpr = true;
1341                    }
1342                }
1343            }
1344            "auto" => {
1345                // Handle auto type deduction
1346                field_type_text = Some("auto".to_string());
1347            }
1348            "decltype" => {
1349                // Handle decltype(expr)
1350                if let Ok(text) = child.utf8_text(content) {
1351                    field_type_text = Some(text.to_string());
1352                }
1353            }
1354            "struct_specifier" | "class_specifier" | "enum_specifier" | "union_specifier" => {
1355                // Handle inline struct/class/enum/union declarations
1356                if let Ok(text) = child.utf8_text(content) {
1357                    field_type_text = Some(text.to_string());
1358                }
1359            }
1360            "field_identifier" => {
1361                if let Ok(name) = child.utf8_text(content) {
1362                    field_names.push(name.trim().to_string());
1363                }
1364            }
1365            "field_declarator"
1366            | "pointer_declarator"
1367            | "reference_declarator"
1368            | "init_declarator" => {
1369                // Recursively extract field name from declarators
1370                if let Some(name) = extract_field_name(child, content) {
1371                    field_names.push(name);
1372                }
1373            }
1374            _ => {}
1375        }
1376    }
1377
1378    // If we found a type and at least one field name, create the nodes and edges
1379    if let Some(type_text) = field_type_text {
1380        let base_type = strip_type_qualifiers(&type_text);
1381        let is_constant = is_const || is_constexpr;
1382
1383        for field_name in field_names {
1384            // Per design §3.1.1: only the LAST separator migrates to `.`;
1385            // the class chain (`namespace::Outer::Inner`) keeps `::`.
1386            let field_qualified = format!("{class_qualified_name}.{field_name}");
1387            let span = span_from_node(node);
1388
1389            // AC-2 + AC-3 + AC-4: pick the right node kind, propagate
1390            // is_static from the `static` keyword, and forward visibility
1391            // from the enclosing access specifier.
1392            let field_id = if is_constant {
1393                helper.add_constant_with_name_static_and_visibility(
1394                    &field_name,
1395                    &field_qualified,
1396                    Some(span),
1397                    is_static_kw,
1398                    Some(visibility),
1399                )
1400            } else {
1401                helper.add_property_with_name_static_and_visibility(
1402                    &field_name,
1403                    &field_qualified,
1404                    Some(span),
1405                    is_static_kw,
1406                    Some(visibility),
1407                )
1408            };
1409
1410            // Create a Type node for the base type (if not primitive)
1411            let type_id = helper.add_type(&base_type, None);
1412
1413            // AC-5: TypeOf edge with Field context + bare field name.
1414            helper.add_typeof_edge_with_context(
1415                field_id,
1416                type_id,
1417                Some(sqry_core::graph::unified::edge::kind::TypeOfContext::Field),
1418                None,
1419                Some(&field_name),
1420            );
1421
1422            // Reference edge preserved for backward-compatible "uses type"
1423            // queries.
1424            helper.add_reference_edge(field_id, type_id);
1425        }
1426    }
1427
1428    Ok(())
1429}
1430
1431/// Process file-level variable declarations (global variables)
1432#[allow(clippy::unnecessary_wraps)]
1433fn process_global_variable_declaration(
1434    node: Node,
1435    content: &[u8],
1436    namespace_stack: &[String],
1437    helper: &mut GraphBuildHelper,
1438) -> GraphResult<()> {
1439    // Check if this is a declaration node (not a field_declaration, which is class-specific)
1440    if node.kind() != "declaration" {
1441        return Ok(());
1442    }
1443
1444    // Skip function declarations (they have function_declarator children)
1445    // These are handled separately via function_definition nodes
1446    let mut cursor_check = node.walk();
1447    for child in node.children(&mut cursor_check) {
1448        if child.kind() == "function_declarator" {
1449            return Ok(());
1450        }
1451    }
1452
1453    // Extract type and variable names
1454    let mut type_text = None;
1455    let mut var_names = Vec::new();
1456
1457    let mut cursor = node.walk();
1458    for child in node.children(&mut cursor) {
1459        match child.kind() {
1460            "type_identifier" | "primitive_type" | "qualified_identifier" | "template_type" => {
1461                if let Ok(text) = child.utf8_text(content) {
1462                    type_text = Some(text.to_string());
1463                }
1464            }
1465            "init_declarator" => {
1466                // Extract variable name from init_declarator
1467                if let Some(declarator) = child.child_by_field_name("declarator")
1468                    && let Some(name) = extract_declarator_name(declarator, content)
1469                {
1470                    var_names.push(name);
1471                }
1472            }
1473            "pointer_declarator" | "reference_declarator" => {
1474                if let Some(name) = extract_declarator_name(child, content) {
1475                    var_names.push(name);
1476                }
1477            }
1478            "identifier" => {
1479                // Direct identifier for simple declarations
1480                if let Ok(name) = child.utf8_text(content) {
1481                    var_names.push(name.to_string());
1482                }
1483            }
1484            _ => {}
1485        }
1486    }
1487
1488    if let Some(type_text) = type_text {
1489        let base_type = strip_type_qualifiers(&type_text);
1490
1491        for var_name in var_names {
1492            // Build qualified name with namespace
1493            let qualified = if namespace_stack.is_empty() {
1494                var_name.clone()
1495            } else {
1496                format!("{}::{}", namespace_stack.join("::"), var_name)
1497            };
1498
1499            let span = span_from_node(node);
1500
1501            // Create variable node (global variables are public by default)
1502            let var_id = helper.add_node_with_visibility(
1503                &qualified,
1504                Some(span),
1505                sqry_core::graph::unified::node::NodeKind::Variable,
1506                Some("public"),
1507            );
1508
1509            // Create Type node
1510            let type_id = helper.add_type(&base_type, None);
1511
1512            // Add TypeOf and Reference edges
1513            helper.add_typeof_edge(var_id, type_id);
1514            helper.add_reference_edge(var_id, type_id);
1515        }
1516    }
1517
1518    Ok(())
1519}
1520
1521/// Extract variable/parameter name from a declarator node
1522fn extract_declarator_name(node: Node, content: &[u8]) -> Option<String> {
1523    match node.kind() {
1524        "identifier" => {
1525            if let Ok(name) = node.utf8_text(content) {
1526                Some(name.to_string())
1527            } else {
1528                None
1529            }
1530        }
1531        "pointer_declarator" | "reference_declarator" | "array_declarator" => {
1532            // Recurse to find the actual name
1533            if let Some(inner) = node.child_by_field_name("declarator") {
1534                extract_declarator_name(inner, content)
1535            } else {
1536                // Try looking for identifier child directly
1537                let mut cursor = node.walk();
1538                for child in node.children(&mut cursor) {
1539                    if child.kind() == "identifier"
1540                        && let Ok(name) = child.utf8_text(content)
1541                    {
1542                        return Some(name.to_string());
1543                    }
1544                }
1545                None
1546            }
1547        }
1548        "init_declarator" => {
1549            // Extract from the declarator field
1550            if let Some(inner) = node.child_by_field_name("declarator") {
1551                extract_declarator_name(inner, content)
1552            } else {
1553                None
1554            }
1555        }
1556        "field_declarator" => {
1557            // Recurse to find the actual name
1558            if let Some(inner) = node.child_by_field_name("declarator") {
1559                extract_declarator_name(inner, content)
1560            } else {
1561                // Try to extract directly
1562                if let Ok(name) = node.utf8_text(content) {
1563                    Some(name.to_string())
1564                } else {
1565                    None
1566                }
1567            }
1568        }
1569        _ => None,
1570    }
1571}
1572
1573/// Walk a class/struct body, processing field declarations and methods with visibility tracking.
1574#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
1575fn walk_class_body(
1576    body_node: Node,
1577    content: &[u8],
1578    class_qualified_name: &str,
1579    is_struct: bool,
1580    ast_graph: &ASTGraph,
1581    helper: &mut GraphBuildHelper,
1582    seen_includes: &mut HashSet<String>,
1583    namespace_stack: &mut Vec<String>,
1584    class_stack: &mut Vec<String>,
1585    ffi_registry: &FfiRegistry,
1586    pure_virtual_registry: &PureVirtualRegistry,
1587    budget: &mut BuildBudget,
1588) -> GraphResult<()> {
1589    // Default visibility: struct = public, class = private
1590    let mut current_visibility = if is_struct { "public" } else { "private" };
1591
1592    let mut cursor = body_node.walk();
1593    for child in body_node.children(&mut cursor) {
1594        budget.checkpoint("cpp:walk_class_body")?;
1595        match child.kind() {
1596            "access_specifier" => {
1597                // Update current visibility (public:, private:, protected:)
1598                if let Ok(text) = child.utf8_text(content) {
1599                    let spec = text.trim().trim_end_matches(':').trim();
1600                    current_visibility = spec;
1601                }
1602            }
1603            "field_declaration" => {
1604                // First, look for nested type declarations (class/struct/union)
1605                // as direct children of the field_declaration. tree-sitter-cpp
1606                // wraps `class Inner { ... };` and `union { int a; };` shapes
1607                // declared inside a class body in a `field_declaration` parent.
1608                //
1609                // - NAMED nested class/struct (e.g. `class Inner { int x; };`)
1610                //   must recurse with the extended class chain so the inner
1611                //   field qualifies as `Outer::Inner.x`. Default visibility
1612                //   resets to the nested type's own default (struct = public,
1613                //   class = private), independent of the OUTER access state.
1614                // - ANONYMOUS union/struct/class (e.g. `union { int a; };`)
1615                //   injects its members into the enclosing class per C++
1616                //   semantics; recurse with the OUTER `class_qualified_name`
1617                //   so members emit as `Outer.a` / `Outer.b`. Visibility for
1618                //   injected members inherits the OUTER `current_visibility`.
1619                let mut handled_nested = false;
1620                let mut inner_cursor = child.walk();
1621                for inner in child.children(&mut inner_cursor) {
1622                    let kind = inner.kind();
1623                    if !matches!(
1624                        kind,
1625                        "class_specifier"
1626                            | "struct_specifier"
1627                            | "union_specifier"
1628                            | "enum_specifier"
1629                    ) {
1630                        continue;
1631                    }
1632
1633                    let is_struct_or_union = matches!(kind, "struct_specifier" | "union_specifier");
1634
1635                    if let Some(name_node) = inner.child_by_field_name("name") {
1636                        // NAMED nested type: emit the type node itself (so it is
1637                        // discoverable via `kind:class` / `kind:struct` /
1638                        // `kind:enum`), wire its inheritance/implements edges, then
1639                        // walk its body with the extended chain so members qualify
1640                        // as `Outer::Inner.field`.
1641                        //
1642                        // Visibility = the enclosing access state (`current_visibility`),
1643                        // matching C++ member-access rules for nested types. Nested
1644                        // types are NEVER exported at file scope, so no Export edge is
1645                        // added here (contrast with the top-level class/struct arm in
1646                        // `walk_tree_for_graph`).
1647                        if let Ok(inner_name) = name_node.utf8_text(content) {
1648                            let inner_name = inner_name.trim();
1649                            let nested_qualified = format!("{class_qualified_name}::{inner_name}");
1650                            let nested_span = span_from_node(inner);
1651
1652                            // NodeKind: enum → Enum; struct/union → Struct; class → Class.
1653                            // (NodeKind has no dedicated Union variant; unions map to
1654                            // Struct, consistent with the nested-member walk below.)
1655                            match kind {
1656                                "enum_specifier" => {
1657                                    // Nested enums carry the enclosing access
1658                                    // visibility, identical to the nested
1659                                    // class/struct path below.
1660                                    helper.add_enum_with_visibility(
1661                                        &nested_qualified,
1662                                        Some(nested_span),
1663                                        Some(current_visibility),
1664                                    );
1665                                }
1666                                _ => {
1667                                    let nested_id = if is_struct_or_union {
1668                                        helper.add_struct_with_visibility(
1669                                            &nested_qualified,
1670                                            Some(nested_span),
1671                                            Some(current_visibility),
1672                                        )
1673                                    } else {
1674                                        helper.add_class_with_visibility(
1675                                            &nested_qualified,
1676                                            Some(nested_span),
1677                                            Some(current_visibility),
1678                                        )
1679                                    };
1680                                    build_inheritance_and_implements_edges(
1681                                        inner,
1682                                        content,
1683                                        &nested_qualified,
1684                                        nested_id,
1685                                        helper,
1686                                        namespace_stack,
1687                                        pure_virtual_registry,
1688                                    )?;
1689                                }
1690                            }
1691
1692                            // Recurse into the body for members. Enums carry no
1693                            // field members we model, so only class/struct/union
1694                            // bodies are walked. Emitting the node above already
1695                            // marks the declaration handled.
1696                            if matches!(
1697                                kind,
1698                                "class_specifier" | "struct_specifier" | "union_specifier"
1699                            ) && let Some(body) = inner.child_by_field_name("body")
1700                            {
1701                                walk_class_body(
1702                                    body,
1703                                    content,
1704                                    &nested_qualified,
1705                                    is_struct_or_union,
1706                                    ast_graph,
1707                                    helper,
1708                                    seen_includes,
1709                                    namespace_stack,
1710                                    class_stack,
1711                                    ffi_registry,
1712                                    pure_virtual_registry,
1713                                    budget,
1714                                )?;
1715                            }
1716                            handled_nested = true;
1717                        }
1718                    } else if let Some(body) = inner.child_by_field_name("body") {
1719                        // ANONYMOUS nested type: inject members into enclosing
1720                        // class. Process direct field_declaration children
1721                        // with OUTER qualifier + OUTER visibility so members
1722                        // surface as `Outer.member`.
1723                        let mut anon_cursor = body.walk();
1724                        for anon_child in body.children(&mut anon_cursor) {
1725                            if anon_child.kind() == "field_declaration" {
1726                                process_field_declaration(
1727                                    anon_child,
1728                                    content,
1729                                    class_qualified_name,
1730                                    current_visibility,
1731                                    helper,
1732                                )?;
1733                            }
1734                        }
1735                        handled_nested = true;
1736                    }
1737                }
1738
1739                // Process the field_declaration itself unless we exclusively
1740                // handled it as a pure nested type with no instance declarator
1741                // (e.g. `class Inner { ... };` has a class_specifier but no
1742                // field_identifier). `process_field_declaration` is harmless
1743                // when no `field_identifier` / declarator child exists — it
1744                // collects an empty `field_names` list and falls through.
1745                // We still call it so cases that mix a nested type with an
1746                // instance declarator (`class Inner { } member;`) keep
1747                // emitting the `Outer.member` Property too. When
1748                // `handled_nested` is true and the type child is absent of
1749                // declarator children, the function is effectively a no-op
1750                // (no field name → no node).
1751                let _ = handled_nested;
1752                process_field_declaration(
1753                    child,
1754                    content,
1755                    class_qualified_name,
1756                    current_visibility,
1757                    helper,
1758                )?;
1759            }
1760            "function_definition" => {
1761                // Process method with current visibility
1762                // Extract function context from AST graph by matching start position
1763                if let Some(context) = ast_graph.context_for_start(child.start_byte()) {
1764                    let span = span_from_node(child);
1765                    helper.add_method_with_signature(
1766                        &context.qualified_name,
1767                        Some(span),
1768                        false, // C++ doesn't have async
1769                        context.is_static,
1770                        Some(current_visibility),
1771                        context.return_type.as_deref(),
1772                    );
1773                }
1774                // Recurse into function body to process call expressions
1775                walk_tree_for_graph(
1776                    child,
1777                    content,
1778                    ast_graph,
1779                    helper,
1780                    seen_includes,
1781                    namespace_stack,
1782                    class_stack,
1783                    ffi_registry,
1784                    pure_virtual_registry,
1785                    budget,
1786                )?;
1787            }
1788            _ => {
1789                // Recurse into other nodes (nested classes, etc.)
1790                walk_tree_for_graph(
1791                    child,
1792                    content,
1793                    ast_graph,
1794                    helper,
1795                    seen_includes,
1796                    namespace_stack,
1797                    class_stack,
1798                    ffi_registry,
1799                    pure_virtual_registry,
1800                    budget,
1801                )?;
1802            }
1803        }
1804    }
1805
1806    Ok(())
1807}
1808
1809/// Walk the tree and populate the staging graph.
1810#[allow(clippy::too_many_arguments)]
1811#[allow(clippy::too_many_lines)] // Central traversal; refactor after C++ AST stabilizes.
1812fn walk_tree_for_graph(
1813    node: Node,
1814    content: &[u8],
1815    ast_graph: &ASTGraph,
1816    helper: &mut GraphBuildHelper,
1817    seen_includes: &mut HashSet<String>,
1818    namespace_stack: &mut Vec<String>,
1819    class_stack: &mut Vec<String>,
1820    ffi_registry: &FfiRegistry,
1821    pure_virtual_registry: &PureVirtualRegistry,
1822    budget: &mut BuildBudget,
1823) -> GraphResult<()> {
1824    budget.checkpoint("cpp:walk_tree_for_graph")?;
1825    match node.kind() {
1826        "preproc_include" => {
1827            // Handle #include directives - create Import edges
1828            build_import_edge(node, content, helper, seen_includes)?;
1829        }
1830        "linkage_specification" => {
1831            // Handle extern "C" blocks - create FFI function nodes
1832            build_ffi_block_for_staging(node, content, helper, namespace_stack);
1833        }
1834        "namespace_definition" => {
1835            // Extract namespace name and track context
1836            if let Some(name_node) = node.child_by_field_name("name")
1837                && let Ok(ns_name) = name_node.utf8_text(content)
1838            {
1839                namespace_stack.push(ns_name.trim().to_string());
1840
1841                // Recurse into namespace body
1842                let mut cursor = node.walk();
1843                for child in node.children(&mut cursor) {
1844                    walk_tree_for_graph(
1845                        child,
1846                        content,
1847                        ast_graph,
1848                        helper,
1849                        seen_includes,
1850                        namespace_stack,
1851                        class_stack,
1852                        ffi_registry,
1853                        pure_virtual_registry,
1854                        budget,
1855                    )?;
1856                }
1857
1858                namespace_stack.pop();
1859                return Ok(());
1860            }
1861        }
1862        "class_specifier" | "struct_specifier" | "union_specifier" => {
1863            // Extract class/struct/union name
1864            if let Some(name_node) = node.child_by_field_name("name")
1865                && let Ok(class_name) = name_node.utf8_text(content)
1866            {
1867                let class_name = class_name.trim();
1868                let span = span_from_node(node);
1869                // Unions have no dedicated NodeKind variant; they map to Struct,
1870                // matching the nested-type handling in `walk_class_body`.
1871                let is_struct = matches!(node.kind(), "struct_specifier" | "union_specifier");
1872
1873                // Build qualified class name
1874                let qualified_class =
1875                    build_qualified_name(namespace_stack, class_stack, class_name);
1876
1877                // Add class/struct node with qualified name
1878                let visibility = "public";
1879                let class_id = if is_struct {
1880                    helper.add_struct_with_visibility(
1881                        &qualified_class,
1882                        Some(span),
1883                        Some(visibility),
1884                    )
1885                } else {
1886                    helper.add_class_with_visibility(&qualified_class, Some(span), Some(visibility))
1887                };
1888
1889                // Handle inheritance with qualified name
1890                // Also check for Implements edges (inheriting from pure virtual interfaces)
1891                build_inheritance_and_implements_edges(
1892                    node,
1893                    content,
1894                    &qualified_class,
1895                    class_id,
1896                    helper,
1897                    namespace_stack,
1898                    pure_virtual_registry,
1899                )?;
1900
1901                // Export classes/structs at file/namespace scope (not nested classes)
1902                // Nested classes have internal linkage unless explicitly exported
1903                if class_stack.is_empty() {
1904                    let module_id = helper.add_module(FILE_MODULE_NAME, None);
1905                    helper.add_export_edge(module_id, class_id);
1906                }
1907
1908                // Track class context for nested classes
1909                class_stack.push(class_name.to_string());
1910
1911                // Process class body with visibility tracking
1912                // Default visibility: struct = public, class = private
1913                if let Some(body) = node.child_by_field_name("body") {
1914                    walk_class_body(
1915                        body,
1916                        content,
1917                        &qualified_class,
1918                        is_struct,
1919                        ast_graph,
1920                        helper,
1921                        seen_includes,
1922                        namespace_stack,
1923                        class_stack,
1924                        ffi_registry,
1925                        pure_virtual_registry,
1926                        budget,
1927                    )?;
1928                }
1929
1930                class_stack.pop();
1931                return Ok(());
1932            }
1933        }
1934        "enum_specifier" => {
1935            if let Some(name_node) = node.child_by_field_name("name")
1936                && let Ok(enum_name) = name_node.utf8_text(content)
1937            {
1938                let enum_name = enum_name.trim();
1939                let span = span_from_node(node);
1940                let qualified_enum = build_qualified_name(namespace_stack, class_stack, enum_name);
1941                let enum_id = helper.add_enum(&qualified_enum, Some(span));
1942
1943                if class_stack.is_empty() {
1944                    let module_id = helper.add_module(FILE_MODULE_NAME, None);
1945                    helper.add_export_edge(module_id, enum_id);
1946                }
1947            }
1948        }
1949        "function_definition" => {
1950            // Skip if we're inside a class body - methods are handled by walk_class_body
1951            // to ensure correct visibility tracking. This check prevents double-adding
1952            // methods with incorrect visibility.
1953            if !class_stack.is_empty() {
1954                // Don't process the function definition as a node here, but do recurse
1955                // into its body to find call expressions
1956                let mut cursor = node.walk();
1957                for child in node.children(&mut cursor) {
1958                    walk_tree_for_graph(
1959                        child,
1960                        content,
1961                        ast_graph,
1962                        helper,
1963                        seen_includes,
1964                        namespace_stack,
1965                        class_stack,
1966                        ffi_registry,
1967                        pure_virtual_registry,
1968                        budget,
1969                    )?;
1970                }
1971                return Ok(());
1972            }
1973
1974            // Extract function context from AST graph by matching start position
1975            if let Some(context) = ast_graph.context_for_start(node.start_byte()) {
1976                let span = span_from_node(node);
1977
1978                // Determine if this is a method or free function based on context
1979                if context.class_stack.is_empty() {
1980                    // This is a free function
1981                    // Visibility: static = private (internal linkage), non-static = public (external linkage)
1982                    let visibility = if context.is_static {
1983                        "private"
1984                    } else {
1985                        "public"
1986                    };
1987                    let fn_id = helper.add_function_with_signature(
1988                        &context.qualified_name,
1989                        Some(span),
1990                        false, // C++ doesn't have async
1991                        false, // C++ doesn't use unsafe keyword
1992                        Some(visibility),
1993                        context.return_type.as_deref(),
1994                    );
1995
1996                    // Export non-static free functions (static functions have internal linkage)
1997                    if !context.is_static {
1998                        let module_id = helper.add_module(FILE_MODULE_NAME, None);
1999                        helper.add_export_edge(module_id, fn_id);
2000                    }
2001                } else {
2002                    // This is an out-of-class method definition (e.g., Resource::Resource())
2003                    // These are public by default in C++ (they must be declared in the class first)
2004                    // Note: We can't determine actual visibility here as that requires
2005                    // correlating with the in-class declaration
2006                    helper.add_method_with_signature(
2007                        &context.qualified_name,
2008                        Some(span),
2009                        false, // C++ doesn't have async
2010                        context.is_static,
2011                        Some("public"), // Default for out-of-class definitions
2012                        context.return_type.as_deref(),
2013                    );
2014                }
2015            }
2016        }
2017        "call_expression" => {
2018            // Build call edge
2019            if let Ok(Some((caller_qname, callee_qname, argument_count, span))) =
2020                build_call_for_staging(ast_graph, node, content)
2021            {
2022                // Ensure caller node exists
2023                let caller_function_id =
2024                    helper.ensure_callee(&caller_qname, span, CalleeKindHint::Function);
2025                let argument_count = u8::try_from(argument_count).unwrap_or(u8::MAX);
2026
2027                // Check if the callee is a known FFI function
2028                // Only do FFI lookup for unqualified calls (no ::)
2029                let is_unqualified = !callee_qname.contains("::");
2030                if is_unqualified {
2031                    if let Some((ffi_qualified, ffi_convention)) = ffi_registry.get(&callee_qname) {
2032                        // This is a call to an FFI function - create FfiCall edge
2033                        let ffi_target_id =
2034                            helper.ensure_callee(ffi_qualified, span, CalleeKindHint::Function);
2035                        helper.add_ffi_edge(caller_function_id, ffi_target_id, *ffi_convention);
2036                    } else {
2037                        // Regular call - create normal Call edge
2038                        let target_function_id =
2039                            helper.ensure_callee(&callee_qname, span, CalleeKindHint::Function);
2040                        helper.add_call_edge_full_with_span(
2041                            caller_function_id,
2042                            target_function_id,
2043                            argument_count,
2044                            false,
2045                            vec![span],
2046                        );
2047                    }
2048                } else {
2049                    // Qualified call - create normal Call edge
2050                    let target_function_id =
2051                        helper.ensure_callee(&callee_qname, span, CalleeKindHint::Function);
2052                    helper.add_call_edge_full_with_span(
2053                        caller_function_id,
2054                        target_function_id,
2055                        argument_count,
2056                        false,
2057                        vec![span],
2058                    );
2059                }
2060            }
2061        }
2062        "declaration" => {
2063            // Handle global/file-level variable declarations (not inside classes)
2064            // Only process if we're not inside a class (class members are handled in walk_class_body)
2065            if class_stack.is_empty() {
2066                process_global_variable_declaration(node, content, namespace_stack, helper)?;
2067            }
2068        }
2069        _ => {}
2070    }
2071
2072    // Recurse into children
2073    let mut cursor = node.walk();
2074    for child in node.children(&mut cursor) {
2075        walk_tree_for_graph(
2076            child,
2077            content,
2078            ast_graph,
2079            helper,
2080            seen_includes,
2081            namespace_stack,
2082            class_stack,
2083            ffi_registry,
2084            pure_virtual_registry,
2085            budget,
2086        )?;
2087    }
2088
2089    Ok(())
2090}
2091
2092/// Build call edge information for the staging graph.
2093fn build_call_for_staging(
2094    ast_graph: &ASTGraph,
2095    call_node: Node<'_>,
2096    content: &[u8],
2097) -> GraphResult<Option<(String, String, usize, Span)>> {
2098    // Find the enclosing function context
2099    let call_context = ast_graph.find_enclosing(call_node.start_byte());
2100    let caller_qualified_name = if let Some(ctx) = call_context {
2101        ctx.qualified_name.clone()
2102    } else {
2103        // Top-level call (e.g., global initializer)
2104        return Ok(None);
2105    };
2106
2107    let Some(function_node) = call_node.child_by_field_name("function") else {
2108        return Ok(None);
2109    };
2110
2111    let callee_text = function_node
2112        .utf8_text(content)
2113        .map_err(|_| GraphBuilderError::ParseError {
2114            span: span_from_node(call_node),
2115            reason: "failed to read call expression".to_string(),
2116        })?
2117        .trim();
2118
2119    if callee_text.is_empty() {
2120        return Ok(None);
2121    }
2122
2123    // Resolve callee name using context
2124    let target_qualified_name = if let Some(ctx) = call_context {
2125        resolve_callee_name(callee_text, ctx, ast_graph)
2126    } else {
2127        callee_text.to_string()
2128    };
2129
2130    let span = span_from_node(call_node);
2131    let argument_count = count_arguments(call_node);
2132
2133    Ok(Some((
2134        caller_qualified_name,
2135        target_qualified_name,
2136        argument_count,
2137        span,
2138    )))
2139}
2140
2141/// Build import edge for `#include` directives.
2142///
2143/// Handles both system includes (`<header>`) and local includes (`"header"`).
2144/// Per the implementation plan, include type (system/local) is tracked via
2145/// node metadata, not the edge's alias field (alias is for import renaming only).
2146/// Duplicate includes are deduplicated using the `seen_includes` set.
2147fn build_import_edge(
2148    include_node: Node<'_>,
2149    content: &[u8],
2150    helper: &mut GraphBuildHelper,
2151    seen_includes: &mut HashSet<String>,
2152) -> GraphResult<()> {
2153    // Look for path child (system_lib_string or string_literal)
2154    let path_node = include_node.child_by_field_name("path").or_else(|| {
2155        // Fallback: find first child that looks like a path
2156        let mut cursor = include_node.walk();
2157        include_node.children(&mut cursor).find(|child| {
2158            matches!(
2159                child.kind(),
2160                "system_lib_string" | "string_literal" | "string_content"
2161            )
2162        })
2163    });
2164
2165    let Some(path_node) = path_node else {
2166        return Ok(());
2167    };
2168
2169    let include_path = path_node
2170        .utf8_text(content)
2171        .map_err(|_| GraphBuilderError::ParseError {
2172            span: span_from_node(include_node),
2173            reason: "failed to read include path".to_string(),
2174        })?
2175        .trim();
2176
2177    if include_path.is_empty() {
2178        return Ok(());
2179    }
2180
2181    // Determine include type and clean up path
2182    let is_system_include = include_path.starts_with('<') && include_path.ends_with('>');
2183    let cleaned_path = if is_system_include {
2184        // System include: <iostream> -> iostream
2185        include_path.trim_start_matches('<').trim_end_matches('>')
2186    } else {
2187        // Local include: "myheader.hpp" -> myheader.hpp
2188        include_path.trim_start_matches('"').trim_end_matches('"')
2189    };
2190
2191    if cleaned_path.is_empty() {
2192        return Ok(());
2193    }
2194
2195    // Deduplicate includes - only add if not seen before
2196    if !seen_includes.insert(cleaned_path.to_string()) {
2197        return Ok(()); // Already seen this include
2198    }
2199
2200    // Create module node for the file being compiled (importer)
2201    let file_module_id = helper.add_module("<file>", None);
2202
2203    // Create import node for the included header
2204    let span = span_from_node(include_node);
2205    let import_id = helper.add_import(cleaned_path, Some(span));
2206
2207    // Add import edge - no alias for #include (alias is for renaming, which C++ doesn't support)
2208    // is_wildcard is false since #include brings in the whole header (but it's not a wildcard import)
2209    helper.add_import_edge(file_module_id, import_id);
2210
2211    Ok(())
2212}
2213
2214// ================================
2215// FFI Support Functions
2216// ================================
2217
2218/// Collect FFI declarations from extern "C" blocks (Pass 1).
2219///
2220/// This function walks the entire AST to find all `extern "C" { ... }` blocks
2221/// and populates the FFI registry with function name → (qualified name, convention)
2222/// mappings. This must be done before processing calls so that FFI calls can be
2223/// properly linked regardless of source code order.
2224fn collect_ffi_declarations(
2225    node: Node<'_>,
2226    content: &[u8],
2227    ffi_registry: &mut FfiRegistry,
2228    budget: &mut BuildBudget,
2229) -> GraphResult<()> {
2230    budget.checkpoint("cpp:collect_ffi_declarations")?;
2231    if node.kind() == "linkage_specification" {
2232        // Get the ABI string (e.g., "C")
2233        let abi = extract_ffi_abi(node, content);
2234        let convention = abi_to_convention(&abi);
2235
2236        // Find the body child (declaration_list or single declaration)
2237        if let Some(body_node) = node.child_by_field_name("body") {
2238            collect_ffi_from_body(body_node, content, &abi, convention, ffi_registry);
2239        }
2240    }
2241
2242    // Recurse into children
2243    let mut cursor = node.walk();
2244    for child in node.children(&mut cursor) {
2245        collect_ffi_declarations(child, content, ffi_registry, budget)?;
2246    }
2247
2248    Ok(())
2249}
2250
2251/// Collect FFI declarations from a linkage specification body.
2252fn collect_ffi_from_body(
2253    body_node: Node<'_>,
2254    content: &[u8],
2255    abi: &str,
2256    convention: FfiConvention,
2257    ffi_registry: &mut FfiRegistry,
2258) {
2259    match body_node.kind() {
2260        "declaration_list" => {
2261            // Multiple declarations in the block
2262            let mut cursor = body_node.walk();
2263            for decl in body_node.children(&mut cursor) {
2264                if decl.kind() == "declaration"
2265                    && let Some(fn_name) = extract_ffi_function_name(decl, content)
2266                {
2267                    let qualified = format!("extern::{abi}::{fn_name}");
2268                    ffi_registry.insert(fn_name, (qualified, convention));
2269                }
2270            }
2271        }
2272        "declaration" => {
2273            // Single declaration (e.g., extern "C" void foo();)
2274            if let Some(fn_name) = extract_ffi_function_name(body_node, content) {
2275                let qualified = format!("extern::{abi}::{fn_name}");
2276                ffi_registry.insert(fn_name, (qualified, convention));
2277            }
2278        }
2279        _ => {}
2280    }
2281}
2282
2283/// Extract function name from an FFI declaration.
2284fn extract_ffi_function_name(decl_node: Node<'_>, content: &[u8]) -> Option<String> {
2285    // Look for declarator field which contains the function declarator
2286    if let Some(declarator_node) = decl_node.child_by_field_name("declarator") {
2287        return extract_function_name_from_declarator(declarator_node, content);
2288    }
2289    None
2290}
2291
2292/// Recursively extract function name from a declarator node.
2293fn extract_function_name_from_declarator(node: Node<'_>, content: &[u8]) -> Option<String> {
2294    match node.kind() {
2295        "function_declarator" => {
2296            // Function declarator has a nested declarator with the name
2297            if let Some(inner) = node.child_by_field_name("declarator") {
2298                return extract_function_name_from_declarator(inner, content);
2299            }
2300        }
2301        "identifier" => {
2302            // Found the name
2303            if let Ok(name) = node.utf8_text(content) {
2304                let name = name.trim();
2305                if !name.is_empty() {
2306                    return Some(name.to_string());
2307                }
2308            }
2309        }
2310        "pointer_declarator" | "reference_declarator" => {
2311            // Handle pointer/reference declarators (e.g., int* (*foo)())
2312            if let Some(inner) = node.child_by_field_name("declarator") {
2313                return extract_function_name_from_declarator(inner, content);
2314            }
2315        }
2316        "parenthesized_declarator" => {
2317            // Handle parenthesized declarators
2318            let mut cursor = node.walk();
2319            for child in node.children(&mut cursor) {
2320                if let Some(name) = extract_function_name_from_declarator(child, content) {
2321                    return Some(name);
2322                }
2323            }
2324        }
2325        _ => {}
2326    }
2327    None
2328}
2329
2330/// Extract the ABI string from an extern "X" block.
2331///
2332/// Returns the ABI string (e.g., "C") or "C" as default.
2333fn extract_ffi_abi(node: Node<'_>, content: &[u8]) -> String {
2334    // Look for the "value" field which contains the string literal
2335    if let Some(value_node) = node.child_by_field_name("value")
2336        && value_node.kind() == "string_literal"
2337    {
2338        // Look for string_content child
2339        let mut cursor = value_node.walk();
2340        for child in value_node.children(&mut cursor) {
2341            if child.kind() == "string_content"
2342                && let Ok(text) = child.utf8_text(content)
2343            {
2344                let trimmed = text.trim();
2345                if !trimmed.is_empty() {
2346                    return trimmed.to_string();
2347                }
2348            }
2349        }
2350    }
2351    // Default to "C" if no ABI specified
2352    "C".to_string()
2353}
2354
2355/// Convert an ABI string to an FFI calling convention.
2356fn abi_to_convention(abi: &str) -> FfiConvention {
2357    match abi.to_lowercase().as_str() {
2358        "system" => FfiConvention::System,
2359        "stdcall" => FfiConvention::Stdcall,
2360        "fastcall" => FfiConvention::Fastcall,
2361        "cdecl" => FfiConvention::Cdecl,
2362        _ => FfiConvention::C, // Default to C
2363    }
2364}
2365
2366/// Build FFI function declarations from extern "C" blocks.
2367///
2368/// Creates Function nodes for FFI declarations with unsafe=true.
2369fn build_ffi_block_for_staging(
2370    node: Node<'_>,
2371    content: &[u8],
2372    helper: &mut GraphBuildHelper,
2373    namespace_stack: &[String],
2374) {
2375    // Get the ABI string
2376    let abi = extract_ffi_abi(node, content);
2377
2378    // Find the body child
2379    if let Some(body_node) = node.child_by_field_name("body") {
2380        build_ffi_from_body(body_node, content, &abi, helper, namespace_stack);
2381    }
2382}
2383
2384/// Build FFI function nodes from a linkage specification body.
2385fn build_ffi_from_body(
2386    body_node: Node<'_>,
2387    content: &[u8],
2388    abi: &str,
2389    helper: &mut GraphBuildHelper,
2390    namespace_stack: &[String],
2391) {
2392    match body_node.kind() {
2393        "declaration_list" => {
2394            // Multiple declarations in the block
2395            let mut cursor = body_node.walk();
2396            for decl in body_node.children(&mut cursor) {
2397                if decl.kind() == "declaration"
2398                    && let Some(fn_name) = extract_ffi_function_name(decl, content)
2399                {
2400                    let span = span_from_node(decl);
2401                    // Build qualified name with namespace context
2402                    let qualified = if namespace_stack.is_empty() {
2403                        format!("extern::{abi}::{fn_name}")
2404                    } else {
2405                        format!("{}::extern::{abi}::{fn_name}", namespace_stack.join("::"))
2406                    };
2407                    // Add as unsafe function (FFI functions are inherently unsafe)
2408                    helper.add_function(
2409                        &qualified,
2410                        Some(span),
2411                        false, // not async
2412                        true,  // unsafe (FFI)
2413                    );
2414                }
2415            }
2416        }
2417        "declaration" => {
2418            // Single declaration
2419            if let Some(fn_name) = extract_ffi_function_name(body_node, content) {
2420                let span = span_from_node(body_node);
2421                let qualified = if namespace_stack.is_empty() {
2422                    format!("extern::{abi}::{fn_name}")
2423                } else {
2424                    format!("{}::extern::{abi}::{fn_name}", namespace_stack.join("::"))
2425                };
2426                helper.add_function(&qualified, Some(span), false, true);
2427            }
2428        }
2429        _ => {}
2430    }
2431}
2432
2433// ================================
2434// Pure Virtual Interface Support
2435// ================================
2436
2437/// Collect pure virtual interfaces (abstract classes with pure virtual methods).
2438///
2439/// A class is considered a "pure virtual interface" if it contains at least one
2440/// pure virtual method (declared with `= 0`). Classes that inherit from such
2441/// interfaces will get Implements edges instead of just Inherits edges.
2442fn collect_pure_virtual_interfaces(
2443    node: Node<'_>,
2444    content: &[u8],
2445    registry: &mut PureVirtualRegistry,
2446    budget: &mut BuildBudget,
2447) -> GraphResult<()> {
2448    budget.checkpoint("cpp:collect_pure_virtual_interfaces")?;
2449    if matches!(node.kind(), "class_specifier" | "struct_specifier")
2450        && let Some(name_node) = node.child_by_field_name("name")
2451        && let Ok(class_name) = name_node.utf8_text(content)
2452    {
2453        let class_name = class_name.trim();
2454        if !class_name.is_empty() && has_pure_virtual_methods(node, content) {
2455            registry.insert(class_name.to_string());
2456        }
2457    }
2458
2459    // Recurse into children
2460    let mut cursor = node.walk();
2461    for child in node.children(&mut cursor) {
2462        collect_pure_virtual_interfaces(child, content, registry, budget)?;
2463    }
2464
2465    Ok(())
2466}
2467
2468/// Check if a class/struct has any pure virtual methods.
2469///
2470/// Pure virtual methods are declared as `virtual ReturnType name() = 0;`
2471fn has_pure_virtual_methods(class_node: Node<'_>, content: &[u8]) -> bool {
2472    if let Some(body) = class_node.child_by_field_name("body") {
2473        let mut cursor = body.walk();
2474        for child in body.children(&mut cursor) {
2475            // Look for field_declaration with virtual and = 0
2476            if child.kind() == "field_declaration" && is_pure_virtual_declaration(child, content) {
2477                return true;
2478            }
2479        }
2480    }
2481    false
2482}
2483
2484/// Check if a field declaration is a pure virtual method (has `virtual` and `= 0`).
2485fn is_pure_virtual_declaration(decl_node: Node<'_>, content: &[u8]) -> bool {
2486    let mut has_virtual = false;
2487    let mut has_pure_specifier = false;
2488
2489    // Check children for virtual keyword and default_value of 0
2490    let mut cursor = decl_node.walk();
2491    for child in decl_node.children(&mut cursor) {
2492        match child.kind() {
2493            "virtual" => {
2494                has_virtual = true;
2495            }
2496            "number_literal" => {
2497                // Check if this is the pure virtual specifier (= 0)
2498                // The number_literal with value "0" after "=" indicates a pure virtual method
2499                if let Ok(text) = child.utf8_text(content)
2500                    && text.trim() == "0"
2501                {
2502                    has_pure_specifier = true;
2503                }
2504            }
2505            _ => {}
2506        }
2507    }
2508
2509    has_virtual && has_pure_specifier
2510}
2511
2512/// Build inheritance and implements edges for a class/struct.
2513///
2514/// For each base class:
2515/// - If the base class is a pure virtual interface, create an Implements edge
2516/// - Otherwise, create an Inherits edge
2517fn build_inheritance_and_implements_edges(
2518    class_node: Node<'_>,
2519    content: &[u8],
2520    _qualified_class_name: &str,
2521    child_id: sqry_core::graph::unified::node::NodeId,
2522    helper: &mut GraphBuildHelper,
2523    namespace_stack: &[String],
2524    pure_virtual_registry: &PureVirtualRegistry,
2525) -> GraphResult<()> {
2526    // Look for base_class_clause child
2527    let mut cursor = class_node.walk();
2528    let base_clause = class_node
2529        .children(&mut cursor)
2530        .find(|child| child.kind() == "base_class_clause");
2531
2532    let Some(base_clause) = base_clause else {
2533        return Ok(()); // No inheritance
2534    };
2535
2536    // Parse all base classes from the base_class_clause
2537    let mut clause_cursor = base_clause.walk();
2538    for child in base_clause.children(&mut clause_cursor) {
2539        match child.kind() {
2540            "type_identifier" => {
2541                let base_name = child
2542                    .utf8_text(content)
2543                    .map_err(|_| GraphBuilderError::ParseError {
2544                        span: span_from_node(child),
2545                        reason: "failed to read base class name".to_string(),
2546                    })?
2547                    .trim();
2548
2549                if !base_name.is_empty() {
2550                    // Qualify with namespace if present
2551                    let qualified_base = if namespace_stack.is_empty() {
2552                        base_name.to_string()
2553                    } else {
2554                        format!("{}::{}", namespace_stack.join("::"), base_name)
2555                    };
2556
2557                    // Check if base is a pure virtual interface
2558                    if pure_virtual_registry.contains(base_name) {
2559                        // Create interface node and Implements edge
2560                        let interface_id = helper.add_interface(&qualified_base, None);
2561                        helper.add_implements_edge(child_id, interface_id);
2562                    } else {
2563                        // Regular inheritance - create Inherits edge
2564                        let parent_id = helper.add_class(&qualified_base, None);
2565                        helper.add_inherits_edge(child_id, parent_id);
2566                    }
2567                }
2568            }
2569            "qualified_identifier" => {
2570                // Already qualified - use as-is
2571                let base_name = child
2572                    .utf8_text(content)
2573                    .map_err(|_| GraphBuilderError::ParseError {
2574                        span: span_from_node(child),
2575                        reason: "failed to read base class name".to_string(),
2576                    })?
2577                    .trim();
2578
2579                if !base_name.is_empty() {
2580                    // Extract simple name for registry lookup
2581                    let simple_name = base_name.rsplit("::").next().unwrap_or(base_name);
2582
2583                    if pure_virtual_registry.contains(simple_name) {
2584                        let interface_id = helper.add_interface(base_name, None);
2585                        helper.add_implements_edge(child_id, interface_id);
2586                    } else {
2587                        let parent_id = helper.add_class(base_name, None);
2588                        helper.add_inherits_edge(child_id, parent_id);
2589                    }
2590                }
2591            }
2592            "template_type" => {
2593                // Template base class: Base<T>
2594                if let Some(template_name_node) = child.child_by_field_name("name")
2595                    && let Ok(base_name) = template_name_node.utf8_text(content)
2596                {
2597                    let base_name = base_name.trim();
2598                    if !base_name.is_empty() {
2599                        let qualified_base =
2600                            if base_name.contains("::") || namespace_stack.is_empty() {
2601                                base_name.to_string()
2602                            } else {
2603                                format!("{}::{}", namespace_stack.join("::"), base_name)
2604                            };
2605
2606                        // Template bases are typically not pure virtual interfaces
2607                        // but check anyway
2608                        if pure_virtual_registry.contains(base_name) {
2609                            let interface_id = helper.add_interface(&qualified_base, None);
2610                            helper.add_implements_edge(child_id, interface_id);
2611                        } else {
2612                            let parent_id = helper.add_class(&qualified_base, None);
2613                            helper.add_inherits_edge(child_id, parent_id);
2614                        }
2615                    }
2616                }
2617            }
2618            _ => {
2619                // Skip access specifiers, colons, commas, and other non-base nodes.
2620            }
2621        }
2622    }
2623
2624    Ok(())
2625}
2626
2627fn span_from_node(node: Node<'_>) -> Span {
2628    let start = node.start_position();
2629    let end = node.end_position();
2630    Span::new(
2631        sqry_core::graph::node::Position::new(start.row, start.column),
2632        sqry_core::graph::node::Position::new(end.row, end.column),
2633    )
2634}
2635
2636fn count_arguments(node: Node<'_>) -> usize {
2637    node.child_by_field_name("arguments").map_or(0, |args| {
2638        let mut count = 0;
2639        let mut cursor = args.walk();
2640        for child in args.children(&mut cursor) {
2641            if !matches!(child.kind(), "(" | ")" | ",") {
2642                count += 1;
2643            }
2644        }
2645        count
2646    })
2647}
2648
2649#[cfg(test)]
2650mod tests {
2651    use super::*;
2652    use sqry_core::graph::unified::build::test_helpers::{
2653        assert_has_node, assert_has_node_with_kind, assert_has_node_with_kind_exact,
2654        collect_call_edges,
2655    };
2656    use sqry_core::graph::unified::node::NodeKind;
2657    use tree_sitter::Parser;
2658
2659    fn parse_cpp(source: &str) -> Tree {
2660        let mut parser = Parser::new();
2661        parser
2662            .set_language(&tree_sitter_cpp::LANGUAGE.into())
2663            .expect("Failed to set Cpp language");
2664        parser
2665            .parse(source.as_bytes(), None)
2666            .expect("Failed to parse Cpp source")
2667    }
2668
2669    fn test_budget() -> BuildBudget {
2670        BuildBudget::new(Path::new("test.cpp"))
2671    }
2672
2673    fn extract_namespace_map_for_test(
2674        tree: &Tree,
2675        source: &str,
2676    ) -> HashMap<std::ops::Range<usize>, String> {
2677        let mut budget = test_budget();
2678        extract_namespace_map(tree.root_node(), source.as_bytes(), &mut budget)
2679            .expect("namespace extraction should succeed in tests")
2680    }
2681
2682    fn extract_cpp_contexts_for_test(
2683        tree: &Tree,
2684        source: &str,
2685        namespace_map: &HashMap<std::ops::Range<usize>, String>,
2686    ) -> Vec<FunctionContext> {
2687        let mut budget = test_budget();
2688        extract_cpp_contexts(
2689            tree.root_node(),
2690            source.as_bytes(),
2691            namespace_map,
2692            &mut budget,
2693        )
2694        .expect("context extraction should succeed in tests")
2695    }
2696
2697    fn extract_field_and_type_info_for_test(
2698        tree: &Tree,
2699        source: &str,
2700        namespace_map: &HashMap<std::ops::Range<usize>, String>,
2701    ) -> (QualifiedNameMap, QualifiedNameMap) {
2702        let mut budget = test_budget();
2703        extract_field_and_type_info(
2704            tree.root_node(),
2705            source.as_bytes(),
2706            namespace_map,
2707            &mut budget,
2708        )
2709        .expect("field/type extraction should succeed in tests")
2710    }
2711
2712    #[test]
2713    fn test_build_graph_times_out_with_expired_budget() {
2714        let source = r"
2715            namespace demo {
2716                class Service {
2717                public:
2718                    void process() {}
2719                };
2720            }
2721        ";
2722        let tree = parse_cpp(source);
2723        let builder = CppGraphBuilder::new();
2724        let mut staging = StagingGraph::new();
2725        let mut budget = BuildBudget::already_expired(Path::new("timeout.cpp"));
2726
2727        let err = builder
2728            .build_graph_with_budget(
2729                &tree,
2730                source.as_bytes(),
2731                Path::new("timeout.cpp"),
2732                &mut staging,
2733                &mut budget,
2734            )
2735            .expect_err("expired budget should force timeout");
2736
2737        match err {
2738            GraphBuilderError::BuildTimedOut {
2739                file,
2740                phase,
2741                timeout_ms,
2742            } => {
2743                assert_eq!(file, PathBuf::from("timeout.cpp"));
2744                assert_eq!(phase, "cpp:extract_namespace_map");
2745                assert_eq!(timeout_ms, 1_000);
2746            }
2747            other => panic!("expected BuildTimedOut, got {other:?}"),
2748        }
2749    }
2750
2751    #[test]
2752    fn test_extract_class() {
2753        let source = "class User { }";
2754        let tree = parse_cpp(source);
2755        let mut staging = StagingGraph::new();
2756        let builder = CppGraphBuilder::new();
2757
2758        let result = builder.build_graph(
2759            &tree,
2760            source.as_bytes(),
2761            Path::new("test.cpp"),
2762            &mut staging,
2763        );
2764
2765        assert!(result.is_ok());
2766        assert_has_node_with_kind(&staging, "User", NodeKind::Class);
2767    }
2768
2769    #[test]
2770    fn test_extract_template_class() {
2771        let source = r"
2772            template <typename T>
2773            class Person {
2774            public:
2775                T name;
2776                T age;
2777            };
2778        ";
2779        let tree = parse_cpp(source);
2780        let mut staging = StagingGraph::new();
2781        let builder = CppGraphBuilder::new();
2782
2783        let result = builder.build_graph(
2784            &tree,
2785            source.as_bytes(),
2786            Path::new("test.cpp"),
2787            &mut staging,
2788        );
2789
2790        assert!(result.is_ok());
2791        assert_has_node_with_kind(&staging, "Person", NodeKind::Class);
2792    }
2793
2794    #[test]
2795    fn test_nested_named_types_emit_nodes() {
2796        // Regression: nested class/struct/union/enum declared inside a class body
2797        // must each emit their OWN type node (previously only their members were
2798        // staged, so `kind:class` / `kind:struct` / `kind:enum` could not see
2799        // them). Covers doubly-nested chains and namespace-nested chains.
2800        let source = r"
2801            class Outer {
2802            public:
2803                class Inner { int z; };
2804                struct InnerS { int w; };
2805                union InnerU { int i; float f; };
2806                enum class InnerE { A, B };
2807                class L1 { public: class L2 { int q; }; };
2808            };
2809            namespace ns {
2810                class NsOuter { public: class NsInner { int n; }; };
2811            }
2812        ";
2813        let staging = build_cpp(source);
2814
2815        // Each nested type emits a node with the `Outer::Inner` qualified shape.
2816        assert_has_node_with_kind_exact(&staging, "Outer::Inner", NodeKind::Class);
2817        assert_has_node_with_kind_exact(&staging, "Outer::InnerS", NodeKind::Struct);
2818        // Unions map to NodeKind::Struct (no dedicated Union variant).
2819        assert_has_node_with_kind_exact(&staging, "Outer::InnerU", NodeKind::Struct);
2820        assert_has_node_with_kind_exact(&staging, "Outer::InnerE", NodeKind::Enum);
2821        // Doubly nested: `Outer::L1` and `Outer::L1::L2`.
2822        assert_has_node_with_kind_exact(&staging, "Outer::L1", NodeKind::Class);
2823        assert_has_node_with_kind_exact(&staging, "Outer::L1::L2", NodeKind::Class);
2824        // Nested inside a namespaced class.
2825        assert_has_node_with_kind_exact(&staging, "ns::NsOuter", NodeKind::Class);
2826        assert_has_node_with_kind_exact(&staging, "ns::NsOuter::NsInner", NodeKind::Class);
2827
2828        // Members still qualify under the nested chain (regression guard: the
2829        // member-walk behaviour that already worked must be preserved).
2830        assert_has_node_with_kind_exact(&staging, "Outer::Inner.z", NodeKind::Property);
2831        assert_has_node_with_kind_exact(&staging, "Outer::L1::L2.q", NodeKind::Property);
2832        assert_has_node_with_kind_exact(&staging, "ns::NsOuter::NsInner.n", NodeKind::Property);
2833    }
2834
2835    #[test]
2836    fn test_nested_enum_carries_enclosing_visibility() {
2837        // Nested enums must carry the enclosing access visibility, identical to
2838        // the nested class/struct path — not an absent visibility. A nested enum
2839        // under `private:` is `private`; under `public:` is `public`.
2840        let source = r"
2841            class Outer {
2842            private:
2843                enum class Secret { A, B };
2844            public:
2845                enum class Pub { X, Y };
2846            };
2847        ";
2848        let staging = build_cpp(source);
2849
2850        let secret = cpp_find_added_node(&staging, "Outer::Secret")
2851            .expect("nested enum Outer::Secret must be staged");
2852        assert_eq!(secret.kind, NodeKind::Enum, "Secret must be an Enum node");
2853        let secret_vis = staging.resolve_local_string(
2854            secret
2855                .visibility
2856                .expect("nested enum must carry a visibility id"),
2857        );
2858        assert_eq!(
2859            secret_vis,
2860            Some("private"),
2861            "nested enum under `private:` must be private"
2862        );
2863
2864        let pub_enum = cpp_find_added_node(&staging, "Outer::Pub")
2865            .expect("nested enum Outer::Pub must be staged");
2866        let pub_vis = staging.resolve_local_string(
2867            pub_enum
2868                .visibility
2869                .expect("nested enum must carry a visibility id"),
2870        );
2871        assert_eq!(
2872            pub_vis,
2873            Some("public"),
2874            "nested enum under `public:` must be public"
2875        );
2876    }
2877
2878    #[test]
2879    fn test_nested_class_emits_inheritance_edge() {
2880        // Regression: a nested class with a base clause must emit an `Inherits`
2881        // edge anchored on the nested class node (previously the nested type was
2882        // never registered, so its lineage edge was lost entirely).
2883        let source = r"
2884            struct Base { virtual ~Base(); };
2885            class Outer {
2886            public:
2887                class Derived : public Base {};
2888            };
2889        ";
2890        let staging = build_cpp(source);
2891
2892        let derived_id = cpp_find_added_node_id(&staging, "Outer::Derived", NodeKind::Class)
2893            .expect("nested Derived class node must be staged");
2894
2895        let has_inherits = staging.operations().iter().any(|op| {
2896            matches!(
2897                op,
2898                StagingOp::AddEdge {
2899                    source: src,
2900                    kind: EdgeKind::Inherits,
2901                    ..
2902                } if *src == derived_id
2903            )
2904        });
2905        assert!(
2906            has_inherits,
2907            "nested Derived must emit an Inherits edge to its base"
2908        );
2909    }
2910
2911    #[test]
2912    fn test_top_level_union_emits_struct_node() {
2913        // Regression: top-level `union` declarations previously produced no node
2914        // (only `class_specifier` / `struct_specifier` were matched). Unions map
2915        // to NodeKind::Struct.
2916        let source = "union Value { int i; float f; };";
2917        let staging = build_cpp(source);
2918        assert_has_node_with_kind_exact(&staging, "Value", NodeKind::Struct);
2919    }
2920
2921    #[test]
2922    fn test_extract_function() {
2923        let source = r#"
2924            #include <cstdio>
2925            void hello() {
2926                std::printf("Hello");
2927            }
2928        "#;
2929        let tree = parse_cpp(source);
2930        let mut staging = StagingGraph::new();
2931        let builder = CppGraphBuilder::new();
2932
2933        let result = builder.build_graph(
2934            &tree,
2935            source.as_bytes(),
2936            Path::new("test.cpp"),
2937            &mut staging,
2938        );
2939
2940        assert!(result.is_ok());
2941        assert_has_node_with_kind(&staging, "hello", NodeKind::Function);
2942    }
2943
2944    #[test]
2945    fn test_extract_virtual_function() {
2946        let source = r"
2947            class Service {
2948            public:
2949                virtual void fetchData() {}
2950            };
2951        ";
2952        let tree = parse_cpp(source);
2953        let mut staging = StagingGraph::new();
2954        let builder = CppGraphBuilder::new();
2955
2956        let result = builder.build_graph(
2957            &tree,
2958            source.as_bytes(),
2959            Path::new("test.cpp"),
2960            &mut staging,
2961        );
2962
2963        assert!(result.is_ok());
2964        assert_has_node(&staging, "fetchData");
2965    }
2966
2967    #[test]
2968    fn test_extract_call_edge() {
2969        let source = r"
2970            void greet() {}
2971
2972            int main() {
2973                greet();
2974                return 0;
2975            }
2976        ";
2977        let tree = parse_cpp(source);
2978        let mut staging = StagingGraph::new();
2979        let builder = CppGraphBuilder::new();
2980
2981        let result = builder.build_graph(
2982            &tree,
2983            source.as_bytes(),
2984            Path::new("test.cpp"),
2985            &mut staging,
2986        );
2987
2988        assert!(result.is_ok());
2989        assert_has_node(&staging, "main");
2990        assert_has_node(&staging, "greet");
2991        let calls = collect_call_edges(&staging);
2992        assert!(!calls.is_empty());
2993    }
2994
2995    #[test]
2996    fn test_extract_member_call_edge() {
2997        let source = r"
2998            class Service {
2999            public:
3000                void helper() {}
3001            };
3002
3003            int main() {
3004                Service svc;
3005                svc.helper();
3006                return 0;
3007            }
3008        ";
3009        let tree = parse_cpp(source);
3010        let mut staging = StagingGraph::new();
3011        let builder = CppGraphBuilder::new();
3012
3013        let result = builder.build_graph(
3014            &tree,
3015            source.as_bytes(),
3016            Path::new("member.cpp"),
3017            &mut staging,
3018        );
3019
3020        assert!(result.is_ok());
3021        assert_has_node(&staging, "main");
3022        assert_has_node(&staging, "helper");
3023        let calls = collect_call_edges(&staging);
3024        assert!(!calls.is_empty());
3025    }
3026
3027    #[test]
3028    fn test_extract_namespace_map_simple() {
3029        let source = r"
3030            namespace demo {
3031                void func() {}
3032            }
3033        ";
3034        let tree = parse_cpp(source);
3035        let namespace_map = extract_namespace_map_for_test(&tree, source);
3036
3037        // Should have one entry mapping the namespace body to "demo::"
3038        assert_eq!(namespace_map.len(), 1);
3039
3040        // Find any namespace entry (we only have one)
3041        let (_, ns_prefix) = namespace_map.iter().next().unwrap();
3042        assert_eq!(ns_prefix, "demo::");
3043    }
3044
3045    #[test]
3046    fn test_extract_namespace_map_nested() {
3047        let source = r"
3048            namespace outer {
3049                namespace inner {
3050                    void func() {}
3051                }
3052            }
3053        ";
3054        let tree = parse_cpp(source);
3055        let namespace_map = extract_namespace_map_for_test(&tree, source);
3056
3057        // Should have entries for both outer and inner namespaces
3058        assert!(namespace_map.len() >= 2);
3059
3060        // Check that we have the expected namespace prefixes
3061        let ns_values: Vec<&String> = namespace_map.values().collect();
3062        assert!(ns_values.iter().any(|v| v.as_str() == "outer::"));
3063        assert!(ns_values.iter().any(|v| v.as_str() == "outer::inner::"));
3064    }
3065
3066    #[test]
3067    fn test_extract_namespace_map_multiple() {
3068        let source = r"
3069            namespace first {
3070                void func1() {}
3071            }
3072            namespace second {
3073                void func2() {}
3074            }
3075        ";
3076        let tree = parse_cpp(source);
3077        let namespace_map = extract_namespace_map_for_test(&tree, source);
3078
3079        // Should have entries for both namespaces
3080        assert_eq!(namespace_map.len(), 2);
3081
3082        let ns_values: Vec<&String> = namespace_map.values().collect();
3083        assert!(ns_values.iter().any(|v| v.as_str() == "first::"));
3084        assert!(ns_values.iter().any(|v| v.as_str() == "second::"));
3085    }
3086
3087    #[test]
3088    fn test_find_namespace_for_offset() {
3089        let source = r"
3090            namespace demo {
3091                void func() {}
3092            }
3093        ";
3094        let tree = parse_cpp(source);
3095        let namespace_map = extract_namespace_map_for_test(&tree, source);
3096
3097        // Find the byte offset of "func" (should be inside demo namespace)
3098        let func_offset = source.find("func").unwrap();
3099        let ns = find_namespace_for_offset(func_offset, &namespace_map);
3100        assert_eq!(ns, "demo::");
3101
3102        // Byte offset before namespace should return empty string
3103        let ns = find_namespace_for_offset(0, &namespace_map);
3104        assert_eq!(ns, "");
3105    }
3106
3107    #[test]
3108    fn test_extract_cpp_contexts_free_function() {
3109        let source = r"
3110            void helper() {}
3111        ";
3112        let tree = parse_cpp(source);
3113        let namespace_map = extract_namespace_map_for_test(&tree, source);
3114        let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3115
3116        assert_eq!(contexts.len(), 1);
3117        assert_eq!(contexts[0].qualified_name, "helper");
3118        assert!(!contexts[0].is_static);
3119        assert!(!contexts[0].is_virtual);
3120    }
3121
3122    #[test]
3123    fn test_extract_cpp_contexts_namespace_function() {
3124        let source = r"
3125            namespace demo {
3126                void helper() {}
3127            }
3128        ";
3129        let tree = parse_cpp(source);
3130        let namespace_map = extract_namespace_map_for_test(&tree, source);
3131        let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3132
3133        assert_eq!(contexts.len(), 1);
3134        assert_eq!(contexts[0].qualified_name, "demo::helper");
3135        assert_eq!(contexts[0].namespace_stack, vec!["demo"]);
3136    }
3137
3138    #[test]
3139    fn test_extract_cpp_contexts_class_method() {
3140        let source = r"
3141            class Service {
3142            public:
3143                void process() {}
3144            };
3145        ";
3146        let tree = parse_cpp(source);
3147        let namespace_map = extract_namespace_map_for_test(&tree, source);
3148        let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3149
3150        assert_eq!(contexts.len(), 1);
3151        assert_eq!(contexts[0].qualified_name, "Service::process");
3152        assert_eq!(contexts[0].class_stack, vec!["Service"]);
3153    }
3154
3155    #[test]
3156    fn test_extract_cpp_contexts_namespace_and_class() {
3157        let source = r"
3158            namespace demo {
3159                class Service {
3160                public:
3161                    void process() {}
3162                };
3163            }
3164        ";
3165        let tree = parse_cpp(source);
3166        let namespace_map = extract_namespace_map_for_test(&tree, source);
3167        let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3168
3169        assert_eq!(contexts.len(), 1);
3170        assert_eq!(contexts[0].qualified_name, "demo::Service::process");
3171        assert_eq!(contexts[0].namespace_stack, vec!["demo"]);
3172        assert_eq!(contexts[0].class_stack, vec!["Service"]);
3173    }
3174
3175    #[test]
3176    fn test_extract_cpp_contexts_static_method() {
3177        let source = r"
3178            class Repository {
3179            public:
3180                static void save() {}
3181            };
3182        ";
3183        let tree = parse_cpp(source);
3184        let namespace_map = extract_namespace_map_for_test(&tree, source);
3185        let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3186
3187        assert_eq!(contexts.len(), 1);
3188        assert_eq!(contexts[0].qualified_name, "Repository::save");
3189        assert!(contexts[0].is_static);
3190    }
3191
3192    #[test]
3193    fn test_extract_cpp_contexts_virtual_method() {
3194        let source = r"
3195            class Base {
3196            public:
3197                virtual void render() {}
3198            };
3199        ";
3200        let tree = parse_cpp(source);
3201        let namespace_map = extract_namespace_map_for_test(&tree, source);
3202        let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3203
3204        assert_eq!(contexts.len(), 1);
3205        assert_eq!(contexts[0].qualified_name, "Base::render");
3206        assert!(contexts[0].is_virtual);
3207    }
3208
3209    #[test]
3210    fn test_extract_cpp_contexts_inline_function() {
3211        let source = r"
3212            inline void helper() {}
3213        ";
3214        let tree = parse_cpp(source);
3215        let namespace_map = extract_namespace_map_for_test(&tree, source);
3216        let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3217
3218        assert_eq!(contexts.len(), 1);
3219        assert_eq!(contexts[0].qualified_name, "helper");
3220        assert!(contexts[0].is_inline);
3221    }
3222
3223    #[test]
3224    fn test_extract_cpp_contexts_out_of_line_definition() {
3225        let source = r"
3226            namespace demo {
3227                class Service {
3228                public:
3229                    int process(int v);
3230                };
3231
3232                inline int Service::process(int v) {
3233                    return v;
3234                }
3235            }
3236        ";
3237        let tree = parse_cpp(source);
3238        let namespace_map = extract_namespace_map_for_test(&tree, source);
3239        let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3240
3241        // Only the definition should be captured (not the declaration)
3242        assert_eq!(contexts.len(), 1);
3243        assert_eq!(contexts[0].qualified_name, "demo::Service::process");
3244        assert!(contexts[0].is_inline);
3245    }
3246
3247    #[test]
3248    fn test_extract_field_types_simple() {
3249        let source = r"
3250            class Service {
3251            public:
3252                Repository repo;
3253            };
3254        ";
3255        let tree = parse_cpp(source);
3256        let namespace_map = extract_namespace_map_for_test(&tree, source);
3257        let (field_types, _type_map) =
3258            extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3259
3260        // Should have one field: Service.repo -> Repository
3261        assert_eq!(field_types.len(), 1);
3262        assert_eq!(
3263            field_types.get(&("Service".to_string(), "repo".to_string())),
3264            Some(&"Repository".to_string())
3265        );
3266    }
3267
3268    #[test]
3269    fn test_extract_field_types_namespace() {
3270        let source = r"
3271            namespace demo {
3272                class Service {
3273                public:
3274                    Repository repo;
3275                };
3276            }
3277        ";
3278        let tree = parse_cpp(source);
3279        let namespace_map = extract_namespace_map_for_test(&tree, source);
3280        let (field_types, _type_map) =
3281            extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3282
3283        // Should have one field with namespace-qualified class
3284        assert_eq!(field_types.len(), 1);
3285        assert_eq!(
3286            field_types.get(&("demo::Service".to_string(), "repo".to_string())),
3287            Some(&"Repository".to_string())
3288        );
3289    }
3290
3291    #[test]
3292    fn test_extract_field_types_no_collision() {
3293        let source = r"
3294            class ServiceA {
3295            public:
3296                Repository repo;
3297            };
3298
3299            class ServiceB {
3300            public:
3301                Repository repo;
3302            };
3303        ";
3304        let tree = parse_cpp(source);
3305        let namespace_map = extract_namespace_map_for_test(&tree, source);
3306        let (field_types, _type_map) =
3307            extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3308
3309        // Should have two distinct fields with no collision
3310        assert_eq!(field_types.len(), 2);
3311        assert_eq!(
3312            field_types.get(&("ServiceA".to_string(), "repo".to_string())),
3313            Some(&"Repository".to_string())
3314        );
3315        assert_eq!(
3316            field_types.get(&("ServiceB".to_string(), "repo".to_string())),
3317            Some(&"Repository".to_string())
3318        );
3319    }
3320
3321    #[test]
3322    fn test_extract_using_declaration() {
3323        let source = r"
3324            using std::vector;
3325
3326            class Service {
3327            public:
3328                vector data;
3329            };
3330        ";
3331        let tree = parse_cpp(source);
3332        let namespace_map = extract_namespace_map_for_test(&tree, source);
3333        let (field_types, type_map) =
3334            extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3335
3336        // Verify field extraction resolves type via using declaration
3337        assert_eq!(field_types.len(), 1);
3338        assert_eq!(
3339            field_types.get(&("Service".to_string(), "data".to_string())),
3340            Some(&"std::vector".to_string()),
3341            "Field type should resolve 'vector' to 'std::vector' via using declaration"
3342        );
3343
3344        // Verify that using declaration populated type_map
3345        assert_eq!(
3346            type_map.get(&(String::new(), "vector".to_string())),
3347            Some(&"std::vector".to_string()),
3348            "Using declaration should map 'vector' to 'std::vector' in type_map"
3349        );
3350    }
3351
3352    #[test]
3353    fn test_extract_field_types_pointer() {
3354        let source = r"
3355            class Service {
3356            public:
3357                Repository* repo;
3358            };
3359        ";
3360        let tree = parse_cpp(source);
3361        let namespace_map = extract_namespace_map_for_test(&tree, source);
3362        let (field_types, _type_map) =
3363            extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3364
3365        // Should extract field even for pointer types
3366        assert_eq!(field_types.len(), 1);
3367        assert_eq!(
3368            field_types.get(&("Service".to_string(), "repo".to_string())),
3369            Some(&"Repository".to_string())
3370        );
3371    }
3372
3373    #[test]
3374    fn test_extract_field_types_multiple_declarators() {
3375        let source = r"
3376            class Service {
3377            public:
3378                Repository repo_a, repo_b, repo_c;
3379            };
3380        ";
3381        let tree = parse_cpp(source);
3382        let namespace_map = extract_namespace_map_for_test(&tree, source);
3383        let (field_types, _type_map) =
3384            extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3385
3386        // Should extract all three fields
3387        assert_eq!(field_types.len(), 3);
3388        assert_eq!(
3389            field_types.get(&("Service".to_string(), "repo_a".to_string())),
3390            Some(&"Repository".to_string())
3391        );
3392        assert_eq!(
3393            field_types.get(&("Service".to_string(), "repo_b".to_string())),
3394            Some(&"Repository".to_string())
3395        );
3396        assert_eq!(
3397            field_types.get(&("Service".to_string(), "repo_c".to_string())),
3398            Some(&"Repository".to_string())
3399        );
3400    }
3401
3402    #[test]
3403    fn test_extract_field_types_nested_struct_with_parent_field() {
3404        // Regression test for nested class FQN building
3405        // Verifies that Inner gets "demo::Outer::Inner" not "demo::Inner"
3406        let source = r"
3407            namespace demo {
3408                struct Outer {
3409                    int outer_field;
3410                    struct Inner {
3411                        int inner_field;
3412                    };
3413                    Inner nested_instance;
3414                };
3415            }
3416        ";
3417        let tree = parse_cpp(source);
3418        let namespace_map = extract_namespace_map_for_test(&tree, source);
3419        let (field_types, _type_map) =
3420            extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3421
3422        // Should have fields from both Outer and Inner with properly qualified class FQNs
3423        // The critical assertion: Inner's field must use "demo::Outer::Inner", not "demo::Inner"
3424        assert!(
3425            field_types.len() >= 2,
3426            "Expected at least outer_field and nested_instance"
3427        );
3428
3429        // Outer's field
3430        assert_eq!(
3431            field_types.get(&("demo::Outer".to_string(), "outer_field".to_string())),
3432            Some(&"int".to_string())
3433        );
3434
3435        // Outer's nested instance field
3436        assert_eq!(
3437            field_types.get(&("demo::Outer".to_string(), "nested_instance".to_string())),
3438            Some(&"Inner".to_string())
3439        );
3440
3441        // If Inner's field is extracted, verify it uses the correct parent-qualified FQN
3442        if field_types.contains_key(&("demo::Outer::Inner".to_string(), "inner_field".to_string()))
3443        {
3444            // Great! The nested class field was extracted with correct FQN
3445            assert_eq!(
3446                field_types.get(&("demo::Outer::Inner".to_string(), "inner_field".to_string())),
3447                Some(&"int".to_string()),
3448                "Inner class fields must use parent-qualified FQN 'demo::Outer::Inner'"
3449            );
3450        }
3451    }
3452
3453    // ========================================================================
3454    // C2_OTHER_CPP — Property/Constant emission for class/struct fields
3455    // REQ:R0001, R0002, R0003, R0004, R0005, R0020, R0023
3456    // ========================================================================
3457    //
3458    // These tests assert the post-fix shape of `process_field_declaration`:
3459    //   - field qualified names use `Class.field` (last separator migrated to `.`
3460    //     per design §3.1.1; class qualifier still uses `::`)
3461    //   - non-`const`/`constexpr` fields → NodeKind::Property
3462    //   - `const` and `constexpr` fields → NodeKind::Constant
3463    //   - `static` keyword → is_static = true
3464    //   - visibility from enclosing access specifier; default `"private"`
3465    //     for class, `"public"` for struct
3466    //   - TypeOf edge emits TypeOfContext::Field with the bare field name
3467    //   - legacy `Class::field` qualified-name lookup returns 0 hits
3468
3469    use sqry_core::graph::unified::build::staging::StagingOp;
3470    use sqry_core::graph::unified::edge::kind::{EdgeKind, TypeOfContext};
3471
3472    /// Locate the staged `AddNode` entry by exact canonical (semantic) name.
3473    fn cpp_find_added_node<'a>(
3474        staging: &'a StagingGraph,
3475        canonical_name: &str,
3476    ) -> Option<&'a sqry_core::graph::unified::storage::arena::NodeEntry> {
3477        staging.operations().iter().find_map(|op| {
3478            if let StagingOp::AddNode { entry, .. } = op
3479                && staging.resolve_node_canonical_name(entry) == Some(canonical_name)
3480            {
3481                Some(entry)
3482            } else {
3483                None
3484            }
3485        })
3486    }
3487
3488    /// Locate the staged `AddNode` `NodeId` for a node by exact canonical name + kind.
3489    fn cpp_find_added_node_id(
3490        staging: &StagingGraph,
3491        canonical_name: &str,
3492        kind: NodeKind,
3493    ) -> Option<sqry_core::graph::unified::NodeId> {
3494        staging.operations().iter().find_map(|op| match op {
3495            StagingOp::AddNode {
3496                entry,
3497                expected_id: Some(id),
3498            } if entry.kind == kind
3499                && staging.resolve_node_canonical_name(entry) == Some(canonical_name) =>
3500            {
3501                Some(*id)
3502            }
3503            _ => None,
3504        })
3505    }
3506
3507    /// Build the unified graph for a C++ source snippet and return the staged graph.
3508    fn build_cpp(source: &str) -> StagingGraph {
3509        let tree = parse_cpp(source);
3510        let mut staging = StagingGraph::new();
3511        let builder = CppGraphBuilder::new();
3512        builder
3513            .build_graph(
3514                &tree,
3515                source.as_bytes(),
3516                Path::new("test.cpp"),
3517                &mut staging,
3518            )
3519            .expect("build_graph must succeed for the test fixture");
3520        staging
3521    }
3522
3523    /// AC-1 + AC-2 + AC-4 (struct default visibility) + AC-5:
3524    /// instance struct fields emit Property nodes with `Class.field`
3525    /// qualified-name shape, `is_static = false`, visibility = `"public"`
3526    /// (struct default), and a `TypeOf` edge using `TypeOfContext::Field` +
3527    /// the bare field name.
3528    #[test]
3529    fn test_struct_field_emits_property_with_field_context() {
3530        let source = "struct Point { int x; int y; };";
3531        let staging = build_cpp(source);
3532
3533        // AC-1: dotted Class.field qualified name.
3534        assert_has_node_with_kind_exact(&staging, "Point.x", NodeKind::Property);
3535        assert_has_node_with_kind_exact(&staging, "Point.y", NodeKind::Property);
3536
3537        let entry =
3538            cpp_find_added_node(&staging, "Point.x").expect("Point.x should be staged as a node");
3539        assert_eq!(entry.kind, NodeKind::Property, "x must be Property");
3540        assert!(!entry.is_static, "instance field is_static must be false");
3541        let vis = staging.resolve_local_string(entry.visibility.expect("visibility id"));
3542        assert_eq!(
3543            vis,
3544            Some("public"),
3545            "struct default visibility must be 'public'"
3546        );
3547        // `span_from_node` packs row/column into `Span::Position`; the helper
3548        // then stores them into `start_line`/`start_column`/`end_line`/
3549        // `end_column` on the entry (start_byte/end_byte are intentionally
3550        // not populated by `add_node_internal`). Assert the packed
3551        // line/column range is non-empty so we catch zero-width spans.
3552        assert!(entry.end_line > 0, "field end_line must be set (got 0)");
3553        assert!(
3554            entry.end_line > entry.start_line
3555                || (entry.end_line == entry.start_line && entry.end_column > entry.start_column),
3556            "field span must be non-empty: [{}:{}..{}:{}]",
3557            entry.start_line,
3558            entry.start_column,
3559            entry.end_line,
3560            entry.end_column,
3561        );
3562
3563        // AC-5: TypeOf edge with Field context + bare name "x".
3564        let x_id = cpp_find_added_node_id(&staging, "Point.x", NodeKind::Property)
3565            .expect("Point.x Property NodeId");
3566        let edge = staging.operations().iter().find_map(|op| {
3567            if let StagingOp::AddEdge {
3568                source: src,
3569                kind: EdgeKind::TypeOf { context, name, .. },
3570                ..
3571            } = op
3572                && *src == x_id
3573            {
3574                Some((*context, *name))
3575            } else {
3576                None
3577            }
3578        });
3579        let (ctx, name) = edge.expect("TypeOf edge from Point.x should be staged");
3580        assert_eq!(
3581            ctx,
3582            Some(TypeOfContext::Field),
3583            "TypeOf edge context must be Field"
3584        );
3585        let resolved_name = name.and_then(|sid| staging.resolve_local_string(sid));
3586        assert_eq!(
3587            resolved_name,
3588            Some("x"),
3589            "TypeOf edge name must be the bare field name 'x'"
3590        );
3591
3592        // AC-1 (negative): old NodeKind::Variable for these names must NOT appear.
3593        let stale_variable = staging.nodes().any(|n| {
3594            n.entry.kind == NodeKind::Variable
3595                && matches!(
3596                    staging.resolve_node_name(n.entry),
3597                    Some("Point.x" | "Point.y" | "Point::x" | "Point::y")
3598                )
3599        });
3600        assert!(
3601            !stale_variable,
3602            "Point fields must not be emitted as NodeKind::Variable"
3603        );
3604    }
3605
3606    /// AC-4: class default visibility is `"private"`.
3607    #[test]
3608    fn test_class_field_default_visibility_is_private() {
3609        let source = "class Foo { int hidden; };";
3610        let staging = build_cpp(source);
3611
3612        let entry = cpp_find_added_node(&staging, "Foo.hidden")
3613            .expect("Foo.hidden should be staged as a node");
3614        assert_eq!(entry.kind, NodeKind::Property);
3615        let vis = staging.resolve_local_string(entry.visibility.expect("visibility id"));
3616        assert_eq!(
3617            vis,
3618            Some("private"),
3619            "class default visibility must be 'private'"
3620        );
3621    }
3622
3623    /// AC-4: explicit access specifier overrides the default.
3624    #[test]
3625    fn test_class_field_respects_explicit_access_specifier() {
3626        let source = "class Foo { public: int public_field; protected: int prot_field; };";
3627        let staging = build_cpp(source);
3628
3629        let pub_entry = cpp_find_added_node(&staging, "Foo.public_field")
3630            .expect("Foo.public_field should be staged");
3631        assert_eq!(
3632            staging.resolve_local_string(pub_entry.visibility.expect("vis")),
3633            Some("public")
3634        );
3635
3636        let prot_entry = cpp_find_added_node(&staging, "Foo.prot_field")
3637            .expect("Foo.prot_field should be staged");
3638        assert_eq!(
3639            staging.resolve_local_string(prot_entry.visibility.expect("vis")),
3640            Some("protected")
3641        );
3642    }
3643
3644    /// AC-2 + AC-3: `const` field → Constant; instance const has
3645    /// `is_static = false` (no `static` keyword present).
3646    #[test]
3647    fn test_const_field_emits_constant() {
3648        let source = "class Foo { const int kMax = 0; };";
3649        let staging = build_cpp(source);
3650
3651        assert_has_node_with_kind_exact(&staging, "Foo.kMax", NodeKind::Constant);
3652        let entry = cpp_find_added_node(&staging, "Foo.kMax").expect("Foo.kMax");
3653        assert_eq!(entry.kind, NodeKind::Constant);
3654        assert!(
3655            !entry.is_static,
3656            "const (non-static) field is_static must be false; only `static` keyword sets is_static"
3657        );
3658    }
3659
3660    /// AC-2 + AC-3: `constexpr` field → Constant. The `static` flag is
3661    /// driven strictly by the `static` keyword (per design §3.4); a bare
3662    /// `constexpr` member without `static` must keep `is_static = false`.
3663    #[test]
3664    fn test_constexpr_field_emits_constant() {
3665        let source = "class Foo { constexpr static int kAnswer = 42; };";
3666        let staging = build_cpp(source);
3667
3668        assert_has_node_with_kind_exact(&staging, "Foo.kAnswer", NodeKind::Constant);
3669        let entry = cpp_find_added_node(&staging, "Foo.kAnswer").expect("Foo.kAnswer");
3670        assert_eq!(entry.kind, NodeKind::Constant);
3671        assert!(
3672            entry.is_static,
3673            "static constexpr member must have is_static = true"
3674        );
3675    }
3676
3677    /// AC-3: `static` keyword sets `is_static = true` on a Property
3678    /// (non-const non-constexpr).
3679    #[test]
3680    fn test_static_field_sets_is_static_true() {
3681        let source = "class Foo { static int counter; };";
3682        let staging = build_cpp(source);
3683
3684        let entry = cpp_find_added_node(&staging, "Foo.counter").expect("Foo.counter");
3685        assert_eq!(entry.kind, NodeKind::Property);
3686        assert!(entry.is_static, "static keyword must set is_static = true");
3687    }
3688
3689    /// AC-6: bit-fields (e.g., `int flags : 4;`) emit Property nodes with the
3690    /// usual `Class.field` form.
3691    #[test]
3692    fn test_bitfield_emits_property() {
3693        let source = "struct Flags { unsigned int low : 4; unsigned int high : 4; };";
3694        let staging = build_cpp(source);
3695
3696        assert_has_node_with_kind_exact(&staging, "Flags.low", NodeKind::Property);
3697        assert_has_node_with_kind_exact(&staging, "Flags.high", NodeKind::Property);
3698    }
3699
3700    /// AC-6: anonymous union — true anonymous unions (no instance name) inject
3701    /// their members into the enclosing class per C++ semantics. Members must
3702    /// emit as Property nodes under the OUTER class qualifier
3703    /// (`Variant.as_int`, `Variant.as_float`), NOT under any synthetic inner
3704    /// qualifier — there is no name to qualify by.
3705    #[test]
3706    fn test_anonymous_union_member_fields_emit_property() {
3707        let source = r"
3708class Variant {
3709public:
3710    int tag;
3711    union {
3712        int as_int;
3713        float as_float;
3714    };
3715};
3716";
3717        let staging = build_cpp(source);
3718
3719        // Outer named field is present with the dotted form.
3720        assert_has_node_with_kind_exact(&staging, "Variant.tag", NodeKind::Property);
3721
3722        // Anonymous-union members are injected into the enclosing class and
3723        // appear under `Variant.<member>` per C++ semantics (design AC-6).
3724        assert_has_node_with_kind_exact(&staging, "Variant.as_int", NodeKind::Property);
3725        assert_has_node_with_kind_exact(&staging, "Variant.as_float", NodeKind::Property);
3726
3727        // Visibility for injected members inherits the OUTER access state
3728        // (`public:` here).
3729        let as_int = cpp_find_added_node(&staging, "Variant.as_int")
3730            .expect("Variant.as_int should be staged");
3731        let vis = staging.resolve_local_string(as_int.visibility.expect("visibility id"));
3732        assert_eq!(
3733            vis,
3734            Some("public"),
3735            "anonymous-union members must inherit OUTER access (`public:` here)"
3736        );
3737
3738        // Negative: there must be no synthetic anonymous-union qualifier
3739        // such as `Variant::.as_int` or members under a bogus inner name.
3740        let bogus = staging.nodes().any(|n| {
3741            staging
3742                .resolve_node_name(n.entry)
3743                .is_some_and(|name| name.contains("::.") || name.starts_with("Variant::."))
3744        });
3745        assert!(
3746            !bogus,
3747            "anonymous union must not produce a synthetic qualifier"
3748        );
3749
3750        // No stale Variable emission for any of these names.
3751        let stale_variable = staging.nodes().any(|n| {
3752            n.entry.kind == NodeKind::Variable
3753                && matches!(
3754                    staging.resolve_node_name(n.entry),
3755                    Some("Variant.tag" | "Variant.as_int" | "Variant.as_float")
3756                )
3757        });
3758        assert!(
3759            !stale_variable,
3760            "anonymous-union members + outer fields must not stay as Variable"
3761        );
3762    }
3763
3764    /// AC-6: templated class — `template<class T> struct Box { T value; };`
3765    /// emits the field under the bare class name (template-args part is
3766    /// stripped for the qualified name; design §4.1 edge cases).
3767    #[test]
3768    fn test_templated_class_field_emits_property() {
3769        let source = r"
3770template<class T>
3771struct Box {
3772    T value;
3773};
3774";
3775        let staging = build_cpp(source);
3776
3777        assert_has_node_with_kind_exact(&staging, "Box.value", NodeKind::Property);
3778        let entry = cpp_find_added_node(&staging, "Box.value").expect("Box.value");
3779        assert_eq!(entry.kind, NodeKind::Property);
3780        assert!(!entry.is_static);
3781    }
3782
3783    /// AC-6: nested class — both the OUTER field (`Outer.outer_value`) and the
3784    /// INNER nested-class fields (`Outer::Inner.x`) must emit as Property
3785    /// nodes. `walk_class_body` recurses into a nested
3786    /// `field_declaration > class_specifier` and extends the qualifier chain
3787    /// with the inner-class name (design AC-6 + §4.1).
3788    #[test]
3789    fn test_outer_class_field_with_nested_class_present() {
3790        let source = r"
3791class Outer {
3792public:
3793    int outer_value;
3794    class Inner {
3795    public:
3796        int x;
3797    };
3798};
3799";
3800        let staging = build_cpp(source);
3801
3802        // AC-6: outer field is emitted under the dotted form.
3803        assert_has_node_with_kind_exact(&staging, "Outer.outer_value", NodeKind::Property);
3804
3805        // AC-6: nested-class field emits under the parent-qualified dotted
3806        // form `Outer::Inner.x` (class chain stays `::`, last separator
3807        // migrates to `.` per design §3.1.1).
3808        assert_has_node_with_kind_exact(&staging, "Outer::Inner.x", NodeKind::Property);
3809
3810        // Negative legacy lookup: the legacy `Outer::outer_value` form must
3811        // not appear (AC-7 + design §3.1.1).
3812        let legacy_hits: Vec<_> = staging
3813            .nodes()
3814            .filter(|n| staging.resolve_node_name(n.entry) == Some("Outer::outer_value"))
3815            .collect();
3816        assert!(
3817            legacy_hits.is_empty(),
3818            "legacy `Outer::outer_value` lookup must return 0 hits"
3819        );
3820
3821        // Negative: nested field must not appear under bare `Inner.x` (lost
3822        // outer chain) or legacy `Outer::Inner::x` (last separator missed
3823        // migration).
3824        for legacy in ["Inner.x", "Outer::Inner::x", "Outer.Inner.x"] {
3825            let hits: Vec<_> = staging
3826                .nodes()
3827                .filter(|n| staging.resolve_node_name(n.entry) == Some(legacy))
3828                .collect();
3829            assert!(
3830                hits.is_empty(),
3831                "nested-class field `{legacy}` must not appear; expected only `Outer::Inner.x`"
3832            );
3833        }
3834    }
3835
3836    /// AC-6: nested struct inside a class — nested struct fields qualify as
3837    /// `Outer::Inner.y`. Default struct visibility is `public`, regardless
3838    /// of the OUTER access state.
3839    #[test]
3840    fn test_outer_class_with_nested_struct_emits_inner_field() {
3841        let source = r"
3842class Outer {
3843private:
3844    struct Inner {
3845        int y;
3846    };
3847};
3848";
3849        let staging = build_cpp(source);
3850
3851        assert_has_node_with_kind_exact(&staging, "Outer::Inner.y", NodeKind::Property);
3852
3853        let entry = cpp_find_added_node(&staging, "Outer::Inner.y")
3854            .expect("Outer::Inner.y should be staged");
3855        let vis = staging.resolve_local_string(entry.visibility.expect("visibility id"));
3856        assert_eq!(
3857            vis,
3858            Some("public"),
3859            "nested struct field default visibility must be 'public' \
3860             regardless of OUTER access state"
3861        );
3862    }
3863
3864    /// Staging-level smoke: post-fix, no staged node for a class field uses
3865    /// the legacy `Class::field` qualified-name shape. This is a
3866    /// fast-feedback companion to the AC-7 contract test — the authoritative
3867    /// AC-7 assertion runs against a finalized `GraphSnapshot` via
3868    /// `find_nodes_by_name` in
3869    /// `tests/integration_tests.rs::test_legacy_double_colon_field_lookup_returns_zero_via_snapshot`
3870    /// (design §4.1).
3871    #[test]
3872    fn test_legacy_double_colon_field_lookup_returns_zero() {
3873        let source = r"
3874class Foo {
3875public:
3876    int bar;
3877    static int baz;
3878    const int qux = 0;
3879};
3880struct Quux {
3881    int corge;
3882};
3883";
3884        let staging = build_cpp(source);
3885
3886        // Positive: dotted form must be present for every field.
3887        assert_has_node_with_kind_exact(&staging, "Foo.bar", NodeKind::Property);
3888        assert_has_node_with_kind_exact(&staging, "Foo.baz", NodeKind::Property);
3889        assert_has_node_with_kind_exact(&staging, "Foo.qux", NodeKind::Constant);
3890        assert_has_node_with_kind_exact(&staging, "Quux.corge", NodeKind::Property);
3891
3892        // Negative: legacy `Class::field` qualified name must not appear for
3893        // any of the fields in the fixture.
3894        for legacy in ["Foo::bar", "Foo::baz", "Foo::qux", "Quux::corge"] {
3895            let hits: Vec<_> = staging
3896                .nodes()
3897                .filter(|n| staging.resolve_node_name(n.entry) == Some(legacy))
3898                .collect();
3899            assert!(
3900                hits.is_empty(),
3901                "legacy lookup for {legacy:?} must return 0 hits, got {} node(s) ({:?})",
3902                hits.len(),
3903                hits.iter()
3904                    .map(|n| (n.entry.kind, staging.resolve_node_name(n.entry)))
3905                    .collect::<Vec<_>>()
3906            );
3907        }
3908    }
3909
3910    /// Field inside a class that lives in a namespace must keep the namespace
3911    /// chain joined by `::` and only flip the LAST separator to `.`.
3912    #[test]
3913    fn test_namespaced_class_field_qualified_name() {
3914        let source = r"
3915namespace demo {
3916    class Service {
3917    public:
3918        int counter;
3919    };
3920}
3921";
3922        let staging = build_cpp(source);
3923
3924        assert_has_node_with_kind_exact(&staging, "demo::Service.counter", NodeKind::Property);
3925    }
3926}