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