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