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