Skip to main content

sqry_lang_cpp/relations/
graph_builder.rs

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