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::{FfiConvention, GraphBuildHelper, StagingGraph};
19use sqry_core::graph::{GraphBuilder, GraphBuilderError, GraphResult, Language, Position, Span};
20use std::{
21    collections::{HashMap, HashSet},
22    path::{Path, PathBuf},
23    time::{Duration, Instant},
24};
25use tree_sitter::{Node, Tree};
26
27/// File-level module name for exports.
28/// In C++, symbols at file/namespace scope with external linkage are exported.
29const FILE_MODULE_NAME: &str = "<file_module>";
30
31/// Type alias for mapping (qualifier, name) tuples to fully-qualified names
32/// Used for both field types and type mappings in C++ AST analysis
33type QualifiedNameMap = HashMap<(String, String), String>;
34
35/// Registry of FFI declarations discovered during graph building.
36///
37/// Maps simple function names (e.g., `printf`) to their qualified FFI name
38/// (e.g., `extern::C::printf`) and calling convention. This allows call edge
39/// construction to detect when a call targets an FFI function and create
40/// `FfiCall` edges instead of regular `Call` edges.
41type FfiRegistry = HashMap<String, (String, FfiConvention)>;
42
43/// Registry of pure virtual interfaces (abstract classes with only pure virtual methods).
44///
45/// Maps interface name to their qualified names for Implements edge creation.
46type PureVirtualRegistry = HashSet<String>;
47
48const DEFAULT_GRAPH_BUILD_TIMEOUT_MS: u64 = 10_000;
49const MIN_GRAPH_BUILD_TIMEOUT_MS: u64 = 1_000;
50const MAX_GRAPH_BUILD_TIMEOUT_MS: u64 = 60_000;
51const BUDGET_CHECK_INTERVAL: u32 = 1024;
52
53fn cpp_graph_build_timeout() -> Duration {
54    let timeout_ms = std::env::var("SQRY_CPP_GRAPH_BUILD_TIMEOUT_MS")
55        .ok()
56        .and_then(|value| value.parse::<u64>().ok())
57        .unwrap_or(DEFAULT_GRAPH_BUILD_TIMEOUT_MS)
58        .clamp(MIN_GRAPH_BUILD_TIMEOUT_MS, MAX_GRAPH_BUILD_TIMEOUT_MS);
59    Duration::from_millis(timeout_ms)
60}
61
62struct BuildBudget {
63    file: PathBuf,
64    phase_timeout: Duration,
65    started_at: Instant,
66    checkpoints: u32,
67}
68
69impl BuildBudget {
70    fn new(file: &Path) -> Self {
71        Self {
72            file: file.to_path_buf(),
73            phase_timeout: cpp_graph_build_timeout(),
74            started_at: Instant::now(),
75            checkpoints: 0,
76        }
77    }
78
79    #[cfg(test)]
80    fn already_expired(file: &Path) -> Self {
81        Self {
82            file: file.to_path_buf(),
83            phase_timeout: Duration::from_secs(1),
84            started_at: Instant::now() - Duration::from_secs(60),
85            checkpoints: BUDGET_CHECK_INTERVAL - 1,
86        }
87    }
88
89    fn checkpoint(&mut self, phase: &'static str) -> GraphResult<()> {
90        self.checkpoints = self.checkpoints.wrapping_add(1);
91        if self.checkpoints.is_multiple_of(BUDGET_CHECK_INTERVAL)
92            && self.started_at.elapsed() > self.phase_timeout
93        {
94            return Err(GraphBuilderError::BuildTimedOut {
95                file: self.file.clone(),
96                phase,
97                timeout_ms: self.phase_timeout.as_millis() as u64,
98            });
99        }
100        Ok(())
101    }
102}
103
104// Helper extension trait for Span creation
105#[allow(dead_code)] // Reserved for future span-based analysis
106trait SpanExt {
107    fn from_node(node: &tree_sitter::Node) -> Self;
108}
109
110impl SpanExt for Span {
111    fn from_node(node: &tree_sitter::Node) -> Self {
112        Span::new(
113            Position::new(node.start_position().row, node.start_position().column),
114            Position::new(node.end_position().row, node.end_position().column),
115        )
116    }
117}
118
119// ================================
120// ASTGraph: In-memory function context index
121// ================================
122
123/// In-memory index of C++ function contexts for O(1) lookups during call edge extraction.
124///
125/// This structure is built in a first pass over the AST and provides:
126/// - Fast lookup of the enclosing function for any byte position
127/// - Qualified names for all functions/methods
128/// - Field type resolution for member variable method calls
129/// - Type name resolution via includes and using declarations
130#[derive(Debug)]
131struct ASTGraph {
132    /// All function/method contexts with their qualified names and byte spans
133    contexts: Vec<FunctionContext>,
134    /// Maps function definition start byte to its context index.
135    context_start_index: HashMap<usize, usize>,
136
137    /// Maps (`class_fqn`, `field_name`) to field's FQN type.
138    /// Example: ("`demo::Service`", "repo") -> "`demo::Repository`"
139    /// This avoids collisions when multiple classes have fields with the same name.
140    /// Used to resolve method calls on member variables (e.g., repo.save -> `demo::Repository::save`)
141    /// Reserved for future call resolution enhancements
142    #[allow(dead_code)]
143    field_types: QualifiedNameMap,
144
145    /// Maps (`namespace_context`, `simple_type_name`) to FQN.
146    /// Example: ("demo", "Repository") -> "`demo::Repository`"
147    /// This handles the fact that the same simple type name can resolve differently
148    /// in different namespaces and using-directive scopes.
149    /// Used to resolve static method calls (e.g., `Repository::save` -> `demo::Repository::save`)
150    /// Reserved for future call resolution enhancements
151    #[allow(dead_code)]
152    type_map: QualifiedNameMap,
153
154    /// Maps byte ranges to namespace prefixes (e.g., range -> "`demo::`")
155    /// Used to determine which namespace context a symbol is defined in
156    /// Reserved for future namespace-aware resolution
157    #[allow(dead_code)]
158    namespace_map: HashMap<std::ops::Range<usize>, String>,
159}
160
161impl ASTGraph {
162    /// Build `ASTGraph` from tree-sitter AST
163    fn from_tree(root: Node, content: &[u8], budget: &mut BuildBudget) -> GraphResult<Self> {
164        // Extract namespace context
165        let namespace_map = extract_namespace_map(root, content, budget)?;
166
167        // Extract function contexts
168        let mut contexts = extract_cpp_contexts(root, content, &namespace_map, budget)?;
169        contexts.sort_by_key(|ctx| ctx.span.0);
170        let context_start_index = contexts
171            .iter()
172            .enumerate()
173            .map(|(idx, ctx)| (ctx.span.0, idx))
174            .collect();
175
176        // Extract field declarations and type mappings
177        let (field_types, type_map) =
178            extract_field_and_type_info(root, content, &namespace_map, budget)?;
179
180        Ok(Self {
181            contexts,
182            context_start_index,
183            field_types,
184            type_map,
185            namespace_map,
186        })
187    }
188
189    /// Find the enclosing function context for a given byte position.
190    ///
191    /// C++ has no nested function definitions, so at most one function span can
192    /// contain any byte offset. With contexts sorted by start byte we can use a
193    /// binary search instead of scanning every function for every call site.
194    fn find_enclosing(&self, byte_pos: usize) -> Option<&FunctionContext> {
195        let insertion_point = self.contexts.partition_point(|ctx| ctx.span.0 <= byte_pos);
196        if insertion_point == 0 {
197            return None;
198        }
199
200        let candidate = &self.contexts[insertion_point - 1];
201        (byte_pos < candidate.span.1).then_some(candidate)
202    }
203
204    fn context_for_start(&self, start_byte: usize) -> Option<&FunctionContext> {
205        self.context_start_index
206            .get(&start_byte)
207            .and_then(|idx| self.contexts.get(*idx))
208    }
209}
210
211/// Represents a C++ function or method with its qualified name and metadata
212#[derive(Debug, Clone)]
213struct FunctionContext {
214    /// Fully qualified name: "`demo::Service::process`" or "`demo::helper`"
215    qualified_name: String,
216    /// Byte span of the function body
217    span: (usize, usize),
218    /// Whether this is a static method
219    /// Reserved for future method resolution enhancements
220    is_static: bool,
221    /// Whether this is a virtual method
222    /// Reserved for future polymorphic call analysis
223    #[allow(dead_code)]
224    is_virtual: bool,
225    /// Whether this is inline
226    /// Reserved for future optimization hints
227    #[allow(dead_code)]
228    is_inline: bool,
229    /// Namespace stack for use in call resolution (e.g., [`demo`])
230    namespace_stack: Vec<String>,
231    /// Class stack for use in call resolution (e.g., [`Service`])
232    /// Reserved for future method resolution enhancements
233    #[allow(dead_code)] // Used in tests and reserved for future call resolution
234    class_stack: Vec<String>,
235    /// Return type of the function (e.g., `int`, `std::string`)
236    return_type: Option<String>,
237}
238
239impl FunctionContext {
240    #[allow(dead_code)] // Reserved for future context queries
241    fn qualified_name(&self) -> &str {
242        &self.qualified_name
243    }
244}
245
246/// Cpp-specific `GraphBuilder` implementation.
247///
248/// Performs multi-pass analysis:
249/// 1. Extract class and object definitions
250/// 2. Extract function and property definitions
251/// 3. Extract call expressions
252///
253/// # Example
254///
255/// ```no_run
256/// use sqry_lang_cpp::relations::CppGraphBuilder;
257/// use sqry_core::graph::GraphBuilder;
258/// use sqry_core::graph::unified::StagingGraph;
259/// use tree_sitter::Parser;
260///
261/// let mut parser = Parser::new();
262/// parser.set_language(&tree_sitter_cpp::LANGUAGE.into()).unwrap();
263/// let tree = parser.parse(b"class User { public: std::string getName() { return \"Alice\"; } };", None).unwrap();
264/// let mut staging = StagingGraph::new();
265/// let builder = CppGraphBuilder::new();
266/// builder.build_graph(&tree, b"class User { public: std::string getName() { return \"Alice\"; } };",
267///                      std::path::Path::new("test.cpp"), &mut staging).unwrap();
268/// ```
269#[derive(Debug, Default, Clone, Copy)]
270pub struct CppGraphBuilder;
271
272impl CppGraphBuilder {
273    /// Create a new Cpp `GraphBuilder`.
274    #[must_use]
275    pub fn new() -> Self {
276        Self
277    }
278
279    fn build_graph_with_budget(
280        &self,
281        tree: &Tree,
282        content: &[u8],
283        file: &Path,
284        staging: &mut StagingGraph,
285        budget: &mut BuildBudget,
286    ) -> GraphResult<()> {
287        // Create helper for staging graph population
288        let mut helper = GraphBuildHelper::new(staging, file, Language::Cpp);
289
290        // Build AST graph for call context tracking
291        let ast_graph = ASTGraph::from_tree(tree.root_node(), content, budget)?;
292
293        // Track seen includes for deduplication
294        let mut seen_includes: HashSet<String> = HashSet::new();
295
296        // Track namespace and class context for qualified naming
297        let mut namespace_stack: Vec<String> = Vec::new();
298        let mut class_stack: Vec<String> = Vec::new();
299
300        // Two-pass approach for FFI call linking:
301        // Pass 1: Collect FFI declarations so calls can be resolved regardless of source order
302        let mut ffi_registry = FfiRegistry::new();
303        collect_ffi_declarations(tree.root_node(), content, &mut ffi_registry, budget)?;
304
305        // Pass 1b: Collect pure virtual interfaces for Implements edge detection
306        let mut pure_virtual_registry = PureVirtualRegistry::new();
307        collect_pure_virtual_interfaces(
308            tree.root_node(),
309            content,
310            &mut pure_virtual_registry,
311            budget,
312        )?;
313
314        // Walk tree to find classes, functions, methods, and calls
315        walk_tree_for_graph(
316            tree.root_node(),
317            content,
318            &ast_graph,
319            &mut helper,
320            &mut seen_includes,
321            &mut namespace_stack,
322            &mut class_stack,
323            &ffi_registry,
324            &pure_virtual_registry,
325            budget,
326        )?;
327
328        Ok(())
329    }
330
331    /// Extract class attributes from modifiers.
332    #[allow(dead_code)] // Scaffolding for class attribute analysis
333    fn extract_class_attributes(node: &tree_sitter::Node, content: &[u8]) -> Vec<String> {
334        let mut attributes = Vec::new();
335        let mut cursor = node.walk();
336        for child in node.children(&mut cursor) {
337            if child.kind() == "modifiers" {
338                let mut mod_cursor = child.walk();
339                for modifier in child.children(&mut mod_cursor) {
340                    if let Ok(mod_text) = modifier.utf8_text(content) {
341                        match mod_text {
342                            "template" => attributes.push("template".to_string()),
343                            "sealed" => attributes.push("sealed".to_string()),
344                            "abstract" => attributes.push("abstract".to_string()),
345                            "open" => attributes.push("open".to_string()),
346                            "final" => attributes.push("final".to_string()),
347                            "inner" => attributes.push("inner".to_string()),
348                            "value" => attributes.push("value".to_string()),
349                            _ => {}
350                        }
351                    }
352                }
353            }
354        }
355        attributes
356    }
357
358    /// Check if a function is virtual (async).
359    #[allow(dead_code)] // Scaffolding for virtual method detection
360    fn extract_is_virtual(node: &tree_sitter::Node, content: &[u8]) -> bool {
361        if let Some(spec) = node.child_by_field_name("declaration_specifiers")
362            && let Ok(text) = spec.utf8_text(content)
363            && text.contains("virtual")
364        {
365            return true;
366        }
367
368        if let Ok(text) = node.utf8_text(content)
369            && text.contains("virtual")
370        {
371            return true;
372        }
373
374        if let Some(parent) = node.parent()
375            && (parent.kind() == "field_declaration" || parent.kind() == "declaration")
376            && let Ok(text) = parent.utf8_text(content)
377            && text.contains("virtual")
378        {
379            return true;
380        }
381
382        false
383    }
384
385    /// Extract function attributes from modifiers.
386    #[allow(dead_code)] // Scaffolding for function attribute analysis
387    fn extract_function_attributes(node: &tree_sitter::Node, content: &[u8]) -> Vec<String> {
388        let mut attributes = Vec::new();
389        for node_ref in [
390            node.child_by_field_name("declaration_specifiers"),
391            node.parent(),
392        ]
393        .into_iter()
394        .flatten()
395        {
396            if let Ok(text) = node_ref.utf8_text(content) {
397                for keyword in [
398                    "virtual",
399                    "inline",
400                    "constexpr",
401                    "operator",
402                    "override",
403                    "static",
404                ] {
405                    if text.contains(keyword) && !attributes.contains(&keyword.to_string()) {
406                        attributes.push(keyword.to_string());
407                    }
408                }
409            }
410        }
411
412        if let Ok(text) = node.utf8_text(content) {
413            for keyword in [
414                "virtual",
415                "inline",
416                "constexpr",
417                "operator",
418                "override",
419                "static",
420            ] {
421                if text.contains(keyword) && !attributes.contains(&keyword.to_string()) {
422                    attributes.push(keyword.to_string());
423                }
424            }
425        }
426
427        attributes
428    }
429}
430
431impl GraphBuilder for CppGraphBuilder {
432    fn language(&self) -> Language {
433        Language::Cpp
434    }
435
436    fn build_graph(
437        &self,
438        tree: &Tree,
439        content: &[u8],
440        file: &Path,
441        staging: &mut StagingGraph,
442    ) -> GraphResult<()> {
443        let mut budget = BuildBudget::new(file);
444        self.build_graph_with_budget(tree, content, file, staging, &mut budget)
445    }
446}
447
448// ================================
449// Context Extraction (Stub Implementations)
450// ================================
451
452/// Extract namespace declarations and build a map from byte ranges to namespace names.
453///
454/// This function recursively traverses the AST and builds a map from byte ranges to namespace
455/// prefixes. For example, if a node is inside `namespace demo { ... }`, its byte range will
456/// map to "`demo::`".
457///
458/// Returns: `HashMap`<Range<usize>, String> mapping byte ranges to namespace prefixes
459fn extract_namespace_map(
460    node: Node,
461    content: &[u8],
462    budget: &mut BuildBudget,
463) -> GraphResult<HashMap<std::ops::Range<usize>, String>> {
464    let mut map = HashMap::new();
465
466    // Create recursion guard with configured limit
467    let recursion_limits = sqry_core::config::RecursionLimits::load_or_default()
468        .expect("Failed to load recursion limits");
469    let file_ops_depth = recursion_limits
470        .effective_file_ops_depth()
471        .expect("Invalid file_ops_depth configuration");
472    let mut guard = sqry_core::query::security::RecursionGuard::new(file_ops_depth)
473        .expect("Failed to create recursion guard");
474
475    extract_namespaces_recursive(node, content, "", &mut map, &mut guard, budget).map_err(|e| {
476        match e {
477            timeout @ GraphBuilderError::BuildTimedOut { .. } => timeout,
478            other => GraphBuilderError::ParseError {
479                span: span_from_node(node),
480                reason: format!("C++ namespace extraction failed: {other}"),
481            },
482        }
483    })?;
484
485    Ok(map)
486}
487
488/// Recursive helper for namespace extraction
489///
490/// # Errors
491///
492/// Returns [`RecursionError::DepthLimitExceeded`] if recursion depth exceeds the guard's limit.
493fn extract_namespaces_recursive(
494    node: Node,
495    content: &[u8],
496    current_ns: &str,
497    map: &mut HashMap<std::ops::Range<usize>, String>,
498    guard: &mut sqry_core::query::security::RecursionGuard,
499    budget: &mut BuildBudget,
500) -> GraphResult<()> {
501    budget.checkpoint("cpp:extract_namespace_map")?;
502    guard.enter().map_err(|e| GraphBuilderError::ParseError {
503        span: span_from_node(node),
504        reason: format!("C++ namespace extraction hit recursion limit: {e}"),
505    })?;
506
507    if node.kind() == "namespace_definition" {
508        // Extract namespace name from the namespace_identifier or identifier child
509        let ns_name = if let Some(name_node) = node.child_by_field_name("name") {
510            extract_identifier(name_node, content)
511        } else {
512            // Anonymous namespace
513            String::from("anonymous")
514        };
515
516        // Build new namespace prefix
517        let new_ns = if current_ns.is_empty() {
518            format!("{ns_name}::")
519        } else {
520            format!("{current_ns}{ns_name}::")
521        };
522
523        // Map the body's byte range to this namespace
524        if let Some(body) = node.child_by_field_name("body") {
525            let range = body.start_byte()..body.end_byte();
526            map.insert(range, new_ns.clone());
527
528            // Recurse into nested namespaces within the body
529            let mut cursor = body.walk();
530            for child in body.children(&mut cursor) {
531                extract_namespaces_recursive(child, content, &new_ns, map, guard, budget)?;
532            }
533        }
534    } else {
535        // Recurse with current namespace
536        let mut cursor = node.walk();
537        for child in node.children(&mut cursor) {
538            extract_namespaces_recursive(child, content, current_ns, map, guard, budget)?;
539        }
540    }
541
542    guard.exit();
543    Ok(())
544}
545
546/// Extract identifier from a node (handles simple identifiers and qualified names)
547fn extract_identifier(node: Node, content: &[u8]) -> String {
548    node.utf8_text(content).unwrap_or("").to_string()
549}
550
551/// Find the namespace prefix for a given byte offset
552fn find_namespace_for_offset(
553    byte_offset: usize,
554    namespace_map: &HashMap<std::ops::Range<usize>, String>,
555) -> String {
556    // Find all ranges that contain this offset
557    let mut matching_ranges: Vec<_> = namespace_map
558        .iter()
559        .filter(|(range, _)| range.contains(&byte_offset))
560        .collect();
561
562    // Sort by range size (smaller ranges are more specific/nested)
563    matching_ranges.sort_by_key(|(range, _)| range.end - range.start);
564
565    // Return the most specific (smallest) range's namespace
566    matching_ranges
567        .first()
568        .map_or("", |(_, ns)| ns.as_str())
569        .to_string()
570}
571
572/// Extract all function/method contexts with their qualified names.
573///
574/// This function traverses the AST and builds a complete list of all functions/methods
575/// with their fully qualified names (including namespace and class context).
576///
577/// Returns: Vec<FunctionContext> with all function/method contexts
578fn extract_cpp_contexts(
579    node: Node,
580    content: &[u8],
581    namespace_map: &HashMap<std::ops::Range<usize>, String>,
582    budget: &mut BuildBudget,
583) -> GraphResult<Vec<FunctionContext>> {
584    let mut contexts = Vec::new();
585    let mut class_stack = Vec::new();
586
587    // Create recursion guard with configured limit
588    let recursion_limits = sqry_core::config::RecursionLimits::load_or_default()
589        .expect("Failed to load recursion limits");
590    let file_ops_depth = recursion_limits
591        .effective_file_ops_depth()
592        .expect("Invalid file_ops_depth configuration");
593    let mut guard = sqry_core::query::security::RecursionGuard::new(file_ops_depth)
594        .expect("Failed to create recursion guard");
595
596    extract_contexts_recursive(
597        node,
598        content,
599        namespace_map,
600        &mut contexts,
601        &mut class_stack,
602        &mut guard,
603        budget,
604    )
605    .map_err(|e| match e {
606        timeout @ GraphBuilderError::BuildTimedOut { .. } => timeout,
607        other => GraphBuilderError::ParseError {
608            span: span_from_node(node),
609            reason: format!("C++ context extraction failed: {other}"),
610        },
611    })?;
612
613    Ok(contexts)
614}
615
616/// Recursive helper for function context extraction
617/// # Errors
618///
619/// Returns [`RecursionError::DepthLimitExceeded`] if recursion depth exceeds the guard's limit.
620fn extract_contexts_recursive(
621    node: Node,
622    content: &[u8],
623    namespace_map: &HashMap<std::ops::Range<usize>, String>,
624    contexts: &mut Vec<FunctionContext>,
625    class_stack: &mut Vec<String>,
626    guard: &mut sqry_core::query::security::RecursionGuard,
627    budget: &mut BuildBudget,
628) -> GraphResult<()> {
629    budget.checkpoint("cpp:extract_contexts")?;
630    guard.enter().map_err(|e| GraphBuilderError::ParseError {
631        span: span_from_node(node),
632        reason: format!("C++ context extraction hit recursion limit: {e}"),
633    })?;
634
635    match node.kind() {
636        "class_specifier" | "struct_specifier" => {
637            // Extract class/struct name
638            if let Some(name_node) = node.child_by_field_name("name") {
639                let class_name = extract_identifier(name_node, content);
640                class_stack.push(class_name);
641
642                // Recurse into class body
643                if let Some(body) = node.child_by_field_name("body") {
644                    let mut cursor = body.walk();
645                    for child in body.children(&mut cursor) {
646                        extract_contexts_recursive(
647                            child,
648                            content,
649                            namespace_map,
650                            contexts,
651                            class_stack,
652                            guard,
653                            budget,
654                        )?;
655                    }
656                }
657
658                class_stack.pop();
659            }
660        }
661
662        "function_definition" => {
663            // Extract function name and build qualified name
664            if let Some(declarator) = node.child_by_field_name("declarator") {
665                let (func_name, class_prefix) =
666                    extract_function_name_with_class(declarator, content);
667
668                // Find enclosing namespace and convert to stack
669                let namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
670                let namespace_stack: Vec<String> = if namespace.is_empty() {
671                    Vec::new()
672                } else {
673                    namespace
674                        .trim_end_matches("::")
675                        .split("::")
676                        .map(String::from)
677                        .collect()
678                };
679
680                // Build the effective class stack:
681                // - If we're inside a class body, use that class stack
682                // - If this is an out-of-class method (e.g., Service::process), use the class prefix
683                let effective_class_stack: Vec<String> = if !class_stack.is_empty() {
684                    class_stack.clone()
685                } else if let Some(ref prefix) = class_prefix {
686                    vec![prefix.clone()]
687                } else {
688                    Vec::new()
689                };
690
691                // Build qualified name
692                let qualified_name =
693                    build_qualified_name(&namespace_stack, &effective_class_stack, &func_name);
694
695                // Extract metadata
696                let is_static = is_static_function(node, content);
697                let is_virtual = is_virtual_function(node, content);
698                let is_inline = is_inline_function(node, content);
699
700                // Extract return type from function definition
701                let return_type = node
702                    .child_by_field_name("type")
703                    .and_then(|type_node| type_node.utf8_text(content).ok())
704                    .map(std::string::ToString::to_string);
705
706                // Get function definition's full span for matching during graph building
707                let span = (node.start_byte(), node.end_byte());
708
709                contexts.push(FunctionContext {
710                    qualified_name,
711                    span,
712                    is_static,
713                    is_virtual,
714                    is_inline,
715                    namespace_stack,
716                    class_stack: effective_class_stack,
717                    return_type,
718                });
719            }
720
721            // Don't recurse into function body - C++ doesn't have nested functions
722        }
723
724        _ => {
725            // Recurse into children
726            let mut cursor = node.walk();
727            for child in node.children(&mut cursor) {
728                extract_contexts_recursive(
729                    child,
730                    content,
731                    namespace_map,
732                    contexts,
733                    class_stack,
734                    guard,
735                    budget,
736                )?;
737            }
738        }
739    }
740
741    guard.exit();
742    Ok(())
743}
744
745/// Build a fully qualified name from namespace stack, class stack, and name.
746///
747/// This function combines namespace context, class hierarchy, and the final name
748/// into a C++-style qualified name (e.g., `namespace::ClassName::methodName`).
749fn build_qualified_name(namespace_stack: &[String], class_stack: &[String], name: &str) -> String {
750    let mut parts = Vec::new();
751
752    // Add namespace stack
753    parts.extend(namespace_stack.iter().cloned());
754
755    // Add class stack
756    for class_name in class_stack {
757        parts.push(class_name.clone());
758    }
759
760    // Add name
761    parts.push(name.to_string());
762
763    parts.join("::")
764}
765
766/// Extract function name and optional class prefix from a function declarator node.
767/// Returns (`function_name`, `optional_class_prefix`).
768/// For `Service::process`, returns ("process", Some("Service")).
769/// For `process`, returns ("process", None).
770fn extract_function_name_with_class(declarator: Node, content: &[u8]) -> (String, Option<String>) {
771    // The declarator can be:
772    // - function_declarator (simple function)
773    // - qualified_identifier (Class::method)
774    // - field_identifier (method)
775    // - destructor_name (~Class)
776    // - operator_name (operator+)
777
778    match declarator.kind() {
779        "function_declarator" => {
780            // Recurse to find the actual name
781            if let Some(declarator_inner) = declarator.child_by_field_name("declarator") {
782                extract_function_name_with_class(declarator_inner, content)
783            } else {
784                (extract_identifier(declarator, content), None)
785            }
786        }
787        "qualified_identifier" => {
788            // For qualified names like Service::process, extract both parts
789            let name = if let Some(name_node) = declarator.child_by_field_name("name") {
790                extract_identifier(name_node, content)
791            } else {
792                extract_identifier(declarator, content)
793            };
794
795            // Extract the scope (class/namespace prefix)
796            let class_prefix = declarator
797                .child_by_field_name("scope")
798                .map(|scope_node| extract_identifier(scope_node, content));
799
800            (name, class_prefix)
801        }
802        "field_identifier" | "identifier" | "destructor_name" | "operator_name" => {
803            (extract_identifier(declarator, content), None)
804        }
805        _ => {
806            // For other cases, try to extract text directly
807            (extract_identifier(declarator, content), None)
808        }
809    }
810}
811
812/// Extract function name from a function declarator node (convenience wrapper)
813#[allow(dead_code)]
814fn extract_function_name(declarator: Node, content: &[u8]) -> String {
815    extract_function_name_with_class(declarator, content).0
816}
817
818/// Check if a function is static
819fn is_static_function(node: Node, content: &[u8]) -> bool {
820    has_specifier(node, "static", content)
821}
822
823/// Check if a function is virtual
824fn is_virtual_function(node: Node, content: &[u8]) -> bool {
825    has_specifier(node, "virtual", content)
826}
827
828/// Check if a function is inline
829fn is_inline_function(node: Node, content: &[u8]) -> bool {
830    has_specifier(node, "inline", content)
831}
832
833/// Check if a function has a specific specifier (static, virtual, inline, etc.)
834fn has_specifier(node: Node, specifier: &str, content: &[u8]) -> bool {
835    // Check declaration specifiers
836    let mut cursor = node.walk();
837    for child in node.children(&mut cursor) {
838        if (child.kind() == "storage_class_specifier"
839            || child.kind() == "type_qualifier"
840            || child.kind() == "virtual"
841            || child.kind() == "inline")
842            && let Ok(text) = child.utf8_text(content)
843            && text == specifier
844        {
845            return true;
846        }
847    }
848    false
849}
850
851/// Extract field declarations and type mappings.
852///
853/// This function traverses the AST and extracts:
854/// 1. Field types: Maps (`class_fqn`, `field_name`) to field's FQN type
855/// 2. Type map: Maps (`namespace_context`, `simple_type_name`) to FQN from using directives
856///
857/// Returns:
858/// - `field_types`: Maps (`class_fqn`, `field_name`) to field's FQN type
859/// - `type_map`: Maps (`namespace_context`, `simple_type_name`) to FQN
860fn extract_field_and_type_info(
861    node: Node,
862    content: &[u8],
863    namespace_map: &HashMap<std::ops::Range<usize>, String>,
864    budget: &mut BuildBudget,
865) -> GraphResult<(QualifiedNameMap, QualifiedNameMap)> {
866    let mut field_types = HashMap::new();
867    let mut type_map = HashMap::new();
868    let mut class_stack = Vec::new();
869
870    extract_fields_recursive(
871        node,
872        content,
873        namespace_map,
874        &mut field_types,
875        &mut type_map,
876        &mut class_stack,
877        budget,
878    )?;
879
880    Ok((field_types, type_map))
881}
882
883/// Recursive helper for field and type extraction
884fn extract_fields_recursive(
885    node: Node,
886    content: &[u8],
887    namespace_map: &HashMap<std::ops::Range<usize>, String>,
888    field_types: &mut HashMap<(String, String), String>,
889    type_map: &mut HashMap<(String, String), String>,
890    class_stack: &mut Vec<String>,
891    budget: &mut BuildBudget,
892) -> GraphResult<()> {
893    budget.checkpoint("cpp:extract_fields")?;
894    match node.kind() {
895        "class_specifier" | "struct_specifier" => {
896            // Extract class name and build FQN
897            if let Some(name_node) = node.child_by_field_name("name") {
898                let class_name = extract_identifier(name_node, content);
899                let namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
900
901                // Build FQN including parent classes from class_stack
902                let class_fqn = if class_stack.is_empty() {
903                    // Top-level class: just namespace + class name
904                    if namespace.is_empty() {
905                        class_name.clone()
906                    } else {
907                        format!("{}::{}", namespace.trim_end_matches("::"), class_name)
908                    }
909                } else {
910                    // Nested class: parent_fqn + class name
911                    format!("{}::{}", class_stack.last().unwrap(), class_name)
912                };
913
914                class_stack.push(class_fqn.clone());
915
916                // Process all children to find field_declaration_list or direct field_declaration
917                let mut cursor = node.walk();
918                for child in node.children(&mut cursor) {
919                    extract_fields_recursive(
920                        child,
921                        content,
922                        namespace_map,
923                        field_types,
924                        type_map,
925                        class_stack,
926                        budget,
927                    )?;
928                }
929
930                class_stack.pop();
931            }
932        }
933
934        "field_declaration" => {
935            // Extract field declaration if we're inside a class
936            if let Some(class_fqn) = class_stack.last() {
937                extract_field_declaration(
938                    node,
939                    content,
940                    class_fqn,
941                    namespace_map,
942                    field_types,
943                    type_map,
944                );
945            }
946        }
947
948        "using_directive" => {
949            // Extract using directive: using namespace std;
950            extract_using_directive(node, content, namespace_map, type_map);
951        }
952
953        "using_declaration" => {
954            // Extract using declaration: using std::vector;
955            extract_using_declaration(node, content, namespace_map, type_map);
956        }
957
958        _ => {
959            // Recurse into children
960            let mut cursor = node.walk();
961            for child in node.children(&mut cursor) {
962                extract_fields_recursive(
963                    child,
964                    content,
965                    namespace_map,
966                    field_types,
967                    type_map,
968                    class_stack,
969                    budget,
970                )?;
971            }
972        }
973    }
974
975    Ok(())
976}
977
978/// Extract a field declaration and store its type
979fn extract_field_declaration(
980    node: Node,
981    content: &[u8],
982    class_fqn: &str,
983    namespace_map: &HashMap<std::ops::Range<usize>, String>,
984    field_types: &mut HashMap<(String, String), String>,
985    type_map: &HashMap<(String, String), String>,
986) {
987    // In tree-sitter-cpp, field_declaration children are:
988    // type_identifier, field_identifier, ;
989    // OR for multiple declarators: type_identifier, declarator1, ',', declarator2, ;
990
991    let mut field_type = None;
992    let mut field_names = Vec::new();
993
994    let mut cursor = node.walk();
995    for child in node.children(&mut cursor) {
996        match child.kind() {
997            "type_identifier" | "primitive_type" | "qualified_identifier" | "template_type" => {
998                field_type = Some(extract_type_name(child, content));
999            }
1000            "field_identifier" => {
1001                // Direct field identifier (simple case: Type name;)
1002                field_names.push(extract_identifier(child, content));
1003            }
1004            "field_declarator"
1005            | "init_declarator"
1006            | "pointer_declarator"
1007            | "reference_declarator"
1008            | "array_declarator" => {
1009                // Declarator (with modifiers: Type* name; or Type name = init;)
1010                if let Some(name) = extract_field_name(child, content) {
1011                    field_names.push(name);
1012                }
1013            }
1014            _ => {}
1015        }
1016    }
1017
1018    // Resolve field type to FQN using namespace/type_map
1019    if let Some(ftype) = field_type {
1020        let namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
1021        let field_type_fqn = resolve_type_to_fqn(&ftype, &namespace, type_map);
1022
1023        // Store each field name with the same type
1024        for fname in field_names {
1025            field_types.insert((class_fqn.to_string(), fname), field_type_fqn.clone());
1026        }
1027    }
1028}
1029
1030/// Extract type name from a type node
1031fn extract_type_name(type_node: Node, content: &[u8]) -> String {
1032    match type_node.kind() {
1033        "type_identifier" | "primitive_type" => extract_identifier(type_node, content),
1034        "qualified_identifier" => {
1035            // For qualified types like std::vector, we want the full name
1036            extract_identifier(type_node, content)
1037        }
1038        "template_type" => {
1039            // For template types like vector<int>, extract the base type
1040            if let Some(name) = type_node.child_by_field_name("name") {
1041                extract_identifier(name, content)
1042            } else {
1043                extract_identifier(type_node, content)
1044            }
1045        }
1046        _ => {
1047            // For other cases, try to extract text directly
1048            extract_identifier(type_node, content)
1049        }
1050    }
1051}
1052
1053/// Extract field name from a declarator
1054fn extract_field_name(declarator: Node, content: &[u8]) -> Option<String> {
1055    match declarator.kind() {
1056        "field_declarator" => {
1057            // Recurse to find the actual name
1058            if let Some(declarator_inner) = declarator.child_by_field_name("declarator") {
1059                extract_field_name(declarator_inner, content)
1060            } else {
1061                Some(extract_identifier(declarator, content))
1062            }
1063        }
1064        "field_identifier" | "identifier" => Some(extract_identifier(declarator, content)),
1065        "pointer_declarator" | "reference_declarator" | "array_declarator" => {
1066            // For pointer/reference/array types, recurse to find the name
1067            if let Some(declarator_inner) = declarator.child_by_field_name("declarator") {
1068                extract_field_name(declarator_inner, content)
1069            } else {
1070                None
1071            }
1072        }
1073        "init_declarator" => {
1074            // For initialized fields, extract the declarator
1075            if let Some(declarator_inner) = declarator.child_by_field_name("declarator") {
1076                extract_field_name(declarator_inner, content)
1077            } else {
1078                None
1079            }
1080        }
1081        _ => None,
1082    }
1083}
1084
1085/// Resolve a simple type name to its FQN using namespace context and `type_map`
1086fn resolve_type_to_fqn(
1087    type_name: &str,
1088    namespace: &str,
1089    type_map: &HashMap<(String, String), String>,
1090) -> String {
1091    // If already qualified (contains ::), return as-is
1092    if type_name.contains("::") {
1093        return type_name.to_string();
1094    }
1095
1096    // Try to resolve using type_map with current namespace
1097    let namespace_key = namespace.trim_end_matches("::").to_string();
1098    if let Some(fqn) = type_map.get(&(namespace_key.clone(), type_name.to_string())) {
1099        return fqn.clone();
1100    }
1101
1102    // Try global namespace
1103    if let Some(fqn) = type_map.get(&(String::new(), type_name.to_string())) {
1104        return fqn.clone();
1105    }
1106
1107    // If no mapping found, return as-is
1108    type_name.to_string()
1109}
1110
1111/// Extract using directive (using namespace X;)
1112fn extract_using_directive(
1113    node: Node,
1114    content: &[u8],
1115    namespace_map: &HashMap<std::ops::Range<usize>, String>,
1116    _type_map: &mut HashMap<(String, String), String>,
1117) {
1118    // For now, we don't store using directives in type_map
1119    // because they affect all types in a namespace, not just specific ones
1120    // This is a simplification - full implementation would track these
1121    let _namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
1122
1123    // Extract the namespace being used
1124    if let Some(name_node) = node.child_by_field_name("name") {
1125        let _using_ns = extract_identifier(name_node, content);
1126        // Using directives (`using namespace std;`) import all names from a namespace,
1127        // requiring scoped directive tracking to resolve unqualified types. Using
1128        // declarations (`using std::vector;`) are handled by extract_using_declaration().
1129    }
1130}
1131
1132/// Extract using declaration (using `X::Y`;)
1133///
1134/// Maps simple names to their fully qualified names for type resolution.
1135/// Example: `using std::vector;` stores `("", "vector") -> "std::vector"`.
1136fn extract_using_declaration(
1137    node: Node,
1138    content: &[u8],
1139    namespace_map: &HashMap<std::ops::Range<usize>, String>,
1140    type_map: &mut HashMap<(String, String), String>,
1141) {
1142    let namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
1143    let namespace_key = namespace.trim_end_matches("::").to_string();
1144
1145    // Find the qualified_identifier child (tree-sitter-cpp doesn't expose a "name" field)
1146    let mut cursor = node.walk();
1147    for child in node.children(&mut cursor) {
1148        if child.kind() == "qualified_identifier" || child.kind() == "identifier" {
1149            let fqn = extract_identifier(child, content);
1150
1151            // Extract the simple name (last part after ::)
1152            if let Some(simple_name) = fqn.split("::").last() {
1153                // Store: (namespace_context, simple_name) -> fqn
1154                type_map.insert((namespace_key, simple_name.to_string()), fqn);
1155            }
1156            break;
1157        }
1158    }
1159}
1160
1161// ================================
1162// Call Resolution
1163// ================================
1164
1165/// Resolve a callee name to its fully qualified name using `ASTGraph` context.
1166///
1167/// This function handles:
1168/// - Simple names: "helper" -> "`demo::helper`" (using namespace context)
1169/// - Qualified names: "`Service::process`" -> "`demo::Service::process`" (adding namespace)
1170/// - Static method calls: "`Repository::save`" -> "`demo::Repository::save`"
1171/// - Member calls would require parsing the AST node further (future work)
1172fn resolve_callee_name(
1173    callee_name: &str,
1174    caller_ctx: &FunctionContext,
1175    _ast_graph: &ASTGraph,
1176) -> String {
1177    // If already fully qualified (starts with ::), return as-is
1178    if callee_name.starts_with("::") {
1179        return callee_name.trim_start_matches("::").to_string();
1180    }
1181
1182    // If contains ::, it might be partially qualified (e.g., "Service::process")
1183    if callee_name.contains("::") {
1184        // Add namespace prefix if not already qualified
1185        if !caller_ctx.namespace_stack.is_empty() {
1186            let namespace_prefix = caller_ctx.namespace_stack.join("::");
1187            return format!("{namespace_prefix}::{callee_name}");
1188        }
1189        return callee_name.to_string();
1190    }
1191
1192    // Simple name: build FQN from caller's namespace and class context
1193    let mut parts = Vec::new();
1194
1195    // Add namespace
1196    if !caller_ctx.namespace_stack.is_empty() {
1197        parts.extend(caller_ctx.namespace_stack.iter().cloned());
1198    }
1199
1200    // For simple names within a class, don't add class context automatically
1201    // (the call might be to a free function or static method from another class)
1202    // Future work: Parse the call expression to determine if it's a member call
1203
1204    // Add function name
1205    parts.push(callee_name.to_string());
1206
1207    parts.join("::")
1208}
1209
1210/// Strip type qualifiers (const, volatile, *, &) to extract the base type name.
1211/// Examples:
1212/// - "const int*" -> "int"
1213/// - "int const*" -> "int"  (postfix const)
1214/// - "`std::string`&" -> "string"
1215/// - "vector<int>" -> "vector"
1216fn strip_type_qualifiers(type_text: &str) -> String {
1217    let mut result = type_text.trim().to_string();
1218
1219    // Remove prefix qualifiers (with trailing space)
1220    result = result.replace("const ", "");
1221    result = result.replace("volatile ", "");
1222    result = result.replace("mutable ", "");
1223    result = result.replace("constexpr ", "");
1224
1225    // Remove postfix qualifiers (with leading space)
1226    result = result.replace(" const", "");
1227    result = result.replace(" volatile", "");
1228    result = result.replace(" mutable", "");
1229    result = result.replace(" constexpr", "");
1230
1231    // Remove pointer and reference markers
1232    result = result.replace(['*', '&'], "");
1233
1234    // Trim any extra whitespace
1235    result = result.trim().to_string();
1236
1237    // Extract the simple name from qualified names (std::string -> string)
1238    if let Some(last_part) = result.split("::").last() {
1239        result = last_part.to_string();
1240    }
1241
1242    // Extract base type from templates (vector<int> -> vector)
1243    if let Some(open_bracket) = result.find('<') {
1244        result = result[..open_bracket].to_string();
1245    }
1246
1247    result.trim().to_string()
1248}
1249
1250/// Process a field declaration inside a class/struct, creating Variable node with `TypeOf` and Reference edges.
1251#[allow(clippy::unnecessary_wraps)]
1252fn process_field_declaration(
1253    node: Node,
1254    content: &[u8],
1255    class_qualified_name: &str,
1256    visibility: &str,
1257    helper: &mut GraphBuildHelper,
1258) -> GraphResult<()> {
1259    // Extract type and field names from the field_declaration
1260    let mut field_type_text = None;
1261    let mut field_names = Vec::new();
1262
1263    let mut cursor = node.walk();
1264    for child in node.children(&mut cursor) {
1265        match child.kind() {
1266            "type_identifier" | "primitive_type" => {
1267                if let Ok(text) = child.utf8_text(content) {
1268                    field_type_text = Some(text.to_string());
1269                }
1270            }
1271            "qualified_identifier" => {
1272                // Handle qualified types like std::string
1273                if let Ok(text) = child.utf8_text(content) {
1274                    field_type_text = Some(text.to_string());
1275                }
1276            }
1277            "template_type" => {
1278                // Handle template types like std::vector<int>
1279                if let Ok(text) = child.utf8_text(content) {
1280                    field_type_text = Some(text.to_string());
1281                }
1282            }
1283            "sized_type_specifier" => {
1284                // Handle sized types like unsigned long, long long
1285                if let Ok(text) = child.utf8_text(content) {
1286                    field_type_text = Some(text.to_string());
1287                }
1288            }
1289            "type_qualifier" => {
1290                // Handle type qualifiers (const, volatile) - may need to combine with following type
1291                if field_type_text.is_none()
1292                    && let Ok(text) = child.utf8_text(content)
1293                {
1294                    field_type_text = Some(text.to_string());
1295                }
1296            }
1297            "auto" => {
1298                // Handle auto type deduction
1299                field_type_text = Some("auto".to_string());
1300            }
1301            "decltype" => {
1302                // Handle decltype(expr)
1303                if let Ok(text) = child.utf8_text(content) {
1304                    field_type_text = Some(text.to_string());
1305                }
1306            }
1307            "struct_specifier" | "class_specifier" | "enum_specifier" | "union_specifier" => {
1308                // Handle inline struct/class/enum/union declarations
1309                if let Ok(text) = child.utf8_text(content) {
1310                    field_type_text = Some(text.to_string());
1311                }
1312            }
1313            "field_identifier" => {
1314                if let Ok(name) = child.utf8_text(content) {
1315                    field_names.push(name.trim().to_string());
1316                }
1317            }
1318            "field_declarator"
1319            | "pointer_declarator"
1320            | "reference_declarator"
1321            | "init_declarator" => {
1322                // Recursively extract field name from declarators
1323                if let Some(name) = extract_field_name(child, content) {
1324                    field_names.push(name);
1325                }
1326            }
1327            _ => {}
1328        }
1329    }
1330
1331    // If we found a type and at least one field name, create the nodes and edges
1332    if let Some(type_text) = field_type_text {
1333        let base_type = strip_type_qualifiers(&type_text);
1334
1335        for field_name in field_names {
1336            let field_qualified = format!("{class_qualified_name}::{field_name}");
1337            let span = span_from_node(node);
1338
1339            // Create variable node with visibility
1340            let var_id = helper.add_node_with_visibility(
1341                &field_qualified,
1342                Some(span),
1343                sqry_core::graph::unified::node::NodeKind::Variable,
1344                Some(visibility),
1345            );
1346
1347            // Create a Type node for the base type (if not primitive)
1348            let type_id = helper.add_type(&base_type, None);
1349
1350            // Add TypeOf edge: variable -> type
1351            helper.add_typeof_edge(var_id, type_id);
1352
1353            // Add Reference edge: variable -> type
1354            helper.add_reference_edge(var_id, type_id);
1355        }
1356    }
1357
1358    Ok(())
1359}
1360
1361/// Process file-level variable declarations (global variables)
1362#[allow(clippy::unnecessary_wraps)]
1363fn process_global_variable_declaration(
1364    node: Node,
1365    content: &[u8],
1366    namespace_stack: &[String],
1367    helper: &mut GraphBuildHelper,
1368) -> GraphResult<()> {
1369    // Check if this is a declaration node (not a field_declaration, which is class-specific)
1370    if node.kind() != "declaration" {
1371        return Ok(());
1372    }
1373
1374    // Skip function declarations (they have function_declarator children)
1375    // These are handled separately via function_definition nodes
1376    let mut cursor_check = node.walk();
1377    for child in node.children(&mut cursor_check) {
1378        if child.kind() == "function_declarator" {
1379            return Ok(());
1380        }
1381    }
1382
1383    // Extract type and variable names
1384    let mut type_text = None;
1385    let mut var_names = Vec::new();
1386
1387    let mut cursor = node.walk();
1388    for child in node.children(&mut cursor) {
1389        match child.kind() {
1390            "type_identifier" | "primitive_type" | "qualified_identifier" | "template_type" => {
1391                if let Ok(text) = child.utf8_text(content) {
1392                    type_text = Some(text.to_string());
1393                }
1394            }
1395            "init_declarator" => {
1396                // Extract variable name from init_declarator
1397                if let Some(declarator) = child.child_by_field_name("declarator")
1398                    && let Some(name) = extract_declarator_name(declarator, content)
1399                {
1400                    var_names.push(name);
1401                }
1402            }
1403            "pointer_declarator" | "reference_declarator" => {
1404                if let Some(name) = extract_declarator_name(child, content) {
1405                    var_names.push(name);
1406                }
1407            }
1408            "identifier" => {
1409                // Direct identifier for simple declarations
1410                if let Ok(name) = child.utf8_text(content) {
1411                    var_names.push(name.to_string());
1412                }
1413            }
1414            _ => {}
1415        }
1416    }
1417
1418    if let Some(type_text) = type_text {
1419        let base_type = strip_type_qualifiers(&type_text);
1420
1421        for var_name in var_names {
1422            // Build qualified name with namespace
1423            let qualified = if namespace_stack.is_empty() {
1424                var_name.clone()
1425            } else {
1426                format!("{}::{}", namespace_stack.join("::"), var_name)
1427            };
1428
1429            let span = span_from_node(node);
1430
1431            // Create variable node (global variables are public by default)
1432            let var_id = helper.add_node_with_visibility(
1433                &qualified,
1434                Some(span),
1435                sqry_core::graph::unified::node::NodeKind::Variable,
1436                Some("public"),
1437            );
1438
1439            // Create Type node
1440            let type_id = helper.add_type(&base_type, None);
1441
1442            // Add TypeOf and Reference edges
1443            helper.add_typeof_edge(var_id, type_id);
1444            helper.add_reference_edge(var_id, type_id);
1445        }
1446    }
1447
1448    Ok(())
1449}
1450
1451/// Extract variable/parameter name from a declarator node
1452fn extract_declarator_name(node: Node, content: &[u8]) -> Option<String> {
1453    match node.kind() {
1454        "identifier" => {
1455            if let Ok(name) = node.utf8_text(content) {
1456                Some(name.to_string())
1457            } else {
1458                None
1459            }
1460        }
1461        "pointer_declarator" | "reference_declarator" | "array_declarator" => {
1462            // Recurse to find the actual name
1463            if let Some(inner) = node.child_by_field_name("declarator") {
1464                extract_declarator_name(inner, content)
1465            } else {
1466                // Try looking for identifier child directly
1467                let mut cursor = node.walk();
1468                for child in node.children(&mut cursor) {
1469                    if child.kind() == "identifier"
1470                        && let Ok(name) = child.utf8_text(content)
1471                    {
1472                        return Some(name.to_string());
1473                    }
1474                }
1475                None
1476            }
1477        }
1478        "init_declarator" => {
1479            // Extract from the declarator field
1480            if let Some(inner) = node.child_by_field_name("declarator") {
1481                extract_declarator_name(inner, content)
1482            } else {
1483                None
1484            }
1485        }
1486        "field_declarator" => {
1487            // Recurse to find the actual name
1488            if let Some(inner) = node.child_by_field_name("declarator") {
1489                extract_declarator_name(inner, content)
1490            } else {
1491                // Try to extract directly
1492                if let Ok(name) = node.utf8_text(content) {
1493                    Some(name.to_string())
1494                } else {
1495                    None
1496                }
1497            }
1498        }
1499        _ => None,
1500    }
1501}
1502
1503/// Walk a class/struct body, processing field declarations and methods with visibility tracking.
1504#[allow(clippy::too_many_arguments)]
1505fn walk_class_body(
1506    body_node: Node,
1507    content: &[u8],
1508    class_qualified_name: &str,
1509    is_struct: bool,
1510    ast_graph: &ASTGraph,
1511    helper: &mut GraphBuildHelper,
1512    seen_includes: &mut HashSet<String>,
1513    namespace_stack: &mut Vec<String>,
1514    class_stack: &mut Vec<String>,
1515    ffi_registry: &FfiRegistry,
1516    pure_virtual_registry: &PureVirtualRegistry,
1517    budget: &mut BuildBudget,
1518) -> GraphResult<()> {
1519    // Default visibility: struct = public, class = private
1520    let mut current_visibility = if is_struct { "public" } else { "private" };
1521
1522    let mut cursor = body_node.walk();
1523    for child in body_node.children(&mut cursor) {
1524        budget.checkpoint("cpp:walk_class_body")?;
1525        match child.kind() {
1526            "access_specifier" => {
1527                // Update current visibility (public:, private:, protected:)
1528                if let Ok(text) = child.utf8_text(content) {
1529                    let spec = text.trim().trim_end_matches(':').trim();
1530                    current_visibility = spec;
1531                }
1532            }
1533            "field_declaration" => {
1534                // Process field with current visibility
1535                process_field_declaration(
1536                    child,
1537                    content,
1538                    class_qualified_name,
1539                    current_visibility,
1540                    helper,
1541                )?;
1542            }
1543            "function_definition" => {
1544                // Process method with current visibility
1545                // Extract function context from AST graph by matching start position
1546                if let Some(context) = ast_graph.context_for_start(child.start_byte()) {
1547                    let span = span_from_node(child);
1548                    helper.add_method_with_signature(
1549                        &context.qualified_name,
1550                        Some(span),
1551                        false, // C++ doesn't have async
1552                        context.is_static,
1553                        Some(current_visibility),
1554                        context.return_type.as_deref(),
1555                    );
1556                }
1557                // Recurse into function body to process call expressions
1558                walk_tree_for_graph(
1559                    child,
1560                    content,
1561                    ast_graph,
1562                    helper,
1563                    seen_includes,
1564                    namespace_stack,
1565                    class_stack,
1566                    ffi_registry,
1567                    pure_virtual_registry,
1568                    budget,
1569                )?;
1570            }
1571            _ => {
1572                // Recurse into other nodes (nested classes, etc.)
1573                walk_tree_for_graph(
1574                    child,
1575                    content,
1576                    ast_graph,
1577                    helper,
1578                    seen_includes,
1579                    namespace_stack,
1580                    class_stack,
1581                    ffi_registry,
1582                    pure_virtual_registry,
1583                    budget,
1584                )?;
1585            }
1586        }
1587    }
1588
1589    Ok(())
1590}
1591
1592/// Walk the tree and populate the staging graph.
1593#[allow(clippy::too_many_arguments)]
1594#[allow(clippy::too_many_lines)] // Central traversal; refactor after C++ AST stabilizes.
1595fn walk_tree_for_graph(
1596    node: Node,
1597    content: &[u8],
1598    ast_graph: &ASTGraph,
1599    helper: &mut GraphBuildHelper,
1600    seen_includes: &mut HashSet<String>,
1601    namespace_stack: &mut Vec<String>,
1602    class_stack: &mut Vec<String>,
1603    ffi_registry: &FfiRegistry,
1604    pure_virtual_registry: &PureVirtualRegistry,
1605    budget: &mut BuildBudget,
1606) -> GraphResult<()> {
1607    budget.checkpoint("cpp:walk_tree_for_graph")?;
1608    match node.kind() {
1609        "preproc_include" => {
1610            // Handle #include directives - create Import edges
1611            build_import_edge(node, content, helper, seen_includes)?;
1612        }
1613        "linkage_specification" => {
1614            // Handle extern "C" blocks - create FFI function nodes
1615            build_ffi_block_for_staging(node, content, helper, namespace_stack);
1616        }
1617        "namespace_definition" => {
1618            // Extract namespace name and track context
1619            if let Some(name_node) = node.child_by_field_name("name")
1620                && let Ok(ns_name) = name_node.utf8_text(content)
1621            {
1622                namespace_stack.push(ns_name.trim().to_string());
1623
1624                // Recurse into namespace body
1625                let mut cursor = node.walk();
1626                for child in node.children(&mut cursor) {
1627                    walk_tree_for_graph(
1628                        child,
1629                        content,
1630                        ast_graph,
1631                        helper,
1632                        seen_includes,
1633                        namespace_stack,
1634                        class_stack,
1635                        ffi_registry,
1636                        pure_virtual_registry,
1637                        budget,
1638                    )?;
1639                }
1640
1641                namespace_stack.pop();
1642                return Ok(());
1643            }
1644        }
1645        "class_specifier" | "struct_specifier" => {
1646            // Extract class/struct name
1647            if let Some(name_node) = node.child_by_field_name("name")
1648                && let Ok(class_name) = name_node.utf8_text(content)
1649            {
1650                let class_name = class_name.trim();
1651                let span = span_from_node(node);
1652                let is_struct = node.kind() == "struct_specifier";
1653
1654                // Build qualified class name
1655                let qualified_class =
1656                    build_qualified_name(namespace_stack, class_stack, class_name);
1657
1658                // Add class/struct node with qualified name
1659                let visibility = "public";
1660                let class_id = if is_struct {
1661                    helper.add_struct_with_visibility(
1662                        &qualified_class,
1663                        Some(span),
1664                        Some(visibility),
1665                    )
1666                } else {
1667                    helper.add_class_with_visibility(&qualified_class, Some(span), Some(visibility))
1668                };
1669
1670                // Handle inheritance with qualified name
1671                // Also check for Implements edges (inheriting from pure virtual interfaces)
1672                build_inheritance_and_implements_edges(
1673                    node,
1674                    content,
1675                    &qualified_class,
1676                    class_id,
1677                    helper,
1678                    namespace_stack,
1679                    pure_virtual_registry,
1680                )?;
1681
1682                // Export classes/structs at file/namespace scope (not nested classes)
1683                // Nested classes have internal linkage unless explicitly exported
1684                if class_stack.is_empty() {
1685                    let module_id = helper.add_module(FILE_MODULE_NAME, None);
1686                    helper.add_export_edge(module_id, class_id);
1687                }
1688
1689                // Track class context for nested classes
1690                class_stack.push(class_name.to_string());
1691
1692                // Process class body with visibility tracking
1693                // Default visibility: struct = public, class = private
1694                if let Some(body) = node.child_by_field_name("body") {
1695                    walk_class_body(
1696                        body,
1697                        content,
1698                        &qualified_class,
1699                        is_struct,
1700                        ast_graph,
1701                        helper,
1702                        seen_includes,
1703                        namespace_stack,
1704                        class_stack,
1705                        ffi_registry,
1706                        pure_virtual_registry,
1707                        budget,
1708                    )?;
1709                }
1710
1711                class_stack.pop();
1712                return Ok(());
1713            }
1714        }
1715        "enum_specifier" => {
1716            if let Some(name_node) = node.child_by_field_name("name")
1717                && let Ok(enum_name) = name_node.utf8_text(content)
1718            {
1719                let enum_name = enum_name.trim();
1720                let span = span_from_node(node);
1721                let qualified_enum = build_qualified_name(namespace_stack, class_stack, enum_name);
1722                let enum_id = helper.add_enum(&qualified_enum, Some(span));
1723
1724                if class_stack.is_empty() {
1725                    let module_id = helper.add_module(FILE_MODULE_NAME, None);
1726                    helper.add_export_edge(module_id, enum_id);
1727                }
1728            }
1729        }
1730        "function_definition" => {
1731            // Skip if we're inside a class body - methods are handled by walk_class_body
1732            // to ensure correct visibility tracking. This check prevents double-adding
1733            // methods with incorrect visibility.
1734            if !class_stack.is_empty() {
1735                // Don't process the function definition as a node here, but do recurse
1736                // into its body to find call expressions
1737                let mut cursor = node.walk();
1738                for child in node.children(&mut cursor) {
1739                    walk_tree_for_graph(
1740                        child,
1741                        content,
1742                        ast_graph,
1743                        helper,
1744                        seen_includes,
1745                        namespace_stack,
1746                        class_stack,
1747                        ffi_registry,
1748                        pure_virtual_registry,
1749                        budget,
1750                    )?;
1751                }
1752                return Ok(());
1753            }
1754
1755            // Extract function context from AST graph by matching start position
1756            if let Some(context) = ast_graph.context_for_start(node.start_byte()) {
1757                let span = span_from_node(node);
1758
1759                // Determine if this is a method or free function based on context
1760                if context.class_stack.is_empty() {
1761                    // This is a free function
1762                    // Visibility: static = private (internal linkage), non-static = public (external linkage)
1763                    let visibility = if context.is_static {
1764                        "private"
1765                    } else {
1766                        "public"
1767                    };
1768                    let fn_id = helper.add_function_with_signature(
1769                        &context.qualified_name,
1770                        Some(span),
1771                        false, // C++ doesn't have async
1772                        false, // C++ doesn't use unsafe keyword
1773                        Some(visibility),
1774                        context.return_type.as_deref(),
1775                    );
1776
1777                    // Export non-static free functions (static functions have internal linkage)
1778                    if !context.is_static {
1779                        let module_id = helper.add_module(FILE_MODULE_NAME, None);
1780                        helper.add_export_edge(module_id, fn_id);
1781                    }
1782                } else {
1783                    // This is an out-of-class method definition (e.g., Resource::Resource())
1784                    // These are public by default in C++ (they must be declared in the class first)
1785                    // Note: We can't determine actual visibility here as that requires
1786                    // correlating with the in-class declaration
1787                    helper.add_method_with_signature(
1788                        &context.qualified_name,
1789                        Some(span),
1790                        false, // C++ doesn't have async
1791                        context.is_static,
1792                        Some("public"), // Default for out-of-class definitions
1793                        context.return_type.as_deref(),
1794                    );
1795                }
1796            }
1797        }
1798        "call_expression" => {
1799            // Build call edge
1800            if let Ok(Some((caller_qname, callee_qname, argument_count, span))) =
1801                build_call_for_staging(ast_graph, node, content)
1802            {
1803                // Ensure caller node exists
1804                let caller_function_id = helper.ensure_function(&caller_qname, None, false, false);
1805                let argument_count = u8::try_from(argument_count).unwrap_or(u8::MAX);
1806
1807                // Check if the callee is a known FFI function
1808                // Only do FFI lookup for unqualified calls (no ::)
1809                let is_unqualified = !callee_qname.contains("::");
1810                if is_unqualified {
1811                    if let Some((ffi_qualified, ffi_convention)) = ffi_registry.get(&callee_qname) {
1812                        // This is a call to an FFI function - create FfiCall edge
1813                        let ffi_target_id =
1814                            helper.ensure_function(ffi_qualified, None, false, true);
1815                        helper.add_ffi_edge(caller_function_id, ffi_target_id, *ffi_convention);
1816                    } else {
1817                        // Regular call - create normal Call edge
1818                        let target_function_id =
1819                            helper.ensure_function(&callee_qname, None, false, false);
1820                        helper.add_call_edge_full_with_span(
1821                            caller_function_id,
1822                            target_function_id,
1823                            argument_count,
1824                            false,
1825                            vec![span],
1826                        );
1827                    }
1828                } else {
1829                    // Qualified call - create normal Call edge
1830                    let target_function_id =
1831                        helper.ensure_function(&callee_qname, None, false, false);
1832                    helper.add_call_edge_full_with_span(
1833                        caller_function_id,
1834                        target_function_id,
1835                        argument_count,
1836                        false,
1837                        vec![span],
1838                    );
1839                }
1840            }
1841        }
1842        "declaration" => {
1843            // Handle global/file-level variable declarations (not inside classes)
1844            // Only process if we're not inside a class (class members are handled in walk_class_body)
1845            if class_stack.is_empty() {
1846                process_global_variable_declaration(node, content, namespace_stack, helper)?;
1847            }
1848        }
1849        _ => {}
1850    }
1851
1852    // Recurse into children
1853    let mut cursor = node.walk();
1854    for child in node.children(&mut cursor) {
1855        walk_tree_for_graph(
1856            child,
1857            content,
1858            ast_graph,
1859            helper,
1860            seen_includes,
1861            namespace_stack,
1862            class_stack,
1863            ffi_registry,
1864            pure_virtual_registry,
1865            budget,
1866        )?;
1867    }
1868
1869    Ok(())
1870}
1871
1872/// Build call edge information for the staging graph.
1873fn build_call_for_staging(
1874    ast_graph: &ASTGraph,
1875    call_node: Node<'_>,
1876    content: &[u8],
1877) -> GraphResult<Option<(String, String, usize, Span)>> {
1878    // Find the enclosing function context
1879    let call_context = ast_graph.find_enclosing(call_node.start_byte());
1880    let caller_qualified_name = if let Some(ctx) = call_context {
1881        ctx.qualified_name.clone()
1882    } else {
1883        // Top-level call (e.g., global initializer)
1884        return Ok(None);
1885    };
1886
1887    let Some(function_node) = call_node.child_by_field_name("function") else {
1888        return Ok(None);
1889    };
1890
1891    let callee_text = function_node
1892        .utf8_text(content)
1893        .map_err(|_| GraphBuilderError::ParseError {
1894            span: span_from_node(call_node),
1895            reason: "failed to read call expression".to_string(),
1896        })?
1897        .trim();
1898
1899    if callee_text.is_empty() {
1900        return Ok(None);
1901    }
1902
1903    // Resolve callee name using context
1904    let target_qualified_name = if let Some(ctx) = call_context {
1905        resolve_callee_name(callee_text, ctx, ast_graph)
1906    } else {
1907        callee_text.to_string()
1908    };
1909
1910    let span = span_from_node(call_node);
1911    let argument_count = count_arguments(call_node);
1912
1913    Ok(Some((
1914        caller_qualified_name,
1915        target_qualified_name,
1916        argument_count,
1917        span,
1918    )))
1919}
1920
1921/// Build import edge for `#include` directives.
1922///
1923/// Handles both system includes (`<header>`) and local includes (`"header"`).
1924/// Per the implementation plan, include type (system/local) is tracked via
1925/// node metadata, not the edge's alias field (alias is for import renaming only).
1926/// Duplicate includes are deduplicated using the `seen_includes` set.
1927fn build_import_edge(
1928    include_node: Node<'_>,
1929    content: &[u8],
1930    helper: &mut GraphBuildHelper,
1931    seen_includes: &mut HashSet<String>,
1932) -> GraphResult<()> {
1933    // Look for path child (system_lib_string or string_literal)
1934    let path_node = include_node.child_by_field_name("path").or_else(|| {
1935        // Fallback: find first child that looks like a path
1936        let mut cursor = include_node.walk();
1937        include_node.children(&mut cursor).find(|child| {
1938            matches!(
1939                child.kind(),
1940                "system_lib_string" | "string_literal" | "string_content"
1941            )
1942        })
1943    });
1944
1945    let Some(path_node) = path_node else {
1946        return Ok(());
1947    };
1948
1949    let include_path = path_node
1950        .utf8_text(content)
1951        .map_err(|_| GraphBuilderError::ParseError {
1952            span: span_from_node(include_node),
1953            reason: "failed to read include path".to_string(),
1954        })?
1955        .trim();
1956
1957    if include_path.is_empty() {
1958        return Ok(());
1959    }
1960
1961    // Determine include type and clean up path
1962    let is_system_include = include_path.starts_with('<') && include_path.ends_with('>');
1963    let cleaned_path = if is_system_include {
1964        // System include: <iostream> -> iostream
1965        include_path.trim_start_matches('<').trim_end_matches('>')
1966    } else {
1967        // Local include: "myheader.hpp" -> myheader.hpp
1968        include_path.trim_start_matches('"').trim_end_matches('"')
1969    };
1970
1971    if cleaned_path.is_empty() {
1972        return Ok(());
1973    }
1974
1975    // Deduplicate includes - only add if not seen before
1976    if !seen_includes.insert(cleaned_path.to_string()) {
1977        return Ok(()); // Already seen this include
1978    }
1979
1980    // Create module node for the file being compiled (importer)
1981    let file_module_id = helper.add_module("<file>", None);
1982
1983    // Create import node for the included header
1984    let span = span_from_node(include_node);
1985    let import_id = helper.add_import(cleaned_path, Some(span));
1986
1987    // Add import edge - no alias for #include (alias is for renaming, which C++ doesn't support)
1988    // is_wildcard is false since #include brings in the whole header (but it's not a wildcard import)
1989    helper.add_import_edge(file_module_id, import_id);
1990
1991    Ok(())
1992}
1993
1994// ================================
1995// FFI Support Functions
1996// ================================
1997
1998/// Collect FFI declarations from extern "C" blocks (Pass 1).
1999///
2000/// This function walks the entire AST to find all `extern "C" { ... }` blocks
2001/// and populates the FFI registry with function name → (qualified name, convention)
2002/// mappings. This must be done before processing calls so that FFI calls can be
2003/// properly linked regardless of source code order.
2004fn collect_ffi_declarations(
2005    node: Node<'_>,
2006    content: &[u8],
2007    ffi_registry: &mut FfiRegistry,
2008    budget: &mut BuildBudget,
2009) -> GraphResult<()> {
2010    budget.checkpoint("cpp:collect_ffi_declarations")?;
2011    if node.kind() == "linkage_specification" {
2012        // Get the ABI string (e.g., "C")
2013        let abi = extract_ffi_abi(node, content);
2014        let convention = abi_to_convention(&abi);
2015
2016        // Find the body child (declaration_list or single declaration)
2017        if let Some(body_node) = node.child_by_field_name("body") {
2018            collect_ffi_from_body(body_node, content, &abi, convention, ffi_registry);
2019        }
2020    }
2021
2022    // Recurse into children
2023    let mut cursor = node.walk();
2024    for child in node.children(&mut cursor) {
2025        collect_ffi_declarations(child, content, ffi_registry, budget)?;
2026    }
2027
2028    Ok(())
2029}
2030
2031/// Collect FFI declarations from a linkage specification body.
2032fn collect_ffi_from_body(
2033    body_node: Node<'_>,
2034    content: &[u8],
2035    abi: &str,
2036    convention: FfiConvention,
2037    ffi_registry: &mut FfiRegistry,
2038) {
2039    match body_node.kind() {
2040        "declaration_list" => {
2041            // Multiple declarations in the block
2042            let mut cursor = body_node.walk();
2043            for decl in body_node.children(&mut cursor) {
2044                if decl.kind() == "declaration"
2045                    && let Some(fn_name) = extract_ffi_function_name(decl, content)
2046                {
2047                    let qualified = format!("extern::{abi}::{fn_name}");
2048                    ffi_registry.insert(fn_name, (qualified, convention));
2049                }
2050            }
2051        }
2052        "declaration" => {
2053            // Single declaration (e.g., extern "C" void foo();)
2054            if let Some(fn_name) = extract_ffi_function_name(body_node, content) {
2055                let qualified = format!("extern::{abi}::{fn_name}");
2056                ffi_registry.insert(fn_name, (qualified, convention));
2057            }
2058        }
2059        _ => {}
2060    }
2061}
2062
2063/// Extract function name from an FFI declaration.
2064fn extract_ffi_function_name(decl_node: Node<'_>, content: &[u8]) -> Option<String> {
2065    // Look for declarator field which contains the function declarator
2066    if let Some(declarator_node) = decl_node.child_by_field_name("declarator") {
2067        return extract_function_name_from_declarator(declarator_node, content);
2068    }
2069    None
2070}
2071
2072/// Recursively extract function name from a declarator node.
2073fn extract_function_name_from_declarator(node: Node<'_>, content: &[u8]) -> Option<String> {
2074    match node.kind() {
2075        "function_declarator" => {
2076            // Function declarator has a nested declarator with the name
2077            if let Some(inner) = node.child_by_field_name("declarator") {
2078                return extract_function_name_from_declarator(inner, content);
2079            }
2080        }
2081        "identifier" => {
2082            // Found the name
2083            if let Ok(name) = node.utf8_text(content) {
2084                let name = name.trim();
2085                if !name.is_empty() {
2086                    return Some(name.to_string());
2087                }
2088            }
2089        }
2090        "pointer_declarator" | "reference_declarator" => {
2091            // Handle pointer/reference declarators (e.g., int* (*foo)())
2092            if let Some(inner) = node.child_by_field_name("declarator") {
2093                return extract_function_name_from_declarator(inner, content);
2094            }
2095        }
2096        "parenthesized_declarator" => {
2097            // Handle parenthesized declarators
2098            let mut cursor = node.walk();
2099            for child in node.children(&mut cursor) {
2100                if let Some(name) = extract_function_name_from_declarator(child, content) {
2101                    return Some(name);
2102                }
2103            }
2104        }
2105        _ => {}
2106    }
2107    None
2108}
2109
2110/// Extract the ABI string from an extern "X" block.
2111///
2112/// Returns the ABI string (e.g., "C") or "C" as default.
2113fn extract_ffi_abi(node: Node<'_>, content: &[u8]) -> String {
2114    // Look for the "value" field which contains the string literal
2115    if let Some(value_node) = node.child_by_field_name("value")
2116        && value_node.kind() == "string_literal"
2117    {
2118        // Look for string_content child
2119        let mut cursor = value_node.walk();
2120        for child in value_node.children(&mut cursor) {
2121            if child.kind() == "string_content"
2122                && let Ok(text) = child.utf8_text(content)
2123            {
2124                let trimmed = text.trim();
2125                if !trimmed.is_empty() {
2126                    return trimmed.to_string();
2127                }
2128            }
2129        }
2130    }
2131    // Default to "C" if no ABI specified
2132    "C".to_string()
2133}
2134
2135/// Convert an ABI string to an FFI calling convention.
2136fn abi_to_convention(abi: &str) -> FfiConvention {
2137    match abi.to_lowercase().as_str() {
2138        "system" => FfiConvention::System,
2139        "stdcall" => FfiConvention::Stdcall,
2140        "fastcall" => FfiConvention::Fastcall,
2141        "cdecl" => FfiConvention::Cdecl,
2142        _ => FfiConvention::C, // Default to C
2143    }
2144}
2145
2146/// Build FFI function declarations from extern "C" blocks.
2147///
2148/// Creates Function nodes for FFI declarations with unsafe=true.
2149fn build_ffi_block_for_staging(
2150    node: Node<'_>,
2151    content: &[u8],
2152    helper: &mut GraphBuildHelper,
2153    namespace_stack: &[String],
2154) {
2155    // Get the ABI string
2156    let abi = extract_ffi_abi(node, content);
2157
2158    // Find the body child
2159    if let Some(body_node) = node.child_by_field_name("body") {
2160        build_ffi_from_body(body_node, content, &abi, helper, namespace_stack);
2161    }
2162}
2163
2164/// Build FFI function nodes from a linkage specification body.
2165fn build_ffi_from_body(
2166    body_node: Node<'_>,
2167    content: &[u8],
2168    abi: &str,
2169    helper: &mut GraphBuildHelper,
2170    namespace_stack: &[String],
2171) {
2172    match body_node.kind() {
2173        "declaration_list" => {
2174            // Multiple declarations in the block
2175            let mut cursor = body_node.walk();
2176            for decl in body_node.children(&mut cursor) {
2177                if decl.kind() == "declaration"
2178                    && let Some(fn_name) = extract_ffi_function_name(decl, content)
2179                {
2180                    let span = span_from_node(decl);
2181                    // Build qualified name with namespace context
2182                    let qualified = if namespace_stack.is_empty() {
2183                        format!("extern::{abi}::{fn_name}")
2184                    } else {
2185                        format!("{}::extern::{abi}::{fn_name}", namespace_stack.join("::"))
2186                    };
2187                    // Add as unsafe function (FFI functions are inherently unsafe)
2188                    helper.add_function(
2189                        &qualified,
2190                        Some(span),
2191                        false, // not async
2192                        true,  // unsafe (FFI)
2193                    );
2194                }
2195            }
2196        }
2197        "declaration" => {
2198            // Single declaration
2199            if let Some(fn_name) = extract_ffi_function_name(body_node, content) {
2200                let span = span_from_node(body_node);
2201                let qualified = if namespace_stack.is_empty() {
2202                    format!("extern::{abi}::{fn_name}")
2203                } else {
2204                    format!("{}::extern::{abi}::{fn_name}", namespace_stack.join("::"))
2205                };
2206                helper.add_function(&qualified, Some(span), false, true);
2207            }
2208        }
2209        _ => {}
2210    }
2211}
2212
2213// ================================
2214// Pure Virtual Interface Support
2215// ================================
2216
2217/// Collect pure virtual interfaces (abstract classes with pure virtual methods).
2218///
2219/// A class is considered a "pure virtual interface" if it contains at least one
2220/// pure virtual method (declared with `= 0`). Classes that inherit from such
2221/// interfaces will get Implements edges instead of just Inherits edges.
2222fn collect_pure_virtual_interfaces(
2223    node: Node<'_>,
2224    content: &[u8],
2225    registry: &mut PureVirtualRegistry,
2226    budget: &mut BuildBudget,
2227) -> GraphResult<()> {
2228    budget.checkpoint("cpp:collect_pure_virtual_interfaces")?;
2229    if matches!(node.kind(), "class_specifier" | "struct_specifier")
2230        && let Some(name_node) = node.child_by_field_name("name")
2231        && let Ok(class_name) = name_node.utf8_text(content)
2232    {
2233        let class_name = class_name.trim();
2234        if !class_name.is_empty() && has_pure_virtual_methods(node, content) {
2235            registry.insert(class_name.to_string());
2236        }
2237    }
2238
2239    // Recurse into children
2240    let mut cursor = node.walk();
2241    for child in node.children(&mut cursor) {
2242        collect_pure_virtual_interfaces(child, content, registry, budget)?;
2243    }
2244
2245    Ok(())
2246}
2247
2248/// Check if a class/struct has any pure virtual methods.
2249///
2250/// Pure virtual methods are declared as `virtual ReturnType name() = 0;`
2251fn has_pure_virtual_methods(class_node: Node<'_>, content: &[u8]) -> bool {
2252    if let Some(body) = class_node.child_by_field_name("body") {
2253        let mut cursor = body.walk();
2254        for child in body.children(&mut cursor) {
2255            // Look for field_declaration with virtual and = 0
2256            if child.kind() == "field_declaration" && is_pure_virtual_declaration(child, content) {
2257                return true;
2258            }
2259        }
2260    }
2261    false
2262}
2263
2264/// Check if a field declaration is a pure virtual method (has `virtual` and `= 0`).
2265fn is_pure_virtual_declaration(decl_node: Node<'_>, content: &[u8]) -> bool {
2266    let mut has_virtual = false;
2267    let mut has_pure_specifier = false;
2268
2269    // Check children for virtual keyword and default_value of 0
2270    let mut cursor = decl_node.walk();
2271    for child in decl_node.children(&mut cursor) {
2272        match child.kind() {
2273            "virtual" => {
2274                has_virtual = true;
2275            }
2276            "number_literal" => {
2277                // Check if this is the pure virtual specifier (= 0)
2278                // The number_literal with value "0" after "=" indicates a pure virtual method
2279                if let Ok(text) = child.utf8_text(content)
2280                    && text.trim() == "0"
2281                {
2282                    has_pure_specifier = true;
2283                }
2284            }
2285            _ => {}
2286        }
2287    }
2288
2289    has_virtual && has_pure_specifier
2290}
2291
2292/// Build inheritance and implements edges for a class/struct.
2293///
2294/// For each base class:
2295/// - If the base class is a pure virtual interface, create an Implements edge
2296/// - Otherwise, create an Inherits edge
2297fn build_inheritance_and_implements_edges(
2298    class_node: Node<'_>,
2299    content: &[u8],
2300    _qualified_class_name: &str,
2301    child_id: sqry_core::graph::unified::node::NodeId,
2302    helper: &mut GraphBuildHelper,
2303    namespace_stack: &[String],
2304    pure_virtual_registry: &PureVirtualRegistry,
2305) -> GraphResult<()> {
2306    // Look for base_class_clause child
2307    let mut cursor = class_node.walk();
2308    let base_clause = class_node
2309        .children(&mut cursor)
2310        .find(|child| child.kind() == "base_class_clause");
2311
2312    let Some(base_clause) = base_clause else {
2313        return Ok(()); // No inheritance
2314    };
2315
2316    // Parse all base classes from the base_class_clause
2317    let mut clause_cursor = base_clause.walk();
2318    for child in base_clause.children(&mut clause_cursor) {
2319        match child.kind() {
2320            "type_identifier" => {
2321                let base_name = child
2322                    .utf8_text(content)
2323                    .map_err(|_| GraphBuilderError::ParseError {
2324                        span: span_from_node(child),
2325                        reason: "failed to read base class name".to_string(),
2326                    })?
2327                    .trim();
2328
2329                if !base_name.is_empty() {
2330                    // Qualify with namespace if present
2331                    let qualified_base = if namespace_stack.is_empty() {
2332                        base_name.to_string()
2333                    } else {
2334                        format!("{}::{}", namespace_stack.join("::"), base_name)
2335                    };
2336
2337                    // Check if base is a pure virtual interface
2338                    if pure_virtual_registry.contains(base_name) {
2339                        // Create interface node and Implements edge
2340                        let interface_id = helper.add_interface(&qualified_base, None);
2341                        helper.add_implements_edge(child_id, interface_id);
2342                    } else {
2343                        // Regular inheritance - create Inherits edge
2344                        let parent_id = helper.add_class(&qualified_base, None);
2345                        helper.add_inherits_edge(child_id, parent_id);
2346                    }
2347                }
2348            }
2349            "qualified_identifier" => {
2350                // Already qualified - use as-is
2351                let base_name = child
2352                    .utf8_text(content)
2353                    .map_err(|_| GraphBuilderError::ParseError {
2354                        span: span_from_node(child),
2355                        reason: "failed to read base class name".to_string(),
2356                    })?
2357                    .trim();
2358
2359                if !base_name.is_empty() {
2360                    // Extract simple name for registry lookup
2361                    let simple_name = base_name.rsplit("::").next().unwrap_or(base_name);
2362
2363                    if pure_virtual_registry.contains(simple_name) {
2364                        let interface_id = helper.add_interface(base_name, None);
2365                        helper.add_implements_edge(child_id, interface_id);
2366                    } else {
2367                        let parent_id = helper.add_class(base_name, None);
2368                        helper.add_inherits_edge(child_id, parent_id);
2369                    }
2370                }
2371            }
2372            "template_type" => {
2373                // Template base class: Base<T>
2374                if let Some(template_name_node) = child.child_by_field_name("name")
2375                    && let Ok(base_name) = template_name_node.utf8_text(content)
2376                {
2377                    let base_name = base_name.trim();
2378                    if !base_name.is_empty() {
2379                        let qualified_base =
2380                            if base_name.contains("::") || namespace_stack.is_empty() {
2381                                base_name.to_string()
2382                            } else {
2383                                format!("{}::{}", namespace_stack.join("::"), base_name)
2384                            };
2385
2386                        // Template bases are typically not pure virtual interfaces
2387                        // but check anyway
2388                        if pure_virtual_registry.contains(base_name) {
2389                            let interface_id = helper.add_interface(&qualified_base, None);
2390                            helper.add_implements_edge(child_id, interface_id);
2391                        } else {
2392                            let parent_id = helper.add_class(&qualified_base, None);
2393                            helper.add_inherits_edge(child_id, parent_id);
2394                        }
2395                    }
2396                }
2397            }
2398            _ => {
2399                // Skip access specifiers, colons, commas, and other non-base nodes.
2400            }
2401        }
2402    }
2403
2404    Ok(())
2405}
2406
2407fn span_from_node(node: Node<'_>) -> Span {
2408    let start = node.start_position();
2409    let end = node.end_position();
2410    Span::new(
2411        sqry_core::graph::node::Position::new(start.row, start.column),
2412        sqry_core::graph::node::Position::new(end.row, end.column),
2413    )
2414}
2415
2416fn count_arguments(node: Node<'_>) -> usize {
2417    node.child_by_field_name("arguments").map_or(0, |args| {
2418        let mut count = 0;
2419        let mut cursor = args.walk();
2420        for child in args.children(&mut cursor) {
2421            if !matches!(child.kind(), "(" | ")" | ",") {
2422                count += 1;
2423            }
2424        }
2425        count
2426    })
2427}
2428
2429#[cfg(test)]
2430mod tests {
2431    use super::*;
2432    use sqry_core::graph::unified::build::test_helpers::{
2433        assert_has_node, assert_has_node_with_kind, collect_call_edges,
2434    };
2435    use sqry_core::graph::unified::node::NodeKind;
2436    use tree_sitter::Parser;
2437
2438    fn parse_cpp(source: &str) -> Tree {
2439        let mut parser = Parser::new();
2440        parser
2441            .set_language(&tree_sitter_cpp::LANGUAGE.into())
2442            .expect("Failed to set Cpp language");
2443        parser
2444            .parse(source.as_bytes(), None)
2445            .expect("Failed to parse Cpp source")
2446    }
2447
2448    fn test_budget() -> BuildBudget {
2449        BuildBudget::new(Path::new("test.cpp"))
2450    }
2451
2452    fn extract_namespace_map_for_test(
2453        tree: &Tree,
2454        source: &str,
2455    ) -> HashMap<std::ops::Range<usize>, String> {
2456        let mut budget = test_budget();
2457        extract_namespace_map(tree.root_node(), source.as_bytes(), &mut budget)
2458            .expect("namespace extraction should succeed in tests")
2459    }
2460
2461    fn extract_cpp_contexts_for_test(
2462        tree: &Tree,
2463        source: &str,
2464        namespace_map: &HashMap<std::ops::Range<usize>, String>,
2465    ) -> Vec<FunctionContext> {
2466        let mut budget = test_budget();
2467        extract_cpp_contexts(
2468            tree.root_node(),
2469            source.as_bytes(),
2470            namespace_map,
2471            &mut budget,
2472        )
2473        .expect("context extraction should succeed in tests")
2474    }
2475
2476    fn extract_field_and_type_info_for_test(
2477        tree: &Tree,
2478        source: &str,
2479        namespace_map: &HashMap<std::ops::Range<usize>, String>,
2480    ) -> (QualifiedNameMap, QualifiedNameMap) {
2481        let mut budget = test_budget();
2482        extract_field_and_type_info(
2483            tree.root_node(),
2484            source.as_bytes(),
2485            namespace_map,
2486            &mut budget,
2487        )
2488        .expect("field/type extraction should succeed in tests")
2489    }
2490
2491    #[test]
2492    fn test_build_graph_times_out_with_expired_budget() {
2493        let source = r"
2494            namespace demo {
2495                class Service {
2496                public:
2497                    void process() {}
2498                };
2499            }
2500        ";
2501        let tree = parse_cpp(source);
2502        let builder = CppGraphBuilder::new();
2503        let mut staging = StagingGraph::new();
2504        let mut budget = BuildBudget::already_expired(Path::new("timeout.cpp"));
2505
2506        let err = builder
2507            .build_graph_with_budget(
2508                &tree,
2509                source.as_bytes(),
2510                Path::new("timeout.cpp"),
2511                &mut staging,
2512                &mut budget,
2513            )
2514            .expect_err("expired budget should force timeout");
2515
2516        match err {
2517            GraphBuilderError::BuildTimedOut {
2518                file,
2519                phase,
2520                timeout_ms,
2521            } => {
2522                assert_eq!(file, PathBuf::from("timeout.cpp"));
2523                assert_eq!(phase, "cpp:extract_namespace_map");
2524                assert_eq!(timeout_ms, 1_000);
2525            }
2526            other => panic!("expected BuildTimedOut, got {other:?}"),
2527        }
2528    }
2529
2530    #[test]
2531    fn test_extract_class() {
2532        let source = "class User { }";
2533        let tree = parse_cpp(source);
2534        let mut staging = StagingGraph::new();
2535        let builder = CppGraphBuilder::new();
2536
2537        let result = builder.build_graph(
2538            &tree,
2539            source.as_bytes(),
2540            Path::new("test.cpp"),
2541            &mut staging,
2542        );
2543
2544        assert!(result.is_ok());
2545        assert_has_node_with_kind(&staging, "User", NodeKind::Class);
2546    }
2547
2548    #[test]
2549    fn test_extract_template_class() {
2550        let source = r"
2551            template <typename T>
2552            class Person {
2553            public:
2554                T name;
2555                T age;
2556            };
2557        ";
2558        let tree = parse_cpp(source);
2559        let mut staging = StagingGraph::new();
2560        let builder = CppGraphBuilder::new();
2561
2562        let result = builder.build_graph(
2563            &tree,
2564            source.as_bytes(),
2565            Path::new("test.cpp"),
2566            &mut staging,
2567        );
2568
2569        assert!(result.is_ok());
2570        assert_has_node_with_kind(&staging, "Person", NodeKind::Class);
2571    }
2572
2573    #[test]
2574    fn test_extract_function() {
2575        let source = r#"
2576            #include <cstdio>
2577            void hello() {
2578                std::printf("Hello");
2579            }
2580        "#;
2581        let tree = parse_cpp(source);
2582        let mut staging = StagingGraph::new();
2583        let builder = CppGraphBuilder::new();
2584
2585        let result = builder.build_graph(
2586            &tree,
2587            source.as_bytes(),
2588            Path::new("test.cpp"),
2589            &mut staging,
2590        );
2591
2592        assert!(result.is_ok());
2593        assert_has_node_with_kind(&staging, "hello", NodeKind::Function);
2594    }
2595
2596    #[test]
2597    fn test_extract_virtual_function() {
2598        let source = r"
2599            class Service {
2600            public:
2601                virtual void fetchData() {}
2602            };
2603        ";
2604        let tree = parse_cpp(source);
2605        let mut staging = StagingGraph::new();
2606        let builder = CppGraphBuilder::new();
2607
2608        let result = builder.build_graph(
2609            &tree,
2610            source.as_bytes(),
2611            Path::new("test.cpp"),
2612            &mut staging,
2613        );
2614
2615        assert!(result.is_ok());
2616        assert_has_node(&staging, "fetchData");
2617    }
2618
2619    #[test]
2620    fn test_extract_call_edge() {
2621        let source = r"
2622            void greet() {}
2623
2624            int main() {
2625                greet();
2626                return 0;
2627            }
2628        ";
2629        let tree = parse_cpp(source);
2630        let mut staging = StagingGraph::new();
2631        let builder = CppGraphBuilder::new();
2632
2633        let result = builder.build_graph(
2634            &tree,
2635            source.as_bytes(),
2636            Path::new("test.cpp"),
2637            &mut staging,
2638        );
2639
2640        assert!(result.is_ok());
2641        assert_has_node(&staging, "main");
2642        assert_has_node(&staging, "greet");
2643        let calls = collect_call_edges(&staging);
2644        assert!(!calls.is_empty());
2645    }
2646
2647    #[test]
2648    fn test_extract_member_call_edge() {
2649        let source = r"
2650            class Service {
2651            public:
2652                void helper() {}
2653            };
2654
2655            int main() {
2656                Service svc;
2657                svc.helper();
2658                return 0;
2659            }
2660        ";
2661        let tree = parse_cpp(source);
2662        let mut staging = StagingGraph::new();
2663        let builder = CppGraphBuilder::new();
2664
2665        let result = builder.build_graph(
2666            &tree,
2667            source.as_bytes(),
2668            Path::new("member.cpp"),
2669            &mut staging,
2670        );
2671
2672        assert!(result.is_ok());
2673        assert_has_node(&staging, "main");
2674        assert_has_node(&staging, "helper");
2675        let calls = collect_call_edges(&staging);
2676        assert!(!calls.is_empty());
2677    }
2678
2679    #[test]
2680    fn test_extract_namespace_map_simple() {
2681        let source = r"
2682            namespace demo {
2683                void func() {}
2684            }
2685        ";
2686        let tree = parse_cpp(source);
2687        let namespace_map = extract_namespace_map_for_test(&tree, source);
2688
2689        // Should have one entry mapping the namespace body to "demo::"
2690        assert_eq!(namespace_map.len(), 1);
2691
2692        // Find any namespace entry (we only have one)
2693        let (_, ns_prefix) = namespace_map.iter().next().unwrap();
2694        assert_eq!(ns_prefix, "demo::");
2695    }
2696
2697    #[test]
2698    fn test_extract_namespace_map_nested() {
2699        let source = r"
2700            namespace outer {
2701                namespace inner {
2702                    void func() {}
2703                }
2704            }
2705        ";
2706        let tree = parse_cpp(source);
2707        let namespace_map = extract_namespace_map_for_test(&tree, source);
2708
2709        // Should have entries for both outer and inner namespaces
2710        assert!(namespace_map.len() >= 2);
2711
2712        // Check that we have the expected namespace prefixes
2713        let ns_values: Vec<&String> = namespace_map.values().collect();
2714        assert!(ns_values.iter().any(|v| v.as_str() == "outer::"));
2715        assert!(ns_values.iter().any(|v| v.as_str() == "outer::inner::"));
2716    }
2717
2718    #[test]
2719    fn test_extract_namespace_map_multiple() {
2720        let source = r"
2721            namespace first {
2722                void func1() {}
2723            }
2724            namespace second {
2725                void func2() {}
2726            }
2727        ";
2728        let tree = parse_cpp(source);
2729        let namespace_map = extract_namespace_map_for_test(&tree, source);
2730
2731        // Should have entries for both namespaces
2732        assert_eq!(namespace_map.len(), 2);
2733
2734        let ns_values: Vec<&String> = namespace_map.values().collect();
2735        assert!(ns_values.iter().any(|v| v.as_str() == "first::"));
2736        assert!(ns_values.iter().any(|v| v.as_str() == "second::"));
2737    }
2738
2739    #[test]
2740    fn test_find_namespace_for_offset() {
2741        let source = r"
2742            namespace demo {
2743                void func() {}
2744            }
2745        ";
2746        let tree = parse_cpp(source);
2747        let namespace_map = extract_namespace_map_for_test(&tree, source);
2748
2749        // Find the byte offset of "func" (should be inside demo namespace)
2750        let func_offset = source.find("func").unwrap();
2751        let ns = find_namespace_for_offset(func_offset, &namespace_map);
2752        assert_eq!(ns, "demo::");
2753
2754        // Byte offset before namespace should return empty string
2755        let ns = find_namespace_for_offset(0, &namespace_map);
2756        assert_eq!(ns, "");
2757    }
2758
2759    #[test]
2760    fn test_extract_cpp_contexts_free_function() {
2761        let source = r"
2762            void helper() {}
2763        ";
2764        let tree = parse_cpp(source);
2765        let namespace_map = extract_namespace_map_for_test(&tree, source);
2766        let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
2767
2768        assert_eq!(contexts.len(), 1);
2769        assert_eq!(contexts[0].qualified_name, "helper");
2770        assert!(!contexts[0].is_static);
2771        assert!(!contexts[0].is_virtual);
2772    }
2773
2774    #[test]
2775    fn test_extract_cpp_contexts_namespace_function() {
2776        let source = r"
2777            namespace demo {
2778                void helper() {}
2779            }
2780        ";
2781        let tree = parse_cpp(source);
2782        let namespace_map = extract_namespace_map_for_test(&tree, source);
2783        let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
2784
2785        assert_eq!(contexts.len(), 1);
2786        assert_eq!(contexts[0].qualified_name, "demo::helper");
2787        assert_eq!(contexts[0].namespace_stack, vec!["demo"]);
2788    }
2789
2790    #[test]
2791    fn test_extract_cpp_contexts_class_method() {
2792        let source = r"
2793            class Service {
2794            public:
2795                void process() {}
2796            };
2797        ";
2798        let tree = parse_cpp(source);
2799        let namespace_map = extract_namespace_map_for_test(&tree, source);
2800        let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
2801
2802        assert_eq!(contexts.len(), 1);
2803        assert_eq!(contexts[0].qualified_name, "Service::process");
2804        assert_eq!(contexts[0].class_stack, vec!["Service"]);
2805    }
2806
2807    #[test]
2808    fn test_extract_cpp_contexts_namespace_and_class() {
2809        let source = r"
2810            namespace demo {
2811                class Service {
2812                public:
2813                    void process() {}
2814                };
2815            }
2816        ";
2817        let tree = parse_cpp(source);
2818        let namespace_map = extract_namespace_map_for_test(&tree, source);
2819        let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
2820
2821        assert_eq!(contexts.len(), 1);
2822        assert_eq!(contexts[0].qualified_name, "demo::Service::process");
2823        assert_eq!(contexts[0].namespace_stack, vec!["demo"]);
2824        assert_eq!(contexts[0].class_stack, vec!["Service"]);
2825    }
2826
2827    #[test]
2828    fn test_extract_cpp_contexts_static_method() {
2829        let source = r"
2830            class Repository {
2831            public:
2832                static void save() {}
2833            };
2834        ";
2835        let tree = parse_cpp(source);
2836        let namespace_map = extract_namespace_map_for_test(&tree, source);
2837        let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
2838
2839        assert_eq!(contexts.len(), 1);
2840        assert_eq!(contexts[0].qualified_name, "Repository::save");
2841        assert!(contexts[0].is_static);
2842    }
2843
2844    #[test]
2845    fn test_extract_cpp_contexts_virtual_method() {
2846        let source = r"
2847            class Base {
2848            public:
2849                virtual void render() {}
2850            };
2851        ";
2852        let tree = parse_cpp(source);
2853        let namespace_map = extract_namespace_map_for_test(&tree, source);
2854        let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
2855
2856        assert_eq!(contexts.len(), 1);
2857        assert_eq!(contexts[0].qualified_name, "Base::render");
2858        assert!(contexts[0].is_virtual);
2859    }
2860
2861    #[test]
2862    fn test_extract_cpp_contexts_inline_function() {
2863        let source = r"
2864            inline void helper() {}
2865        ";
2866        let tree = parse_cpp(source);
2867        let namespace_map = extract_namespace_map_for_test(&tree, source);
2868        let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
2869
2870        assert_eq!(contexts.len(), 1);
2871        assert_eq!(contexts[0].qualified_name, "helper");
2872        assert!(contexts[0].is_inline);
2873    }
2874
2875    #[test]
2876    fn test_extract_cpp_contexts_out_of_line_definition() {
2877        let source = r"
2878            namespace demo {
2879                class Service {
2880                public:
2881                    int process(int v);
2882                };
2883
2884                inline int Service::process(int v) {
2885                    return v;
2886                }
2887            }
2888        ";
2889        let tree = parse_cpp(source);
2890        let namespace_map = extract_namespace_map_for_test(&tree, source);
2891        let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
2892
2893        // Only the definition should be captured (not the declaration)
2894        assert_eq!(contexts.len(), 1);
2895        assert_eq!(contexts[0].qualified_name, "demo::Service::process");
2896        assert!(contexts[0].is_inline);
2897    }
2898
2899    #[test]
2900    fn test_extract_field_types_simple() {
2901        let source = r"
2902            class Service {
2903            public:
2904                Repository repo;
2905            };
2906        ";
2907        let tree = parse_cpp(source);
2908        let namespace_map = extract_namespace_map_for_test(&tree, source);
2909        let (field_types, _type_map) =
2910            extract_field_and_type_info_for_test(&tree, source, &namespace_map);
2911
2912        // Should have one field: Service.repo -> Repository
2913        assert_eq!(field_types.len(), 1);
2914        assert_eq!(
2915            field_types.get(&("Service".to_string(), "repo".to_string())),
2916            Some(&"Repository".to_string())
2917        );
2918    }
2919
2920    #[test]
2921    fn test_extract_field_types_namespace() {
2922        let source = r"
2923            namespace demo {
2924                class Service {
2925                public:
2926                    Repository repo;
2927                };
2928            }
2929        ";
2930        let tree = parse_cpp(source);
2931        let namespace_map = extract_namespace_map_for_test(&tree, source);
2932        let (field_types, _type_map) =
2933            extract_field_and_type_info_for_test(&tree, source, &namespace_map);
2934
2935        // Should have one field with namespace-qualified class
2936        assert_eq!(field_types.len(), 1);
2937        assert_eq!(
2938            field_types.get(&("demo::Service".to_string(), "repo".to_string())),
2939            Some(&"Repository".to_string())
2940        );
2941    }
2942
2943    #[test]
2944    fn test_extract_field_types_no_collision() {
2945        let source = r"
2946            class ServiceA {
2947            public:
2948                Repository repo;
2949            };
2950
2951            class ServiceB {
2952            public:
2953                Repository repo;
2954            };
2955        ";
2956        let tree = parse_cpp(source);
2957        let namespace_map = extract_namespace_map_for_test(&tree, source);
2958        let (field_types, _type_map) =
2959            extract_field_and_type_info_for_test(&tree, source, &namespace_map);
2960
2961        // Should have two distinct fields with no collision
2962        assert_eq!(field_types.len(), 2);
2963        assert_eq!(
2964            field_types.get(&("ServiceA".to_string(), "repo".to_string())),
2965            Some(&"Repository".to_string())
2966        );
2967        assert_eq!(
2968            field_types.get(&("ServiceB".to_string(), "repo".to_string())),
2969            Some(&"Repository".to_string())
2970        );
2971    }
2972
2973    #[test]
2974    fn test_extract_using_declaration() {
2975        let source = r"
2976            using std::vector;
2977
2978            class Service {
2979            public:
2980                vector data;
2981            };
2982        ";
2983        let tree = parse_cpp(source);
2984        let namespace_map = extract_namespace_map_for_test(&tree, source);
2985        let (field_types, type_map) =
2986            extract_field_and_type_info_for_test(&tree, source, &namespace_map);
2987
2988        // Verify field extraction resolves type via using declaration
2989        assert_eq!(field_types.len(), 1);
2990        assert_eq!(
2991            field_types.get(&("Service".to_string(), "data".to_string())),
2992            Some(&"std::vector".to_string()),
2993            "Field type should resolve 'vector' to 'std::vector' via using declaration"
2994        );
2995
2996        // Verify that using declaration populated type_map
2997        assert_eq!(
2998            type_map.get(&(String::new(), "vector".to_string())),
2999            Some(&"std::vector".to_string()),
3000            "Using declaration should map 'vector' to 'std::vector' in type_map"
3001        );
3002    }
3003
3004    #[test]
3005    fn test_extract_field_types_pointer() {
3006        let source = r"
3007            class Service {
3008            public:
3009                Repository* repo;
3010            };
3011        ";
3012        let tree = parse_cpp(source);
3013        let namespace_map = extract_namespace_map_for_test(&tree, source);
3014        let (field_types, _type_map) =
3015            extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3016
3017        // Should extract field even for pointer types
3018        assert_eq!(field_types.len(), 1);
3019        assert_eq!(
3020            field_types.get(&("Service".to_string(), "repo".to_string())),
3021            Some(&"Repository".to_string())
3022        );
3023    }
3024
3025    #[test]
3026    fn test_extract_field_types_multiple_declarators() {
3027        let source = r"
3028            class Service {
3029            public:
3030                Repository repo_a, repo_b, repo_c;
3031            };
3032        ";
3033        let tree = parse_cpp(source);
3034        let namespace_map = extract_namespace_map_for_test(&tree, source);
3035        let (field_types, _type_map) =
3036            extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3037
3038        // Should extract all three fields
3039        assert_eq!(field_types.len(), 3);
3040        assert_eq!(
3041            field_types.get(&("Service".to_string(), "repo_a".to_string())),
3042            Some(&"Repository".to_string())
3043        );
3044        assert_eq!(
3045            field_types.get(&("Service".to_string(), "repo_b".to_string())),
3046            Some(&"Repository".to_string())
3047        );
3048        assert_eq!(
3049            field_types.get(&("Service".to_string(), "repo_c".to_string())),
3050            Some(&"Repository".to_string())
3051        );
3052    }
3053
3054    #[test]
3055    fn test_extract_field_types_nested_struct_with_parent_field() {
3056        // Regression test for nested class FQN building
3057        // Verifies that Inner gets "demo::Outer::Inner" not "demo::Inner"
3058        let source = r"
3059            namespace demo {
3060                struct Outer {
3061                    int outer_field;
3062                    struct Inner {
3063                        int inner_field;
3064                    };
3065                    Inner nested_instance;
3066                };
3067            }
3068        ";
3069        let tree = parse_cpp(source);
3070        let namespace_map = extract_namespace_map_for_test(&tree, source);
3071        let (field_types, _type_map) =
3072            extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3073
3074        // Should have fields from both Outer and Inner with properly qualified class FQNs
3075        // The critical assertion: Inner's field must use "demo::Outer::Inner", not "demo::Inner"
3076        assert!(
3077            field_types.len() >= 2,
3078            "Expected at least outer_field and nested_instance"
3079        );
3080
3081        // Outer's field
3082        assert_eq!(
3083            field_types.get(&("demo::Outer".to_string(), "outer_field".to_string())),
3084            Some(&"int".to_string())
3085        );
3086
3087        // Outer's nested instance field
3088        assert_eq!(
3089            field_types.get(&("demo::Outer".to_string(), "nested_instance".to_string())),
3090            Some(&"Inner".to_string())
3091        );
3092
3093        // If Inner's field is extracted, verify it uses the correct parent-qualified FQN
3094        if field_types.contains_key(&("demo::Outer::Inner".to_string(), "inner_field".to_string()))
3095        {
3096            // Great! The nested class field was extracted with correct FQN
3097            assert_eq!(
3098                field_types.get(&("demo::Outer::Inner".to_string(), "inner_field".to_string())),
3099                Some(&"int".to_string()),
3100                "Inner class fields must use parent-qualified FQN 'demo::Outer::Inner'"
3101            );
3102        }
3103    }
3104}