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