Skip to main content

sqry_lang_cpp/relations/
graph_builder.rs

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