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