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