Skip to main content

sqry_lang_cpp/relations/
graph_builder.rs

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