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                            if kind == "enum_specifier" {
1656                                // Nested enums carry the enclosing access
1657                                // visibility, identical to the nested
1658                                // class/struct path below.
1659                                helper.add_enum_with_visibility(
1660                                    &nested_qualified,
1661                                    Some(nested_span),
1662                                    Some(current_visibility),
1663                                );
1664                            } else {
1665                                let nested_id = if is_struct_or_union {
1666                                    helper.add_struct_with_visibility(
1667                                        &nested_qualified,
1668                                        Some(nested_span),
1669                                        Some(current_visibility),
1670                                    )
1671                                } else {
1672                                    helper.add_class_with_visibility(
1673                                        &nested_qualified,
1674                                        Some(nested_span),
1675                                        Some(current_visibility),
1676                                    )
1677                                };
1678                                build_inheritance_and_implements_edges(
1679                                    inner,
1680                                    content,
1681                                    &nested_qualified,
1682                                    nested_id,
1683                                    helper,
1684                                    namespace_stack,
1685                                    pure_virtual_registry,
1686                                )?;
1687                            }
1688
1689                            // Recurse into the body for members. Enums carry no
1690                            // field members we model, so only class/struct/union
1691                            // bodies are walked. Emitting the node above already
1692                            // marks the declaration handled.
1693                            if matches!(
1694                                kind,
1695                                "class_specifier" | "struct_specifier" | "union_specifier"
1696                            ) && let Some(body) = inner.child_by_field_name("body")
1697                            {
1698                                walk_class_body(
1699                                    body,
1700                                    content,
1701                                    &nested_qualified,
1702                                    is_struct_or_union,
1703                                    ast_graph,
1704                                    helper,
1705                                    seen_includes,
1706                                    namespace_stack,
1707                                    class_stack,
1708                                    ffi_registry,
1709                                    pure_virtual_registry,
1710                                    budget,
1711                                )?;
1712                            }
1713                            handled_nested = true;
1714                        }
1715                    } else if let Some(body) = inner.child_by_field_name("body") {
1716                        // ANONYMOUS nested type: inject members into enclosing
1717                        // class. Process direct field_declaration children
1718                        // with OUTER qualifier + OUTER visibility so members
1719                        // surface as `Outer.member`.
1720                        let mut anon_cursor = body.walk();
1721                        for anon_child in body.children(&mut anon_cursor) {
1722                            if anon_child.kind() == "field_declaration" {
1723                                process_field_declaration(
1724                                    anon_child,
1725                                    content,
1726                                    class_qualified_name,
1727                                    current_visibility,
1728                                    helper,
1729                                )?;
1730                            }
1731                        }
1732                        handled_nested = true;
1733                    }
1734                }
1735
1736                // Process the field_declaration itself unless we exclusively
1737                // handled it as a pure nested type with no instance declarator
1738                // (e.g. `class Inner { ... };` has a class_specifier but no
1739                // field_identifier). `process_field_declaration` is harmless
1740                // when no `field_identifier` / declarator child exists — it
1741                // collects an empty `field_names` list and falls through.
1742                // We still call it so cases that mix a nested type with an
1743                // instance declarator (`class Inner { } member;`) keep
1744                // emitting the `Outer.member` Property too. When
1745                // `handled_nested` is true and the type child is absent of
1746                // declarator children, the function is effectively a no-op
1747                // (no field name → no node).
1748                let _ = handled_nested;
1749                process_field_declaration(
1750                    child,
1751                    content,
1752                    class_qualified_name,
1753                    current_visibility,
1754                    helper,
1755                )?;
1756            }
1757            "function_definition" => {
1758                // Process method with current visibility
1759                // Extract function context from AST graph by matching start position
1760                if let Some(context) = ast_graph.context_for_start(child.start_byte()) {
1761                    let span = span_from_node(child);
1762                    helper.add_method_with_signature(
1763                        &context.qualified_name,
1764                        Some(span),
1765                        false, // C++ doesn't have async
1766                        context.is_static,
1767                        Some(current_visibility),
1768                        context.return_type.as_deref(),
1769                    );
1770                }
1771                // Recurse into function body to process call expressions
1772                walk_tree_for_graph(
1773                    child,
1774                    content,
1775                    ast_graph,
1776                    helper,
1777                    seen_includes,
1778                    namespace_stack,
1779                    class_stack,
1780                    ffi_registry,
1781                    pure_virtual_registry,
1782                    budget,
1783                )?;
1784            }
1785            _ => {
1786                // Recurse into other nodes (nested classes, etc.)
1787                walk_tree_for_graph(
1788                    child,
1789                    content,
1790                    ast_graph,
1791                    helper,
1792                    seen_includes,
1793                    namespace_stack,
1794                    class_stack,
1795                    ffi_registry,
1796                    pure_virtual_registry,
1797                    budget,
1798                )?;
1799            }
1800        }
1801    }
1802
1803    Ok(())
1804}
1805
1806/// Walk the tree and populate the staging graph.
1807#[allow(clippy::too_many_arguments)]
1808#[allow(clippy::too_many_lines)] // Central traversal; refactor after C++ AST stabilizes.
1809fn walk_tree_for_graph(
1810    node: Node,
1811    content: &[u8],
1812    ast_graph: &ASTGraph,
1813    helper: &mut GraphBuildHelper,
1814    seen_includes: &mut HashSet<String>,
1815    namespace_stack: &mut Vec<String>,
1816    class_stack: &mut Vec<String>,
1817    ffi_registry: &FfiRegistry,
1818    pure_virtual_registry: &PureVirtualRegistry,
1819    budget: &mut BuildBudget,
1820) -> GraphResult<()> {
1821    budget.checkpoint("cpp:walk_tree_for_graph")?;
1822    match node.kind() {
1823        "preproc_include" => {
1824            // Handle #include directives - create Import edges
1825            build_import_edge(node, content, helper, seen_includes)?;
1826        }
1827        "linkage_specification" => {
1828            // Handle extern "C" blocks - create FFI function nodes
1829            build_ffi_block_for_staging(node, content, helper, namespace_stack);
1830        }
1831        "namespace_definition" => {
1832            // Extract namespace name and track context
1833            if let Some(name_node) = node.child_by_field_name("name")
1834                && let Ok(ns_name) = name_node.utf8_text(content)
1835            {
1836                namespace_stack.push(ns_name.trim().to_string());
1837
1838                // Recurse into namespace body
1839                let mut cursor = node.walk();
1840                for child in node.children(&mut cursor) {
1841                    walk_tree_for_graph(
1842                        child,
1843                        content,
1844                        ast_graph,
1845                        helper,
1846                        seen_includes,
1847                        namespace_stack,
1848                        class_stack,
1849                        ffi_registry,
1850                        pure_virtual_registry,
1851                        budget,
1852                    )?;
1853                }
1854
1855                namespace_stack.pop();
1856                return Ok(());
1857            }
1858        }
1859        "class_specifier" | "struct_specifier" | "union_specifier" => {
1860            // Extract class/struct/union name
1861            if let Some(name_node) = node.child_by_field_name("name")
1862                && let Ok(class_name) = name_node.utf8_text(content)
1863            {
1864                let class_name = class_name.trim();
1865                let span = span_from_node(node);
1866                // Unions have no dedicated NodeKind variant; they map to Struct,
1867                // matching the nested-type handling in `walk_class_body`.
1868                let is_struct = matches!(node.kind(), "struct_specifier" | "union_specifier");
1869
1870                // Build qualified class name
1871                let qualified_class =
1872                    build_qualified_name(namespace_stack, class_stack, class_name);
1873
1874                // Add class/struct node with qualified name
1875                let visibility = "public";
1876                let class_id = if is_struct {
1877                    helper.add_struct_with_visibility(
1878                        &qualified_class,
1879                        Some(span),
1880                        Some(visibility),
1881                    )
1882                } else {
1883                    helper.add_class_with_visibility(&qualified_class, Some(span), Some(visibility))
1884                };
1885
1886                // Handle inheritance with qualified name
1887                // Also check for Implements edges (inheriting from pure virtual interfaces)
1888                build_inheritance_and_implements_edges(
1889                    node,
1890                    content,
1891                    &qualified_class,
1892                    class_id,
1893                    helper,
1894                    namespace_stack,
1895                    pure_virtual_registry,
1896                )?;
1897
1898                // Export classes/structs at file/namespace scope (not nested classes)
1899                // Nested classes have internal linkage unless explicitly exported
1900                if class_stack.is_empty() {
1901                    let module_id = helper.add_module(FILE_MODULE_NAME, None);
1902                    helper.add_export_edge(module_id, class_id);
1903                }
1904
1905                // Track class context for nested classes
1906                class_stack.push(class_name.to_string());
1907
1908                // Process class body with visibility tracking
1909                // Default visibility: struct = public, class = private
1910                if let Some(body) = node.child_by_field_name("body") {
1911                    walk_class_body(
1912                        body,
1913                        content,
1914                        &qualified_class,
1915                        is_struct,
1916                        ast_graph,
1917                        helper,
1918                        seen_includes,
1919                        namespace_stack,
1920                        class_stack,
1921                        ffi_registry,
1922                        pure_virtual_registry,
1923                        budget,
1924                    )?;
1925                }
1926
1927                class_stack.pop();
1928                return Ok(());
1929            }
1930        }
1931        "enum_specifier" => {
1932            if let Some(name_node) = node.child_by_field_name("name")
1933                && let Ok(enum_name) = name_node.utf8_text(content)
1934            {
1935                let enum_name = enum_name.trim();
1936                let span = span_from_node(node);
1937                let qualified_enum = build_qualified_name(namespace_stack, class_stack, enum_name);
1938                let enum_id = helper.add_enum(&qualified_enum, Some(span));
1939
1940                if class_stack.is_empty() {
1941                    let module_id = helper.add_module(FILE_MODULE_NAME, None);
1942                    helper.add_export_edge(module_id, enum_id);
1943                }
1944            }
1945        }
1946        "function_definition" => {
1947            // Skip if we're inside a class body - methods are handled by walk_class_body
1948            // to ensure correct visibility tracking. This check prevents double-adding
1949            // methods with incorrect visibility.
1950            if !class_stack.is_empty() {
1951                // Don't process the function definition as a node here, but do recurse
1952                // into its body to find call expressions
1953                let mut cursor = node.walk();
1954                for child in node.children(&mut cursor) {
1955                    walk_tree_for_graph(
1956                        child,
1957                        content,
1958                        ast_graph,
1959                        helper,
1960                        seen_includes,
1961                        namespace_stack,
1962                        class_stack,
1963                        ffi_registry,
1964                        pure_virtual_registry,
1965                        budget,
1966                    )?;
1967                }
1968                return Ok(());
1969            }
1970
1971            // Extract function context from AST graph by matching start position
1972            if let Some(context) = ast_graph.context_for_start(node.start_byte()) {
1973                let span = span_from_node(node);
1974
1975                // Determine if this is a method or free function based on context
1976                if context.class_stack.is_empty() {
1977                    // This is a free function
1978                    // Visibility: static = private (internal linkage), non-static = public (external linkage)
1979                    let visibility = if context.is_static {
1980                        "private"
1981                    } else {
1982                        "public"
1983                    };
1984                    let fn_id = helper.add_function_with_signature(
1985                        &context.qualified_name,
1986                        Some(span),
1987                        false, // C++ doesn't have async
1988                        false, // C++ doesn't use unsafe keyword
1989                        Some(visibility),
1990                        context.return_type.as_deref(),
1991                    );
1992
1993                    // Export non-static free functions (static functions have internal linkage)
1994                    if !context.is_static {
1995                        let module_id = helper.add_module(FILE_MODULE_NAME, None);
1996                        helper.add_export_edge(module_id, fn_id);
1997                    }
1998                } else {
1999                    // This is an out-of-class method definition (e.g., Resource::Resource())
2000                    // These are public by default in C++ (they must be declared in the class first)
2001                    // Note: We can't determine actual visibility here as that requires
2002                    // correlating with the in-class declaration
2003                    helper.add_method_with_signature(
2004                        &context.qualified_name,
2005                        Some(span),
2006                        false, // C++ doesn't have async
2007                        context.is_static,
2008                        Some("public"), // Default for out-of-class definitions
2009                        context.return_type.as_deref(),
2010                    );
2011                }
2012            }
2013        }
2014        "call_expression" => {
2015            // Build call edge
2016            if let Ok(Some((caller_qname, callee_qname, argument_count, span))) =
2017                build_call_for_staging(ast_graph, node, content)
2018            {
2019                // Ensure caller node exists
2020                let caller_function_id =
2021                    helper.ensure_callee(&caller_qname, span, CalleeKindHint::Function);
2022                let argument_count = u8::try_from(argument_count).unwrap_or(u8::MAX);
2023
2024                // Check if the callee is a known FFI function
2025                // Only do FFI lookup for unqualified calls (no ::)
2026                let is_unqualified = !callee_qname.contains("::");
2027                if is_unqualified {
2028                    if let Some((ffi_qualified, ffi_convention)) = ffi_registry.get(&callee_qname) {
2029                        // This is a call to an FFI function - create FfiCall edge
2030                        let ffi_target_id =
2031                            helper.ensure_callee(ffi_qualified, span, CalleeKindHint::Function);
2032                        helper.add_ffi_edge(caller_function_id, ffi_target_id, *ffi_convention);
2033                    } else {
2034                        // Regular call - create normal Call edge
2035                        let target_function_id =
2036                            helper.ensure_callee(&callee_qname, span, CalleeKindHint::Function);
2037                        helper.add_call_edge_full_with_span(
2038                            caller_function_id,
2039                            target_function_id,
2040                            argument_count,
2041                            false,
2042                            vec![span],
2043                        );
2044                    }
2045                } else {
2046                    // Qualified call - create normal Call edge
2047                    let target_function_id =
2048                        helper.ensure_callee(&callee_qname, span, CalleeKindHint::Function);
2049                    helper.add_call_edge_full_with_span(
2050                        caller_function_id,
2051                        target_function_id,
2052                        argument_count,
2053                        false,
2054                        vec![span],
2055                    );
2056                }
2057            }
2058        }
2059        "declaration" => {
2060            // Handle global/file-level variable declarations (not inside classes)
2061            // Only process if we're not inside a class (class members are handled in walk_class_body)
2062            if class_stack.is_empty() {
2063                process_global_variable_declaration(node, content, namespace_stack, helper)?;
2064            }
2065        }
2066        _ => {}
2067    }
2068
2069    // Recurse into children
2070    let mut cursor = node.walk();
2071    for child in node.children(&mut cursor) {
2072        walk_tree_for_graph(
2073            child,
2074            content,
2075            ast_graph,
2076            helper,
2077            seen_includes,
2078            namespace_stack,
2079            class_stack,
2080            ffi_registry,
2081            pure_virtual_registry,
2082            budget,
2083        )?;
2084    }
2085
2086    Ok(())
2087}
2088
2089/// Build call edge information for the staging graph.
2090fn build_call_for_staging(
2091    ast_graph: &ASTGraph,
2092    call_node: Node<'_>,
2093    content: &[u8],
2094) -> GraphResult<Option<(String, String, usize, Span)>> {
2095    // Find the enclosing function context
2096    let call_context = ast_graph.find_enclosing(call_node.start_byte());
2097    let caller_qualified_name = if let Some(ctx) = call_context {
2098        ctx.qualified_name.clone()
2099    } else {
2100        // Top-level call (e.g., global initializer)
2101        return Ok(None);
2102    };
2103
2104    let Some(function_node) = call_node.child_by_field_name("function") else {
2105        return Ok(None);
2106    };
2107
2108    let callee_text = function_node
2109        .utf8_text(content)
2110        .map_err(|_| GraphBuilderError::ParseError {
2111            span: span_from_node(call_node),
2112            reason: "failed to read call expression".to_string(),
2113        })?
2114        .trim();
2115
2116    if callee_text.is_empty() {
2117        return Ok(None);
2118    }
2119
2120    // Resolve callee name using context
2121    let target_qualified_name = if let Some(ctx) = call_context {
2122        resolve_callee_name(callee_text, ctx, ast_graph)
2123    } else {
2124        callee_text.to_string()
2125    };
2126
2127    let span = span_from_node(call_node);
2128    let argument_count = count_arguments(call_node);
2129
2130    Ok(Some((
2131        caller_qualified_name,
2132        target_qualified_name,
2133        argument_count,
2134        span,
2135    )))
2136}
2137
2138/// Build import edge for `#include` directives.
2139///
2140/// Handles both system includes (`<header>`) and local includes (`"header"`).
2141/// Per the implementation plan, include type (system/local) is tracked via
2142/// node metadata, not the edge's alias field (alias is for import renaming only).
2143/// Duplicate includes are deduplicated using the `seen_includes` set.
2144fn build_import_edge(
2145    include_node: Node<'_>,
2146    content: &[u8],
2147    helper: &mut GraphBuildHelper,
2148    seen_includes: &mut HashSet<String>,
2149) -> GraphResult<()> {
2150    // Look for path child (system_lib_string or string_literal)
2151    let path_node = include_node.child_by_field_name("path").or_else(|| {
2152        // Fallback: find first child that looks like a path
2153        let mut cursor = include_node.walk();
2154        include_node.children(&mut cursor).find(|child| {
2155            matches!(
2156                child.kind(),
2157                "system_lib_string" | "string_literal" | "string_content"
2158            )
2159        })
2160    });
2161
2162    let Some(path_node) = path_node else {
2163        return Ok(());
2164    };
2165
2166    let include_path = path_node
2167        .utf8_text(content)
2168        .map_err(|_| GraphBuilderError::ParseError {
2169            span: span_from_node(include_node),
2170            reason: "failed to read include path".to_string(),
2171        })?
2172        .trim();
2173
2174    if include_path.is_empty() {
2175        return Ok(());
2176    }
2177
2178    // Determine include type and clean up path
2179    let is_system_include = include_path.starts_with('<') && include_path.ends_with('>');
2180    let cleaned_path = if is_system_include {
2181        // System include: <iostream> -> iostream
2182        include_path.trim_start_matches('<').trim_end_matches('>')
2183    } else {
2184        // Local include: "myheader.hpp" -> myheader.hpp
2185        include_path.trim_start_matches('"').trim_end_matches('"')
2186    };
2187
2188    if cleaned_path.is_empty() {
2189        return Ok(());
2190    }
2191
2192    // Deduplicate includes - only add if not seen before
2193    if !seen_includes.insert(cleaned_path.to_string()) {
2194        return Ok(()); // Already seen this include
2195    }
2196
2197    // Create module node for the file being compiled (importer)
2198    let file_module_id = helper.add_module("<file>", None);
2199
2200    // Create import node for the included header
2201    let span = span_from_node(include_node);
2202    let import_id = helper.add_import(cleaned_path, Some(span));
2203
2204    // Add import edge - no alias for #include (alias is for renaming, which C++ doesn't support)
2205    // is_wildcard is false since #include brings in the whole header (but it's not a wildcard import)
2206    helper.add_import_edge(file_module_id, import_id);
2207
2208    Ok(())
2209}
2210
2211// ================================
2212// FFI Support Functions
2213// ================================
2214
2215/// Collect FFI declarations from extern "C" blocks (Pass 1).
2216///
2217/// This function walks the entire AST to find all `extern "C" { ... }` blocks
2218/// and populates the FFI registry with function name → (qualified name, convention)
2219/// mappings. This must be done before processing calls so that FFI calls can be
2220/// properly linked regardless of source code order.
2221fn collect_ffi_declarations(
2222    node: Node<'_>,
2223    content: &[u8],
2224    ffi_registry: &mut FfiRegistry,
2225    budget: &mut BuildBudget,
2226) -> GraphResult<()> {
2227    budget.checkpoint("cpp:collect_ffi_declarations")?;
2228    if node.kind() == "linkage_specification" {
2229        // Get the ABI string (e.g., "C")
2230        let abi = extract_ffi_abi(node, content);
2231        let convention = abi_to_convention(&abi);
2232
2233        // Find the body child (declaration_list or single declaration)
2234        if let Some(body_node) = node.child_by_field_name("body") {
2235            collect_ffi_from_body(body_node, content, &abi, convention, ffi_registry);
2236        }
2237    }
2238
2239    // Recurse into children
2240    let mut cursor = node.walk();
2241    for child in node.children(&mut cursor) {
2242        collect_ffi_declarations(child, content, ffi_registry, budget)?;
2243    }
2244
2245    Ok(())
2246}
2247
2248/// Collect FFI declarations from a linkage specification body.
2249fn collect_ffi_from_body(
2250    body_node: Node<'_>,
2251    content: &[u8],
2252    abi: &str,
2253    convention: FfiConvention,
2254    ffi_registry: &mut FfiRegistry,
2255) {
2256    match body_node.kind() {
2257        "declaration_list" => {
2258            // Multiple declarations in the block
2259            let mut cursor = body_node.walk();
2260            for decl in body_node.children(&mut cursor) {
2261                if decl.kind() == "declaration"
2262                    && let Some(fn_name) = extract_ffi_function_name(decl, content)
2263                {
2264                    let qualified = format!("extern::{abi}::{fn_name}");
2265                    ffi_registry.insert(fn_name, (qualified, convention));
2266                }
2267            }
2268        }
2269        "declaration" => {
2270            // Single declaration (e.g., extern "C" void foo();)
2271            if let Some(fn_name) = extract_ffi_function_name(body_node, content) {
2272                let qualified = format!("extern::{abi}::{fn_name}");
2273                ffi_registry.insert(fn_name, (qualified, convention));
2274            }
2275        }
2276        _ => {}
2277    }
2278}
2279
2280/// Extract function name from an FFI declaration.
2281fn extract_ffi_function_name(decl_node: Node<'_>, content: &[u8]) -> Option<String> {
2282    // Look for declarator field which contains the function declarator
2283    if let Some(declarator_node) = decl_node.child_by_field_name("declarator") {
2284        return extract_function_name_from_declarator(declarator_node, content);
2285    }
2286    None
2287}
2288
2289/// Recursively extract function name from a declarator node.
2290fn extract_function_name_from_declarator(node: Node<'_>, content: &[u8]) -> Option<String> {
2291    match node.kind() {
2292        "function_declarator" => {
2293            // Function declarator has a nested declarator with the name
2294            if let Some(inner) = node.child_by_field_name("declarator") {
2295                return extract_function_name_from_declarator(inner, content);
2296            }
2297        }
2298        "identifier" => {
2299            // Found the name
2300            if let Ok(name) = node.utf8_text(content) {
2301                let name = name.trim();
2302                if !name.is_empty() {
2303                    return Some(name.to_string());
2304                }
2305            }
2306        }
2307        "pointer_declarator" | "reference_declarator" => {
2308            // Handle pointer/reference declarators (e.g., int* (*foo)())
2309            if let Some(inner) = node.child_by_field_name("declarator") {
2310                return extract_function_name_from_declarator(inner, content);
2311            }
2312        }
2313        "parenthesized_declarator" => {
2314            // Handle parenthesized declarators
2315            let mut cursor = node.walk();
2316            for child in node.children(&mut cursor) {
2317                if let Some(name) = extract_function_name_from_declarator(child, content) {
2318                    return Some(name);
2319                }
2320            }
2321        }
2322        _ => {}
2323    }
2324    None
2325}
2326
2327/// Extract the ABI string from an extern "X" block.
2328///
2329/// Returns the ABI string (e.g., "C") or "C" as default.
2330fn extract_ffi_abi(node: Node<'_>, content: &[u8]) -> String {
2331    // Look for the "value" field which contains the string literal
2332    if let Some(value_node) = node.child_by_field_name("value")
2333        && value_node.kind() == "string_literal"
2334    {
2335        // Look for string_content child
2336        let mut cursor = value_node.walk();
2337        for child in value_node.children(&mut cursor) {
2338            if child.kind() == "string_content"
2339                && let Ok(text) = child.utf8_text(content)
2340            {
2341                let trimmed = text.trim();
2342                if !trimmed.is_empty() {
2343                    return trimmed.to_string();
2344                }
2345            }
2346        }
2347    }
2348    // Default to "C" if no ABI specified
2349    "C".to_string()
2350}
2351
2352/// Convert an ABI string to an FFI calling convention.
2353fn abi_to_convention(abi: &str) -> FfiConvention {
2354    match abi.to_lowercase().as_str() {
2355        "system" => FfiConvention::System,
2356        "stdcall" => FfiConvention::Stdcall,
2357        "fastcall" => FfiConvention::Fastcall,
2358        "cdecl" => FfiConvention::Cdecl,
2359        _ => FfiConvention::C, // Default to C
2360    }
2361}
2362
2363/// Build FFI function declarations from extern "C" blocks.
2364///
2365/// Creates Function nodes for FFI declarations with unsafe=true.
2366fn build_ffi_block_for_staging(
2367    node: Node<'_>,
2368    content: &[u8],
2369    helper: &mut GraphBuildHelper,
2370    namespace_stack: &[String],
2371) {
2372    // Get the ABI string
2373    let abi = extract_ffi_abi(node, content);
2374
2375    // Find the body child
2376    if let Some(body_node) = node.child_by_field_name("body") {
2377        build_ffi_from_body(body_node, content, &abi, helper, namespace_stack);
2378    }
2379}
2380
2381/// Build FFI function nodes from a linkage specification body.
2382fn build_ffi_from_body(
2383    body_node: Node<'_>,
2384    content: &[u8],
2385    abi: &str,
2386    helper: &mut GraphBuildHelper,
2387    namespace_stack: &[String],
2388) {
2389    match body_node.kind() {
2390        "declaration_list" => {
2391            // Multiple declarations in the block
2392            let mut cursor = body_node.walk();
2393            for decl in body_node.children(&mut cursor) {
2394                if decl.kind() == "declaration"
2395                    && let Some(fn_name) = extract_ffi_function_name(decl, content)
2396                {
2397                    let span = span_from_node(decl);
2398                    // Build qualified name with namespace context
2399                    let qualified = if namespace_stack.is_empty() {
2400                        format!("extern::{abi}::{fn_name}")
2401                    } else {
2402                        format!("{}::extern::{abi}::{fn_name}", namespace_stack.join("::"))
2403                    };
2404                    // Add as unsafe function (FFI functions are inherently unsafe)
2405                    helper.add_function(
2406                        &qualified,
2407                        Some(span),
2408                        false, // not async
2409                        true,  // unsafe (FFI)
2410                    );
2411                }
2412            }
2413        }
2414        "declaration" => {
2415            // Single declaration
2416            if let Some(fn_name) = extract_ffi_function_name(body_node, content) {
2417                let span = span_from_node(body_node);
2418                let qualified = if namespace_stack.is_empty() {
2419                    format!("extern::{abi}::{fn_name}")
2420                } else {
2421                    format!("{}::extern::{abi}::{fn_name}", namespace_stack.join("::"))
2422                };
2423                helper.add_function(&qualified, Some(span), false, true);
2424            }
2425        }
2426        _ => {}
2427    }
2428}
2429
2430// ================================
2431// Pure Virtual Interface Support
2432// ================================
2433
2434/// Collect pure virtual interfaces (abstract classes with pure virtual methods).
2435///
2436/// A class is considered a "pure virtual interface" if it contains at least one
2437/// pure virtual method (declared with `= 0`). Classes that inherit from such
2438/// interfaces will get Implements edges instead of just Inherits edges.
2439fn collect_pure_virtual_interfaces(
2440    node: Node<'_>,
2441    content: &[u8],
2442    registry: &mut PureVirtualRegistry,
2443    budget: &mut BuildBudget,
2444) -> GraphResult<()> {
2445    budget.checkpoint("cpp:collect_pure_virtual_interfaces")?;
2446    if matches!(node.kind(), "class_specifier" | "struct_specifier")
2447        && let Some(name_node) = node.child_by_field_name("name")
2448        && let Ok(class_name) = name_node.utf8_text(content)
2449    {
2450        let class_name = class_name.trim();
2451        if !class_name.is_empty() && has_pure_virtual_methods(node, content) {
2452            registry.insert(class_name.to_string());
2453        }
2454    }
2455
2456    // Recurse into children
2457    let mut cursor = node.walk();
2458    for child in node.children(&mut cursor) {
2459        collect_pure_virtual_interfaces(child, content, registry, budget)?;
2460    }
2461
2462    Ok(())
2463}
2464
2465/// Check if a class/struct has any pure virtual methods.
2466///
2467/// Pure virtual methods are declared as `virtual ReturnType name() = 0;`
2468fn has_pure_virtual_methods(class_node: Node<'_>, content: &[u8]) -> bool {
2469    if let Some(body) = class_node.child_by_field_name("body") {
2470        let mut cursor = body.walk();
2471        for child in body.children(&mut cursor) {
2472            // Look for field_declaration with virtual and = 0
2473            if child.kind() == "field_declaration" && is_pure_virtual_declaration(child, content) {
2474                return true;
2475            }
2476        }
2477    }
2478    false
2479}
2480
2481/// Check if a field declaration is a pure virtual method (has `virtual` and `= 0`).
2482fn is_pure_virtual_declaration(decl_node: Node<'_>, content: &[u8]) -> bool {
2483    let mut has_virtual = false;
2484    let mut has_pure_specifier = false;
2485
2486    // Check children for virtual keyword and default_value of 0
2487    let mut cursor = decl_node.walk();
2488    for child in decl_node.children(&mut cursor) {
2489        match child.kind() {
2490            "virtual" => {
2491                has_virtual = true;
2492            }
2493            "number_literal" => {
2494                // Check if this is the pure virtual specifier (= 0)
2495                // The number_literal with value "0" after "=" indicates a pure virtual method
2496                if let Ok(text) = child.utf8_text(content)
2497                    && text.trim() == "0"
2498                {
2499                    has_pure_specifier = true;
2500                }
2501            }
2502            _ => {}
2503        }
2504    }
2505
2506    has_virtual && has_pure_specifier
2507}
2508
2509/// Build inheritance and implements edges for a class/struct.
2510///
2511/// For each base class:
2512/// - If the base class is a pure virtual interface, create an Implements edge
2513/// - Otherwise, create an Inherits edge
2514fn build_inheritance_and_implements_edges(
2515    class_node: Node<'_>,
2516    content: &[u8],
2517    _qualified_class_name: &str,
2518    child_id: sqry_core::graph::unified::node::NodeId,
2519    helper: &mut GraphBuildHelper,
2520    namespace_stack: &[String],
2521    pure_virtual_registry: &PureVirtualRegistry,
2522) -> GraphResult<()> {
2523    // Look for base_class_clause child
2524    let mut cursor = class_node.walk();
2525    let base_clause = class_node
2526        .children(&mut cursor)
2527        .find(|child| child.kind() == "base_class_clause");
2528
2529    let Some(base_clause) = base_clause else {
2530        return Ok(()); // No inheritance
2531    };
2532
2533    // Parse all base classes from the base_class_clause
2534    let mut clause_cursor = base_clause.walk();
2535    for child in base_clause.children(&mut clause_cursor) {
2536        match child.kind() {
2537            "type_identifier" => {
2538                let base_name = child
2539                    .utf8_text(content)
2540                    .map_err(|_| GraphBuilderError::ParseError {
2541                        span: span_from_node(child),
2542                        reason: "failed to read base class name".to_string(),
2543                    })?
2544                    .trim();
2545
2546                if !base_name.is_empty() {
2547                    // Qualify with namespace if present
2548                    let qualified_base = if namespace_stack.is_empty() {
2549                        base_name.to_string()
2550                    } else {
2551                        format!("{}::{}", namespace_stack.join("::"), base_name)
2552                    };
2553
2554                    // Check if base is a pure virtual interface
2555                    if pure_virtual_registry.contains(base_name) {
2556                        // Create interface node and Implements edge
2557                        let interface_id = helper.add_interface(&qualified_base, None);
2558                        helper.add_implements_edge(child_id, interface_id);
2559                    } else {
2560                        // Regular inheritance - create Inherits edge
2561                        let parent_id = helper.add_class(&qualified_base, None);
2562                        helper.add_inherits_edge(child_id, parent_id);
2563                    }
2564                }
2565            }
2566            "qualified_identifier" => {
2567                // Already qualified - use as-is
2568                let base_name = child
2569                    .utf8_text(content)
2570                    .map_err(|_| GraphBuilderError::ParseError {
2571                        span: span_from_node(child),
2572                        reason: "failed to read base class name".to_string(),
2573                    })?
2574                    .trim();
2575
2576                if !base_name.is_empty() {
2577                    // Extract simple name for registry lookup
2578                    let simple_name = base_name.rsplit("::").next().unwrap_or(base_name);
2579
2580                    if pure_virtual_registry.contains(simple_name) {
2581                        let interface_id = helper.add_interface(base_name, None);
2582                        helper.add_implements_edge(child_id, interface_id);
2583                    } else {
2584                        let parent_id = helper.add_class(base_name, None);
2585                        helper.add_inherits_edge(child_id, parent_id);
2586                    }
2587                }
2588            }
2589            "template_type" => {
2590                // Template base class: Base<T>
2591                if let Some(template_name_node) = child.child_by_field_name("name")
2592                    && let Ok(base_name) = template_name_node.utf8_text(content)
2593                {
2594                    let base_name = base_name.trim();
2595                    if !base_name.is_empty() {
2596                        let qualified_base =
2597                            if base_name.contains("::") || namespace_stack.is_empty() {
2598                                base_name.to_string()
2599                            } else {
2600                                format!("{}::{}", namespace_stack.join("::"), base_name)
2601                            };
2602
2603                        // Template bases are typically not pure virtual interfaces
2604                        // but check anyway
2605                        if pure_virtual_registry.contains(base_name) {
2606                            let interface_id = helper.add_interface(&qualified_base, None);
2607                            helper.add_implements_edge(child_id, interface_id);
2608                        } else {
2609                            let parent_id = helper.add_class(&qualified_base, None);
2610                            helper.add_inherits_edge(child_id, parent_id);
2611                        }
2612                    }
2613                }
2614            }
2615            _ => {
2616                // Skip access specifiers, colons, commas, and other non-base nodes.
2617            }
2618        }
2619    }
2620
2621    Ok(())
2622}
2623
2624fn span_from_node(node: Node<'_>) -> Span {
2625    let start = node.start_position();
2626    let end = node.end_position();
2627    Span::new(
2628        sqry_core::graph::node::Position::new(start.row, start.column),
2629        sqry_core::graph::node::Position::new(end.row, end.column),
2630    )
2631}
2632
2633fn count_arguments(node: Node<'_>) -> usize {
2634    node.child_by_field_name("arguments").map_or(0, |args| {
2635        let mut count = 0;
2636        let mut cursor = args.walk();
2637        for child in args.children(&mut cursor) {
2638            if !matches!(child.kind(), "(" | ")" | ",") {
2639                count += 1;
2640            }
2641        }
2642        count
2643    })
2644}
2645
2646#[cfg(test)]
2647mod tests {
2648    use super::*;
2649    use sqry_core::graph::unified::build::test_helpers::{
2650        assert_has_node, assert_has_node_with_kind, assert_has_node_with_kind_exact,
2651        collect_call_edges,
2652    };
2653    use sqry_core::graph::unified::node::NodeKind;
2654    use tree_sitter::Parser;
2655
2656    fn parse_cpp(source: &str) -> Tree {
2657        let mut parser = Parser::new();
2658        parser
2659            .set_language(&tree_sitter_cpp::LANGUAGE.into())
2660            .expect("Failed to set Cpp language");
2661        parser
2662            .parse(source.as_bytes(), None)
2663            .expect("Failed to parse Cpp source")
2664    }
2665
2666    fn test_budget() -> BuildBudget {
2667        BuildBudget::new(Path::new("test.cpp"))
2668    }
2669
2670    fn extract_namespace_map_for_test(
2671        tree: &Tree,
2672        source: &str,
2673    ) -> HashMap<std::ops::Range<usize>, String> {
2674        let mut budget = test_budget();
2675        extract_namespace_map(tree.root_node(), source.as_bytes(), &mut budget)
2676            .expect("namespace extraction should succeed in tests")
2677    }
2678
2679    fn extract_cpp_contexts_for_test(
2680        tree: &Tree,
2681        source: &str,
2682        namespace_map: &HashMap<std::ops::Range<usize>, String>,
2683    ) -> Vec<FunctionContext> {
2684        let mut budget = test_budget();
2685        extract_cpp_contexts(
2686            tree.root_node(),
2687            source.as_bytes(),
2688            namespace_map,
2689            &mut budget,
2690        )
2691        .expect("context extraction should succeed in tests")
2692    }
2693
2694    fn extract_field_and_type_info_for_test(
2695        tree: &Tree,
2696        source: &str,
2697        namespace_map: &HashMap<std::ops::Range<usize>, String>,
2698    ) -> (QualifiedNameMap, QualifiedNameMap) {
2699        let mut budget = test_budget();
2700        extract_field_and_type_info(
2701            tree.root_node(),
2702            source.as_bytes(),
2703            namespace_map,
2704            &mut budget,
2705        )
2706        .expect("field/type extraction should succeed in tests")
2707    }
2708
2709    #[test]
2710    fn test_build_graph_times_out_with_expired_budget() {
2711        let source = r"
2712            namespace demo {
2713                class Service {
2714                public:
2715                    void process() {}
2716                };
2717            }
2718        ";
2719        let tree = parse_cpp(source);
2720        let builder = CppGraphBuilder::new();
2721        let mut staging = StagingGraph::new();
2722        let mut budget = BuildBudget::already_expired(Path::new("timeout.cpp"));
2723
2724        let err = builder
2725            .build_graph_with_budget(
2726                &tree,
2727                source.as_bytes(),
2728                Path::new("timeout.cpp"),
2729                &mut staging,
2730                &mut budget,
2731            )
2732            .expect_err("expired budget should force timeout");
2733
2734        match err {
2735            GraphBuilderError::BuildTimedOut {
2736                file,
2737                phase,
2738                timeout_ms,
2739            } => {
2740                assert_eq!(file, PathBuf::from("timeout.cpp"));
2741                assert_eq!(phase, "cpp:extract_namespace_map");
2742                assert_eq!(timeout_ms, 1_000);
2743            }
2744            other => panic!("expected BuildTimedOut, got {other:?}"),
2745        }
2746    }
2747
2748    #[test]
2749    fn test_extract_class() {
2750        let source = "class User { }";
2751        let tree = parse_cpp(source);
2752        let mut staging = StagingGraph::new();
2753        let builder = CppGraphBuilder::new();
2754
2755        let result = builder.build_graph(
2756            &tree,
2757            source.as_bytes(),
2758            Path::new("test.cpp"),
2759            &mut staging,
2760        );
2761
2762        assert!(result.is_ok());
2763        assert_has_node_with_kind(&staging, "User", NodeKind::Class);
2764    }
2765
2766    #[test]
2767    fn test_extract_template_class() {
2768        let source = r"
2769            template <typename T>
2770            class Person {
2771            public:
2772                T name;
2773                T age;
2774            };
2775        ";
2776        let tree = parse_cpp(source);
2777        let mut staging = StagingGraph::new();
2778        let builder = CppGraphBuilder::new();
2779
2780        let result = builder.build_graph(
2781            &tree,
2782            source.as_bytes(),
2783            Path::new("test.cpp"),
2784            &mut staging,
2785        );
2786
2787        assert!(result.is_ok());
2788        assert_has_node_with_kind(&staging, "Person", NodeKind::Class);
2789    }
2790
2791    #[test]
2792    fn test_nested_named_types_emit_nodes() {
2793        // Regression: nested class/struct/union/enum declared inside a class body
2794        // must each emit their OWN type node (previously only their members were
2795        // staged, so `kind:class` / `kind:struct` / `kind:enum` could not see
2796        // them). Covers doubly-nested chains and namespace-nested chains.
2797        let source = r"
2798            class Outer {
2799            public:
2800                class Inner { int z; };
2801                struct InnerS { int w; };
2802                union InnerU { int i; float f; };
2803                enum class InnerE { A, B };
2804                class L1 { public: class L2 { int q; }; };
2805            };
2806            namespace ns {
2807                class NsOuter { public: class NsInner { int n; }; };
2808            }
2809        ";
2810        let staging = build_cpp(source);
2811
2812        // Each nested type emits a node with the `Outer::Inner` qualified shape.
2813        assert_has_node_with_kind_exact(&staging, "Outer::Inner", NodeKind::Class);
2814        assert_has_node_with_kind_exact(&staging, "Outer::InnerS", NodeKind::Struct);
2815        // Unions map to NodeKind::Struct (no dedicated Union variant).
2816        assert_has_node_with_kind_exact(&staging, "Outer::InnerU", NodeKind::Struct);
2817        assert_has_node_with_kind_exact(&staging, "Outer::InnerE", NodeKind::Enum);
2818        // Doubly nested: `Outer::L1` and `Outer::L1::L2`.
2819        assert_has_node_with_kind_exact(&staging, "Outer::L1", NodeKind::Class);
2820        assert_has_node_with_kind_exact(&staging, "Outer::L1::L2", NodeKind::Class);
2821        // Nested inside a namespaced class.
2822        assert_has_node_with_kind_exact(&staging, "ns::NsOuter", NodeKind::Class);
2823        assert_has_node_with_kind_exact(&staging, "ns::NsOuter::NsInner", NodeKind::Class);
2824
2825        // Members still qualify under the nested chain (regression guard: the
2826        // member-walk behaviour that already worked must be preserved).
2827        assert_has_node_with_kind_exact(&staging, "Outer::Inner.z", NodeKind::Property);
2828        assert_has_node_with_kind_exact(&staging, "Outer::L1::L2.q", NodeKind::Property);
2829        assert_has_node_with_kind_exact(&staging, "ns::NsOuter::NsInner.n", NodeKind::Property);
2830    }
2831
2832    #[test]
2833    fn test_nested_enum_carries_enclosing_visibility() {
2834        // Nested enums must carry the enclosing access visibility, identical to
2835        // the nested class/struct path — not an absent visibility. A nested enum
2836        // under `private:` is `private`; under `public:` is `public`.
2837        let source = r"
2838            class Outer {
2839            private:
2840                enum class Secret { A, B };
2841            public:
2842                enum class Pub { X, Y };
2843            };
2844        ";
2845        let staging = build_cpp(source);
2846
2847        let secret = cpp_find_added_node(&staging, "Outer::Secret")
2848            .expect("nested enum Outer::Secret must be staged");
2849        assert_eq!(secret.kind, NodeKind::Enum, "Secret must be an Enum node");
2850        let secret_vis = staging.resolve_local_string(
2851            secret
2852                .visibility
2853                .expect("nested enum must carry a visibility id"),
2854        );
2855        assert_eq!(
2856            secret_vis,
2857            Some("private"),
2858            "nested enum under `private:` must be private"
2859        );
2860
2861        let pub_enum = cpp_find_added_node(&staging, "Outer::Pub")
2862            .expect("nested enum Outer::Pub must be staged");
2863        let pub_vis = staging.resolve_local_string(
2864            pub_enum
2865                .visibility
2866                .expect("nested enum must carry a visibility id"),
2867        );
2868        assert_eq!(
2869            pub_vis,
2870            Some("public"),
2871            "nested enum under `public:` must be public"
2872        );
2873    }
2874
2875    #[test]
2876    fn test_nested_class_emits_inheritance_edge() {
2877        // Regression: a nested class with a base clause must emit an `Inherits`
2878        // edge anchored on the nested class node (previously the nested type was
2879        // never registered, so its lineage edge was lost entirely).
2880        let source = r"
2881            struct Base { virtual ~Base(); };
2882            class Outer {
2883            public:
2884                class Derived : public Base {};
2885            };
2886        ";
2887        let staging = build_cpp(source);
2888
2889        let derived_id = cpp_find_added_node_id(&staging, "Outer::Derived", NodeKind::Class)
2890            .expect("nested Derived class node must be staged");
2891
2892        let has_inherits = staging.operations().iter().any(|op| {
2893            matches!(
2894                op,
2895                StagingOp::AddEdge {
2896                    source: src,
2897                    kind: EdgeKind::Inherits,
2898                    ..
2899                } if *src == derived_id
2900            )
2901        });
2902        assert!(
2903            has_inherits,
2904            "nested Derived must emit an Inherits edge to its base"
2905        );
2906    }
2907
2908    #[test]
2909    fn test_top_level_union_emits_struct_node() {
2910        // Regression: top-level `union` declarations previously produced no node
2911        // (only `class_specifier` / `struct_specifier` were matched). Unions map
2912        // to NodeKind::Struct.
2913        let source = "union Value { int i; float f; };";
2914        let staging = build_cpp(source);
2915        assert_has_node_with_kind_exact(&staging, "Value", NodeKind::Struct);
2916    }
2917
2918    #[test]
2919    fn test_extract_function() {
2920        let source = r#"
2921            #include <cstdio>
2922            void hello() {
2923                std::printf("Hello");
2924            }
2925        "#;
2926        let tree = parse_cpp(source);
2927        let mut staging = StagingGraph::new();
2928        let builder = CppGraphBuilder::new();
2929
2930        let result = builder.build_graph(
2931            &tree,
2932            source.as_bytes(),
2933            Path::new("test.cpp"),
2934            &mut staging,
2935        );
2936
2937        assert!(result.is_ok());
2938        assert_has_node_with_kind(&staging, "hello", NodeKind::Function);
2939    }
2940
2941    #[test]
2942    fn test_extract_virtual_function() {
2943        let source = r"
2944            class Service {
2945            public:
2946                virtual void fetchData() {}
2947            };
2948        ";
2949        let tree = parse_cpp(source);
2950        let mut staging = StagingGraph::new();
2951        let builder = CppGraphBuilder::new();
2952
2953        let result = builder.build_graph(
2954            &tree,
2955            source.as_bytes(),
2956            Path::new("test.cpp"),
2957            &mut staging,
2958        );
2959
2960        assert!(result.is_ok());
2961        assert_has_node(&staging, "fetchData");
2962    }
2963
2964    #[test]
2965    fn test_extract_call_edge() {
2966        let source = r"
2967            void greet() {}
2968
2969            int main() {
2970                greet();
2971                return 0;
2972            }
2973        ";
2974        let tree = parse_cpp(source);
2975        let mut staging = StagingGraph::new();
2976        let builder = CppGraphBuilder::new();
2977
2978        let result = builder.build_graph(
2979            &tree,
2980            source.as_bytes(),
2981            Path::new("test.cpp"),
2982            &mut staging,
2983        );
2984
2985        assert!(result.is_ok());
2986        assert_has_node(&staging, "main");
2987        assert_has_node(&staging, "greet");
2988        let calls = collect_call_edges(&staging);
2989        assert!(!calls.is_empty());
2990    }
2991
2992    #[test]
2993    fn test_extract_member_call_edge() {
2994        let source = r"
2995            class Service {
2996            public:
2997                void helper() {}
2998            };
2999
3000            int main() {
3001                Service svc;
3002                svc.helper();
3003                return 0;
3004            }
3005        ";
3006        let tree = parse_cpp(source);
3007        let mut staging = StagingGraph::new();
3008        let builder = CppGraphBuilder::new();
3009
3010        let result = builder.build_graph(
3011            &tree,
3012            source.as_bytes(),
3013            Path::new("member.cpp"),
3014            &mut staging,
3015        );
3016
3017        assert!(result.is_ok());
3018        assert_has_node(&staging, "main");
3019        assert_has_node(&staging, "helper");
3020        let calls = collect_call_edges(&staging);
3021        assert!(!calls.is_empty());
3022    }
3023
3024    #[test]
3025    fn test_extract_namespace_map_simple() {
3026        let source = r"
3027            namespace demo {
3028                void func() {}
3029            }
3030        ";
3031        let tree = parse_cpp(source);
3032        let namespace_map = extract_namespace_map_for_test(&tree, source);
3033
3034        // Should have one entry mapping the namespace body to "demo::"
3035        assert_eq!(namespace_map.len(), 1);
3036
3037        // Find any namespace entry (we only have one)
3038        let (_, ns_prefix) = namespace_map.iter().next().unwrap();
3039        assert_eq!(ns_prefix, "demo::");
3040    }
3041
3042    #[test]
3043    fn test_extract_namespace_map_nested() {
3044        let source = r"
3045            namespace outer {
3046                namespace inner {
3047                    void func() {}
3048                }
3049            }
3050        ";
3051        let tree = parse_cpp(source);
3052        let namespace_map = extract_namespace_map_for_test(&tree, source);
3053
3054        // Should have entries for both outer and inner namespaces
3055        assert!(namespace_map.len() >= 2);
3056
3057        // Check that we have the expected namespace prefixes
3058        let ns_values: Vec<&String> = namespace_map.values().collect();
3059        assert!(ns_values.iter().any(|v| v.as_str() == "outer::"));
3060        assert!(ns_values.iter().any(|v| v.as_str() == "outer::inner::"));
3061    }
3062
3063    #[test]
3064    fn test_extract_namespace_map_multiple() {
3065        let source = r"
3066            namespace first {
3067                void func1() {}
3068            }
3069            namespace second {
3070                void func2() {}
3071            }
3072        ";
3073        let tree = parse_cpp(source);
3074        let namespace_map = extract_namespace_map_for_test(&tree, source);
3075
3076        // Should have entries for both namespaces
3077        assert_eq!(namespace_map.len(), 2);
3078
3079        let ns_values: Vec<&String> = namespace_map.values().collect();
3080        assert!(ns_values.iter().any(|v| v.as_str() == "first::"));
3081        assert!(ns_values.iter().any(|v| v.as_str() == "second::"));
3082    }
3083
3084    #[test]
3085    fn test_find_namespace_for_offset() {
3086        let source = r"
3087            namespace demo {
3088                void func() {}
3089            }
3090        ";
3091        let tree = parse_cpp(source);
3092        let namespace_map = extract_namespace_map_for_test(&tree, source);
3093
3094        // Find the byte offset of "func" (should be inside demo namespace)
3095        let func_offset = source.find("func").unwrap();
3096        let ns = find_namespace_for_offset(func_offset, &namespace_map);
3097        assert_eq!(ns, "demo::");
3098
3099        // Byte offset before namespace should return empty string
3100        let ns = find_namespace_for_offset(0, &namespace_map);
3101        assert_eq!(ns, "");
3102    }
3103
3104    #[test]
3105    fn test_extract_cpp_contexts_free_function() {
3106        let source = r"
3107            void helper() {}
3108        ";
3109        let tree = parse_cpp(source);
3110        let namespace_map = extract_namespace_map_for_test(&tree, source);
3111        let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3112
3113        assert_eq!(contexts.len(), 1);
3114        assert_eq!(contexts[0].qualified_name, "helper");
3115        assert!(!contexts[0].is_static);
3116        assert!(!contexts[0].is_virtual);
3117    }
3118
3119    #[test]
3120    fn test_extract_cpp_contexts_namespace_function() {
3121        let source = r"
3122            namespace demo {
3123                void helper() {}
3124            }
3125        ";
3126        let tree = parse_cpp(source);
3127        let namespace_map = extract_namespace_map_for_test(&tree, source);
3128        let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3129
3130        assert_eq!(contexts.len(), 1);
3131        assert_eq!(contexts[0].qualified_name, "demo::helper");
3132        assert_eq!(contexts[0].namespace_stack, vec!["demo"]);
3133    }
3134
3135    #[test]
3136    fn test_extract_cpp_contexts_class_method() {
3137        let source = r"
3138            class Service {
3139            public:
3140                void process() {}
3141            };
3142        ";
3143        let tree = parse_cpp(source);
3144        let namespace_map = extract_namespace_map_for_test(&tree, source);
3145        let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3146
3147        assert_eq!(contexts.len(), 1);
3148        assert_eq!(contexts[0].qualified_name, "Service::process");
3149        assert_eq!(contexts[0].class_stack, vec!["Service"]);
3150    }
3151
3152    #[test]
3153    fn test_extract_cpp_contexts_namespace_and_class() {
3154        let source = r"
3155            namespace demo {
3156                class Service {
3157                public:
3158                    void process() {}
3159                };
3160            }
3161        ";
3162        let tree = parse_cpp(source);
3163        let namespace_map = extract_namespace_map_for_test(&tree, source);
3164        let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3165
3166        assert_eq!(contexts.len(), 1);
3167        assert_eq!(contexts[0].qualified_name, "demo::Service::process");
3168        assert_eq!(contexts[0].namespace_stack, vec!["demo"]);
3169        assert_eq!(contexts[0].class_stack, vec!["Service"]);
3170    }
3171
3172    #[test]
3173    fn test_extract_cpp_contexts_static_method() {
3174        let source = r"
3175            class Repository {
3176            public:
3177                static void save() {}
3178            };
3179        ";
3180        let tree = parse_cpp(source);
3181        let namespace_map = extract_namespace_map_for_test(&tree, source);
3182        let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3183
3184        assert_eq!(contexts.len(), 1);
3185        assert_eq!(contexts[0].qualified_name, "Repository::save");
3186        assert!(contexts[0].is_static);
3187    }
3188
3189    #[test]
3190    fn test_extract_cpp_contexts_virtual_method() {
3191        let source = r"
3192            class Base {
3193            public:
3194                virtual void render() {}
3195            };
3196        ";
3197        let tree = parse_cpp(source);
3198        let namespace_map = extract_namespace_map_for_test(&tree, source);
3199        let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3200
3201        assert_eq!(contexts.len(), 1);
3202        assert_eq!(contexts[0].qualified_name, "Base::render");
3203        assert!(contexts[0].is_virtual);
3204    }
3205
3206    #[test]
3207    fn test_extract_cpp_contexts_inline_function() {
3208        let source = r"
3209            inline void helper() {}
3210        ";
3211        let tree = parse_cpp(source);
3212        let namespace_map = extract_namespace_map_for_test(&tree, source);
3213        let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3214
3215        assert_eq!(contexts.len(), 1);
3216        assert_eq!(contexts[0].qualified_name, "helper");
3217        assert!(contexts[0].is_inline);
3218    }
3219
3220    #[test]
3221    fn test_extract_cpp_contexts_out_of_line_definition() {
3222        let source = r"
3223            namespace demo {
3224                class Service {
3225                public:
3226                    int process(int v);
3227                };
3228
3229                inline int Service::process(int v) {
3230                    return v;
3231                }
3232            }
3233        ";
3234        let tree = parse_cpp(source);
3235        let namespace_map = extract_namespace_map_for_test(&tree, source);
3236        let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3237
3238        // Only the definition should be captured (not the declaration)
3239        assert_eq!(contexts.len(), 1);
3240        assert_eq!(contexts[0].qualified_name, "demo::Service::process");
3241        assert!(contexts[0].is_inline);
3242    }
3243
3244    #[test]
3245    fn test_extract_field_types_simple() {
3246        let source = r"
3247            class Service {
3248            public:
3249                Repository repo;
3250            };
3251        ";
3252        let tree = parse_cpp(source);
3253        let namespace_map = extract_namespace_map_for_test(&tree, source);
3254        let (field_types, _type_map) =
3255            extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3256
3257        // Should have one field: Service.repo -> Repository
3258        assert_eq!(field_types.len(), 1);
3259        assert_eq!(
3260            field_types.get(&("Service".to_string(), "repo".to_string())),
3261            Some(&"Repository".to_string())
3262        );
3263    }
3264
3265    #[test]
3266    fn test_extract_field_types_namespace() {
3267        let source = r"
3268            namespace demo {
3269                class Service {
3270                public:
3271                    Repository repo;
3272                };
3273            }
3274        ";
3275        let tree = parse_cpp(source);
3276        let namespace_map = extract_namespace_map_for_test(&tree, source);
3277        let (field_types, _type_map) =
3278            extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3279
3280        // Should have one field with namespace-qualified class
3281        assert_eq!(field_types.len(), 1);
3282        assert_eq!(
3283            field_types.get(&("demo::Service".to_string(), "repo".to_string())),
3284            Some(&"Repository".to_string())
3285        );
3286    }
3287
3288    #[test]
3289    fn test_extract_field_types_no_collision() {
3290        let source = r"
3291            class ServiceA {
3292            public:
3293                Repository repo;
3294            };
3295
3296            class ServiceB {
3297            public:
3298                Repository repo;
3299            };
3300        ";
3301        let tree = parse_cpp(source);
3302        let namespace_map = extract_namespace_map_for_test(&tree, source);
3303        let (field_types, _type_map) =
3304            extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3305
3306        // Should have two distinct fields with no collision
3307        assert_eq!(field_types.len(), 2);
3308        assert_eq!(
3309            field_types.get(&("ServiceA".to_string(), "repo".to_string())),
3310            Some(&"Repository".to_string())
3311        );
3312        assert_eq!(
3313            field_types.get(&("ServiceB".to_string(), "repo".to_string())),
3314            Some(&"Repository".to_string())
3315        );
3316    }
3317
3318    #[test]
3319    fn test_extract_using_declaration() {
3320        let source = r"
3321            using std::vector;
3322
3323            class Service {
3324            public:
3325                vector data;
3326            };
3327        ";
3328        let tree = parse_cpp(source);
3329        let namespace_map = extract_namespace_map_for_test(&tree, source);
3330        let (field_types, type_map) =
3331            extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3332
3333        // Verify field extraction resolves type via using declaration
3334        assert_eq!(field_types.len(), 1);
3335        assert_eq!(
3336            field_types.get(&("Service".to_string(), "data".to_string())),
3337            Some(&"std::vector".to_string()),
3338            "Field type should resolve 'vector' to 'std::vector' via using declaration"
3339        );
3340
3341        // Verify that using declaration populated type_map
3342        assert_eq!(
3343            type_map.get(&(String::new(), "vector".to_string())),
3344            Some(&"std::vector".to_string()),
3345            "Using declaration should map 'vector' to 'std::vector' in type_map"
3346        );
3347    }
3348
3349    #[test]
3350    fn test_extract_field_types_pointer() {
3351        let source = r"
3352            class Service {
3353            public:
3354                Repository* repo;
3355            };
3356        ";
3357        let tree = parse_cpp(source);
3358        let namespace_map = extract_namespace_map_for_test(&tree, source);
3359        let (field_types, _type_map) =
3360            extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3361
3362        // Should extract field even for pointer types
3363        assert_eq!(field_types.len(), 1);
3364        assert_eq!(
3365            field_types.get(&("Service".to_string(), "repo".to_string())),
3366            Some(&"Repository".to_string())
3367        );
3368    }
3369
3370    #[test]
3371    fn test_extract_field_types_multiple_declarators() {
3372        let source = r"
3373            class Service {
3374            public:
3375                Repository repo_a, repo_b, repo_c;
3376            };
3377        ";
3378        let tree = parse_cpp(source);
3379        let namespace_map = extract_namespace_map_for_test(&tree, source);
3380        let (field_types, _type_map) =
3381            extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3382
3383        // Should extract all three fields
3384        assert_eq!(field_types.len(), 3);
3385        assert_eq!(
3386            field_types.get(&("Service".to_string(), "repo_a".to_string())),
3387            Some(&"Repository".to_string())
3388        );
3389        assert_eq!(
3390            field_types.get(&("Service".to_string(), "repo_b".to_string())),
3391            Some(&"Repository".to_string())
3392        );
3393        assert_eq!(
3394            field_types.get(&("Service".to_string(), "repo_c".to_string())),
3395            Some(&"Repository".to_string())
3396        );
3397    }
3398
3399    #[test]
3400    fn test_extract_field_types_nested_struct_with_parent_field() {
3401        // Regression test for nested class FQN building
3402        // Verifies that Inner gets "demo::Outer::Inner" not "demo::Inner"
3403        let source = r"
3404            namespace demo {
3405                struct Outer {
3406                    int outer_field;
3407                    struct Inner {
3408                        int inner_field;
3409                    };
3410                    Inner nested_instance;
3411                };
3412            }
3413        ";
3414        let tree = parse_cpp(source);
3415        let namespace_map = extract_namespace_map_for_test(&tree, source);
3416        let (field_types, _type_map) =
3417            extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3418
3419        // Should have fields from both Outer and Inner with properly qualified class FQNs
3420        // The critical assertion: Inner's field must use "demo::Outer::Inner", not "demo::Inner"
3421        assert!(
3422            field_types.len() >= 2,
3423            "Expected at least outer_field and nested_instance"
3424        );
3425
3426        // Outer's field
3427        assert_eq!(
3428            field_types.get(&("demo::Outer".to_string(), "outer_field".to_string())),
3429            Some(&"int".to_string())
3430        );
3431
3432        // Outer's nested instance field
3433        assert_eq!(
3434            field_types.get(&("demo::Outer".to_string(), "nested_instance".to_string())),
3435            Some(&"Inner".to_string())
3436        );
3437
3438        // If Inner's field is extracted, verify it uses the correct parent-qualified FQN
3439        if field_types.contains_key(&("demo::Outer::Inner".to_string(), "inner_field".to_string()))
3440        {
3441            // Great! The nested class field was extracted with correct FQN
3442            assert_eq!(
3443                field_types.get(&("demo::Outer::Inner".to_string(), "inner_field".to_string())),
3444                Some(&"int".to_string()),
3445                "Inner class fields must use parent-qualified FQN 'demo::Outer::Inner'"
3446            );
3447        }
3448    }
3449
3450    // ========================================================================
3451    // C2_OTHER_CPP — Property/Constant emission for class/struct fields
3452    // REQ:R0001, R0002, R0003, R0004, R0005, R0020, R0023
3453    // ========================================================================
3454    //
3455    // These tests assert the post-fix shape of `process_field_declaration`:
3456    //   - field qualified names use `Class.field` (last separator migrated to `.`
3457    //     per design §3.1.1; class qualifier still uses `::`)
3458    //   - non-`const`/`constexpr` fields → NodeKind::Property
3459    //   - `const` and `constexpr` fields → NodeKind::Constant
3460    //   - `static` keyword → is_static = true
3461    //   - visibility from enclosing access specifier; default `"private"`
3462    //     for class, `"public"` for struct
3463    //   - TypeOf edge emits TypeOfContext::Field with the bare field name
3464    //   - legacy `Class::field` qualified-name lookup returns 0 hits
3465
3466    use sqry_core::graph::unified::build::staging::StagingOp;
3467    use sqry_core::graph::unified::edge::kind::{EdgeKind, TypeOfContext};
3468
3469    /// Locate the staged `AddNode` entry by exact canonical (semantic) name.
3470    fn cpp_find_added_node<'a>(
3471        staging: &'a StagingGraph,
3472        canonical_name: &str,
3473    ) -> Option<&'a sqry_core::graph::unified::storage::arena::NodeEntry> {
3474        staging.operations().iter().find_map(|op| {
3475            if let StagingOp::AddNode { entry, .. } = op
3476                && staging.resolve_node_canonical_name(entry) == Some(canonical_name)
3477            {
3478                Some(entry)
3479            } else {
3480                None
3481            }
3482        })
3483    }
3484
3485    /// Locate the staged `AddNode` `NodeId` for a node by exact canonical name + kind.
3486    fn cpp_find_added_node_id(
3487        staging: &StagingGraph,
3488        canonical_name: &str,
3489        kind: NodeKind,
3490    ) -> Option<sqry_core::graph::unified::NodeId> {
3491        staging.operations().iter().find_map(|op| match op {
3492            StagingOp::AddNode {
3493                entry,
3494                expected_id: Some(id),
3495            } if entry.kind == kind
3496                && staging.resolve_node_canonical_name(entry) == Some(canonical_name) =>
3497            {
3498                Some(*id)
3499            }
3500            _ => None,
3501        })
3502    }
3503
3504    /// Build the unified graph for a C++ source snippet and return the staged graph.
3505    fn build_cpp(source: &str) -> StagingGraph {
3506        let tree = parse_cpp(source);
3507        let mut staging = StagingGraph::new();
3508        let builder = CppGraphBuilder::new();
3509        builder
3510            .build_graph(
3511                &tree,
3512                source.as_bytes(),
3513                Path::new("test.cpp"),
3514                &mut staging,
3515            )
3516            .expect("build_graph must succeed for the test fixture");
3517        staging
3518    }
3519
3520    /// AC-1 + AC-2 + AC-4 (struct default visibility) + AC-5:
3521    /// instance struct fields emit Property nodes with `Class.field`
3522    /// qualified-name shape, `is_static = false`, visibility = `"public"`
3523    /// (struct default), and a `TypeOf` edge using `TypeOfContext::Field` +
3524    /// the bare field name.
3525    #[test]
3526    fn test_struct_field_emits_property_with_field_context() {
3527        let source = "struct Point { int x; int y; };";
3528        let staging = build_cpp(source);
3529
3530        // AC-1: dotted Class.field qualified name.
3531        assert_has_node_with_kind_exact(&staging, "Point.x", NodeKind::Property);
3532        assert_has_node_with_kind_exact(&staging, "Point.y", NodeKind::Property);
3533
3534        let entry =
3535            cpp_find_added_node(&staging, "Point.x").expect("Point.x should be staged as a node");
3536        assert_eq!(entry.kind, NodeKind::Property, "x must be Property");
3537        assert!(!entry.is_static, "instance field is_static must be false");
3538        let vis = staging.resolve_local_string(entry.visibility.expect("visibility id"));
3539        assert_eq!(
3540            vis,
3541            Some("public"),
3542            "struct default visibility must be 'public'"
3543        );
3544        // `span_from_node` packs row/column into `Span::Position`; the helper
3545        // then stores them into `start_line`/`start_column`/`end_line`/
3546        // `end_column` on the entry (start_byte/end_byte are intentionally
3547        // not populated by `add_node_internal`). Assert the packed
3548        // line/column range is non-empty so we catch zero-width spans.
3549        assert!(entry.end_line > 0, "field end_line must be set (got 0)");
3550        assert!(
3551            entry.end_line > entry.start_line
3552                || (entry.end_line == entry.start_line && entry.end_column > entry.start_column),
3553            "field span must be non-empty: [{}:{}..{}:{}]",
3554            entry.start_line,
3555            entry.start_column,
3556            entry.end_line,
3557            entry.end_column,
3558        );
3559
3560        // AC-5: TypeOf edge with Field context + bare name "x".
3561        let x_id = cpp_find_added_node_id(&staging, "Point.x", NodeKind::Property)
3562            .expect("Point.x Property NodeId");
3563        let edge = staging.operations().iter().find_map(|op| {
3564            if let StagingOp::AddEdge {
3565                source: src,
3566                kind: EdgeKind::TypeOf { context, name, .. },
3567                ..
3568            } = op
3569                && *src == x_id
3570            {
3571                Some((*context, *name))
3572            } else {
3573                None
3574            }
3575        });
3576        let (ctx, name) = edge.expect("TypeOf edge from Point.x should be staged");
3577        assert_eq!(
3578            ctx,
3579            Some(TypeOfContext::Field),
3580            "TypeOf edge context must be Field"
3581        );
3582        let resolved_name = name.and_then(|sid| staging.resolve_local_string(sid));
3583        assert_eq!(
3584            resolved_name,
3585            Some("x"),
3586            "TypeOf edge name must be the bare field name 'x'"
3587        );
3588
3589        // AC-1 (negative): old NodeKind::Variable for these names must NOT appear.
3590        let stale_variable = staging.nodes().any(|n| {
3591            n.entry.kind == NodeKind::Variable
3592                && matches!(
3593                    staging.resolve_node_name(n.entry),
3594                    Some("Point.x" | "Point.y" | "Point::x" | "Point::y")
3595                )
3596        });
3597        assert!(
3598            !stale_variable,
3599            "Point fields must not be emitted as NodeKind::Variable"
3600        );
3601    }
3602
3603    /// AC-4: class default visibility is `"private"`.
3604    #[test]
3605    fn test_class_field_default_visibility_is_private() {
3606        let source = "class Foo { int hidden; };";
3607        let staging = build_cpp(source);
3608
3609        let entry = cpp_find_added_node(&staging, "Foo.hidden")
3610            .expect("Foo.hidden should be staged as a node");
3611        assert_eq!(entry.kind, NodeKind::Property);
3612        let vis = staging.resolve_local_string(entry.visibility.expect("visibility id"));
3613        assert_eq!(
3614            vis,
3615            Some("private"),
3616            "class default visibility must be 'private'"
3617        );
3618    }
3619
3620    /// AC-4: explicit access specifier overrides the default.
3621    #[test]
3622    fn test_class_field_respects_explicit_access_specifier() {
3623        let source = "class Foo { public: int public_field; protected: int prot_field; };";
3624        let staging = build_cpp(source);
3625
3626        let pub_entry = cpp_find_added_node(&staging, "Foo.public_field")
3627            .expect("Foo.public_field should be staged");
3628        assert_eq!(
3629            staging.resolve_local_string(pub_entry.visibility.expect("vis")),
3630            Some("public")
3631        );
3632
3633        let prot_entry = cpp_find_added_node(&staging, "Foo.prot_field")
3634            .expect("Foo.prot_field should be staged");
3635        assert_eq!(
3636            staging.resolve_local_string(prot_entry.visibility.expect("vis")),
3637            Some("protected")
3638        );
3639    }
3640
3641    /// AC-2 + AC-3: `const` field → Constant; instance const has
3642    /// `is_static = false` (no `static` keyword present).
3643    #[test]
3644    fn test_const_field_emits_constant() {
3645        let source = "class Foo { const int kMax = 0; };";
3646        let staging = build_cpp(source);
3647
3648        assert_has_node_with_kind_exact(&staging, "Foo.kMax", NodeKind::Constant);
3649        let entry = cpp_find_added_node(&staging, "Foo.kMax").expect("Foo.kMax");
3650        assert_eq!(entry.kind, NodeKind::Constant);
3651        assert!(
3652            !entry.is_static,
3653            "const (non-static) field is_static must be false; only `static` keyword sets is_static"
3654        );
3655    }
3656
3657    /// AC-2 + AC-3: `constexpr` field → Constant. The `static` flag is
3658    /// driven strictly by the `static` keyword (per design §3.4); a bare
3659    /// `constexpr` member without `static` must keep `is_static = false`.
3660    #[test]
3661    fn test_constexpr_field_emits_constant() {
3662        let source = "class Foo { constexpr static int kAnswer = 42; };";
3663        let staging = build_cpp(source);
3664
3665        assert_has_node_with_kind_exact(&staging, "Foo.kAnswer", NodeKind::Constant);
3666        let entry = cpp_find_added_node(&staging, "Foo.kAnswer").expect("Foo.kAnswer");
3667        assert_eq!(entry.kind, NodeKind::Constant);
3668        assert!(
3669            entry.is_static,
3670            "static constexpr member must have is_static = true"
3671        );
3672    }
3673
3674    /// AC-3: `static` keyword sets `is_static = true` on a Property
3675    /// (non-const non-constexpr).
3676    #[test]
3677    fn test_static_field_sets_is_static_true() {
3678        let source = "class Foo { static int counter; };";
3679        let staging = build_cpp(source);
3680
3681        let entry = cpp_find_added_node(&staging, "Foo.counter").expect("Foo.counter");
3682        assert_eq!(entry.kind, NodeKind::Property);
3683        assert!(entry.is_static, "static keyword must set is_static = true");
3684    }
3685
3686    /// AC-6: bit-fields (e.g., `int flags : 4;`) emit Property nodes with the
3687    /// usual `Class.field` form.
3688    #[test]
3689    fn test_bitfield_emits_property() {
3690        let source = "struct Flags { unsigned int low : 4; unsigned int high : 4; };";
3691        let staging = build_cpp(source);
3692
3693        assert_has_node_with_kind_exact(&staging, "Flags.low", NodeKind::Property);
3694        assert_has_node_with_kind_exact(&staging, "Flags.high", NodeKind::Property);
3695    }
3696
3697    /// AC-6: anonymous union — true anonymous unions (no instance name) inject
3698    /// their members into the enclosing class per C++ semantics. Members must
3699    /// emit as Property nodes under the OUTER class qualifier
3700    /// (`Variant.as_int`, `Variant.as_float`), NOT under any synthetic inner
3701    /// qualifier — there is no name to qualify by.
3702    #[test]
3703    fn test_anonymous_union_member_fields_emit_property() {
3704        let source = r"
3705class Variant {
3706public:
3707    int tag;
3708    union {
3709        int as_int;
3710        float as_float;
3711    };
3712};
3713";
3714        let staging = build_cpp(source);
3715
3716        // Outer named field is present with the dotted form.
3717        assert_has_node_with_kind_exact(&staging, "Variant.tag", NodeKind::Property);
3718
3719        // Anonymous-union members are injected into the enclosing class and
3720        // appear under `Variant.<member>` per C++ semantics (design AC-6).
3721        assert_has_node_with_kind_exact(&staging, "Variant.as_int", NodeKind::Property);
3722        assert_has_node_with_kind_exact(&staging, "Variant.as_float", NodeKind::Property);
3723
3724        // Visibility for injected members inherits the OUTER access state
3725        // (`public:` here).
3726        let as_int = cpp_find_added_node(&staging, "Variant.as_int")
3727            .expect("Variant.as_int should be staged");
3728        let vis = staging.resolve_local_string(as_int.visibility.expect("visibility id"));
3729        assert_eq!(
3730            vis,
3731            Some("public"),
3732            "anonymous-union members must inherit OUTER access (`public:` here)"
3733        );
3734
3735        // Negative: there must be no synthetic anonymous-union qualifier
3736        // such as `Variant::.as_int` or members under a bogus inner name.
3737        let bogus = staging.nodes().any(|n| {
3738            staging
3739                .resolve_node_name(n.entry)
3740                .is_some_and(|name| name.contains("::.") || name.starts_with("Variant::."))
3741        });
3742        assert!(
3743            !bogus,
3744            "anonymous union must not produce a synthetic qualifier"
3745        );
3746
3747        // No stale Variable emission for any of these names.
3748        let stale_variable = staging.nodes().any(|n| {
3749            n.entry.kind == NodeKind::Variable
3750                && matches!(
3751                    staging.resolve_node_name(n.entry),
3752                    Some("Variant.tag" | "Variant.as_int" | "Variant.as_float")
3753                )
3754        });
3755        assert!(
3756            !stale_variable,
3757            "anonymous-union members + outer fields must not stay as Variable"
3758        );
3759    }
3760
3761    /// AC-6: templated class — `template<class T> struct Box { T value; };`
3762    /// emits the field under the bare class name (template-args part is
3763    /// stripped for the qualified name; design §4.1 edge cases).
3764    #[test]
3765    fn test_templated_class_field_emits_property() {
3766        let source = r"
3767template<class T>
3768struct Box {
3769    T value;
3770};
3771";
3772        let staging = build_cpp(source);
3773
3774        assert_has_node_with_kind_exact(&staging, "Box.value", NodeKind::Property);
3775        let entry = cpp_find_added_node(&staging, "Box.value").expect("Box.value");
3776        assert_eq!(entry.kind, NodeKind::Property);
3777        assert!(!entry.is_static);
3778    }
3779
3780    /// AC-6: nested class — both the OUTER field (`Outer.outer_value`) and the
3781    /// INNER nested-class fields (`Outer::Inner.x`) must emit as Property
3782    /// nodes. `walk_class_body` recurses into a nested
3783    /// `field_declaration > class_specifier` and extends the qualifier chain
3784    /// with the inner-class name (design AC-6 + §4.1).
3785    #[test]
3786    fn test_outer_class_field_with_nested_class_present() {
3787        let source = r"
3788class Outer {
3789public:
3790    int outer_value;
3791    class Inner {
3792    public:
3793        int x;
3794    };
3795};
3796";
3797        let staging = build_cpp(source);
3798
3799        // AC-6: outer field is emitted under the dotted form.
3800        assert_has_node_with_kind_exact(&staging, "Outer.outer_value", NodeKind::Property);
3801
3802        // AC-6: nested-class field emits under the parent-qualified dotted
3803        // form `Outer::Inner.x` (class chain stays `::`, last separator
3804        // migrates to `.` per design §3.1.1).
3805        assert_has_node_with_kind_exact(&staging, "Outer::Inner.x", NodeKind::Property);
3806
3807        // Negative legacy lookup: the legacy `Outer::outer_value` form must
3808        // not appear (AC-7 + design §3.1.1).
3809        let legacy_hits: Vec<_> = staging
3810            .nodes()
3811            .filter(|n| staging.resolve_node_name(n.entry) == Some("Outer::outer_value"))
3812            .collect();
3813        assert!(
3814            legacy_hits.is_empty(),
3815            "legacy `Outer::outer_value` lookup must return 0 hits"
3816        );
3817
3818        // Negative: nested field must not appear under bare `Inner.x` (lost
3819        // outer chain) or legacy `Outer::Inner::x` (last separator missed
3820        // migration).
3821        for legacy in ["Inner.x", "Outer::Inner::x", "Outer.Inner.x"] {
3822            let hits: Vec<_> = staging
3823                .nodes()
3824                .filter(|n| staging.resolve_node_name(n.entry) == Some(legacy))
3825                .collect();
3826            assert!(
3827                hits.is_empty(),
3828                "nested-class field `{legacy}` must not appear; expected only `Outer::Inner.x`"
3829            );
3830        }
3831    }
3832
3833    /// AC-6: nested struct inside a class — nested struct fields qualify as
3834    /// `Outer::Inner.y`. Default struct visibility is `public`, regardless
3835    /// of the OUTER access state.
3836    #[test]
3837    fn test_outer_class_with_nested_struct_emits_inner_field() {
3838        let source = r"
3839class Outer {
3840private:
3841    struct Inner {
3842        int y;
3843    };
3844};
3845";
3846        let staging = build_cpp(source);
3847
3848        assert_has_node_with_kind_exact(&staging, "Outer::Inner.y", NodeKind::Property);
3849
3850        let entry = cpp_find_added_node(&staging, "Outer::Inner.y")
3851            .expect("Outer::Inner.y should be staged");
3852        let vis = staging.resolve_local_string(entry.visibility.expect("visibility id"));
3853        assert_eq!(
3854            vis,
3855            Some("public"),
3856            "nested struct field default visibility must be 'public' \
3857             regardless of OUTER access state"
3858        );
3859    }
3860
3861    /// Staging-level smoke: post-fix, no staged node for a class field uses
3862    /// the legacy `Class::field` qualified-name shape. This is a
3863    /// fast-feedback companion to the AC-7 contract test — the authoritative
3864    /// AC-7 assertion runs against a finalized `GraphSnapshot` via
3865    /// `find_nodes_by_name` in
3866    /// `tests/integration_tests.rs::test_legacy_double_colon_field_lookup_returns_zero_via_snapshot`
3867    /// (design §4.1).
3868    #[test]
3869    fn test_legacy_double_colon_field_lookup_returns_zero() {
3870        let source = r"
3871class Foo {
3872public:
3873    int bar;
3874    static int baz;
3875    const int qux = 0;
3876};
3877struct Quux {
3878    int corge;
3879};
3880";
3881        let staging = build_cpp(source);
3882
3883        // Positive: dotted form must be present for every field.
3884        assert_has_node_with_kind_exact(&staging, "Foo.bar", NodeKind::Property);
3885        assert_has_node_with_kind_exact(&staging, "Foo.baz", NodeKind::Property);
3886        assert_has_node_with_kind_exact(&staging, "Foo.qux", NodeKind::Constant);
3887        assert_has_node_with_kind_exact(&staging, "Quux.corge", NodeKind::Property);
3888
3889        // Negative: legacy `Class::field` qualified name must not appear for
3890        // any of the fields in the fixture.
3891        for legacy in ["Foo::bar", "Foo::baz", "Foo::qux", "Quux::corge"] {
3892            let hits: Vec<_> = staging
3893                .nodes()
3894                .filter(|n| staging.resolve_node_name(n.entry) == Some(legacy))
3895                .collect();
3896            assert!(
3897                hits.is_empty(),
3898                "legacy lookup for {legacy:?} must return 0 hits, got {} node(s) ({:?})",
3899                hits.len(),
3900                hits.iter()
3901                    .map(|n| (n.entry.kind, staging.resolve_node_name(n.entry)))
3902                    .collect::<Vec<_>>()
3903            );
3904        }
3905    }
3906
3907    /// Field inside a class that lives in a namespace must keep the namespace
3908    /// chain joined by `::` and only flip the LAST separator to `.`.
3909    #[test]
3910    fn test_namespaced_class_field_qualified_name() {
3911        let source = r"
3912namespace demo {
3913    class Service {
3914    public:
3915        int counter;
3916    };
3917}
3918";
3919        let staging = build_cpp(source);
3920
3921        assert_has_node_with_kind_exact(&staging, "demo::Service.counter", NodeKind::Property);
3922    }
3923}