1use sqry_core::graph::unified::{FfiConvention, GraphBuildHelper, StagingGraph};
19use sqry_core::graph::{GraphBuilder, GraphBuilderError, GraphResult, Language, Position, Span};
20use std::{
21 collections::{HashMap, HashSet},
22 path::{Path, PathBuf},
23 time::{Duration, Instant},
24};
25use tree_sitter::{Node, Tree};
26
27const FILE_MODULE_NAME: &str = "<file_module>";
30
31type QualifiedNameMap = HashMap<(String, String), String>;
34
35type FfiRegistry = HashMap<String, (String, FfiConvention)>;
42
43type PureVirtualRegistry = HashSet<String>;
47
48const DEFAULT_GRAPH_BUILD_TIMEOUT_MS: u64 = 10_000;
49const MIN_GRAPH_BUILD_TIMEOUT_MS: u64 = 1_000;
50const MAX_GRAPH_BUILD_TIMEOUT_MS: u64 = 60_000;
51const BUDGET_CHECK_INTERVAL: u32 = 1024;
52
53fn cpp_graph_build_timeout() -> Duration {
54 let timeout_ms = std::env::var("SQRY_CPP_GRAPH_BUILD_TIMEOUT_MS")
55 .ok()
56 .and_then(|value| value.parse::<u64>().ok())
57 .unwrap_or(DEFAULT_GRAPH_BUILD_TIMEOUT_MS)
58 .clamp(MIN_GRAPH_BUILD_TIMEOUT_MS, MAX_GRAPH_BUILD_TIMEOUT_MS);
59 Duration::from_millis(timeout_ms)
60}
61
62struct BuildBudget {
63 file: PathBuf,
64 phase_timeout: Duration,
65 started_at: Instant,
66 checkpoints: u32,
67}
68
69impl BuildBudget {
70 fn new(file: &Path) -> Self {
71 Self {
72 file: file.to_path_buf(),
73 phase_timeout: cpp_graph_build_timeout(),
74 started_at: Instant::now(),
75 checkpoints: 0,
76 }
77 }
78
79 #[cfg(test)]
80 fn already_expired(file: &Path) -> Self {
81 Self {
82 file: file.to_path_buf(),
83 phase_timeout: Duration::from_secs(1),
84 started_at: Instant::now().checked_sub(Duration::from_secs(60)).unwrap(),
85 checkpoints: BUDGET_CHECK_INTERVAL - 1,
86 }
87 }
88
89 fn checkpoint(&mut self, phase: &'static str) -> GraphResult<()> {
90 self.checkpoints = self.checkpoints.wrapping_add(1);
91 if self.checkpoints.is_multiple_of(BUDGET_CHECK_INTERVAL)
92 && self.started_at.elapsed() > self.phase_timeout
93 {
94 return Err(GraphBuilderError::BuildTimedOut {
95 file: self.file.clone(),
96 phase,
97 #[allow(clippy::cast_possible_truncation)] timeout_ms: self.phase_timeout.as_millis() as u64,
99 });
100 }
101 Ok(())
102 }
103}
104
105#[allow(dead_code)] trait SpanExt {
108 fn from_node(node: &tree_sitter::Node) -> Self;
109}
110
111impl SpanExt for Span {
112 fn from_node(node: &tree_sitter::Node) -> Self {
113 Span::new(
114 Position::new(node.start_position().row, node.start_position().column),
115 Position::new(node.end_position().row, node.end_position().column),
116 )
117 }
118}
119
120#[derive(Debug)]
132struct ASTGraph {
133 contexts: Vec<FunctionContext>,
135 context_start_index: HashMap<usize, usize>,
137
138 #[allow(dead_code)]
144 field_types: QualifiedNameMap,
145
146 #[allow(dead_code)]
153 type_map: QualifiedNameMap,
154
155 #[allow(dead_code)]
159 namespace_map: HashMap<std::ops::Range<usize>, String>,
160}
161
162impl ASTGraph {
163 fn from_tree(root: Node, content: &[u8], budget: &mut BuildBudget) -> GraphResult<Self> {
165 let namespace_map = extract_namespace_map(root, content, budget)?;
167
168 let mut contexts = extract_cpp_contexts(root, content, &namespace_map, budget)?;
170 contexts.sort_by_key(|ctx| ctx.span.0);
171 let context_start_index = contexts
172 .iter()
173 .enumerate()
174 .map(|(idx, ctx)| (ctx.span.0, idx))
175 .collect();
176
177 let (field_types, type_map) =
179 extract_field_and_type_info(root, content, &namespace_map, budget)?;
180
181 Ok(Self {
182 contexts,
183 context_start_index,
184 field_types,
185 type_map,
186 namespace_map,
187 })
188 }
189
190 fn find_enclosing(&self, byte_pos: usize) -> Option<&FunctionContext> {
196 let insertion_point = self.contexts.partition_point(|ctx| ctx.span.0 <= byte_pos);
197 if insertion_point == 0 {
198 return None;
199 }
200
201 let candidate = &self.contexts[insertion_point - 1];
202 (byte_pos < candidate.span.1).then_some(candidate)
203 }
204
205 fn context_for_start(&self, start_byte: usize) -> Option<&FunctionContext> {
206 self.context_start_index
207 .get(&start_byte)
208 .and_then(|idx| self.contexts.get(*idx))
209 }
210}
211
212#[derive(Debug, Clone)]
214struct FunctionContext {
215 qualified_name: String,
217 span: (usize, usize),
219 is_static: bool,
222 #[allow(dead_code)]
225 is_virtual: bool,
226 #[allow(dead_code)]
229 is_inline: bool,
230 namespace_stack: Vec<String>,
232 #[allow(dead_code)] class_stack: Vec<String>,
236 return_type: Option<String>,
238}
239
240impl FunctionContext {
241 #[allow(dead_code)] fn qualified_name(&self) -> &str {
243 &self.qualified_name
244 }
245}
246
247#[derive(Debug, Default, Clone, Copy)]
271pub struct CppGraphBuilder;
272
273impl CppGraphBuilder {
274 #[must_use]
276 pub fn new() -> Self {
277 Self
278 }
279
280 #[allow(clippy::unused_self)] #[allow(clippy::trivially_copy_pass_by_ref)] fn build_graph_with_budget(
283 #[allow(clippy::trivially_copy_pass_by_ref)] &self,
285 tree: &Tree,
286 content: &[u8],
287 file: &Path,
288 staging: &mut StagingGraph,
289 budget: &mut BuildBudget,
290 ) -> GraphResult<()> {
291 let mut helper = GraphBuildHelper::new(staging, file, Language::Cpp);
293
294 let ast_graph = ASTGraph::from_tree(tree.root_node(), content, budget)?;
296
297 let mut seen_includes: HashSet<String> = HashSet::new();
299
300 let mut namespace_stack: Vec<String> = Vec::new();
302 let mut class_stack: Vec<String> = Vec::new();
303
304 let mut ffi_registry = FfiRegistry::new();
307 collect_ffi_declarations(tree.root_node(), content, &mut ffi_registry, budget)?;
308
309 let mut pure_virtual_registry = PureVirtualRegistry::new();
311 collect_pure_virtual_interfaces(
312 tree.root_node(),
313 content,
314 &mut pure_virtual_registry,
315 budget,
316 )?;
317
318 walk_tree_for_graph(
320 tree.root_node(),
321 content,
322 &ast_graph,
323 &mut helper,
324 &mut seen_includes,
325 &mut namespace_stack,
326 &mut class_stack,
327 &ffi_registry,
328 &pure_virtual_registry,
329 budget,
330 )?;
331
332 Ok(())
333 }
334
335 #[allow(dead_code)] fn extract_class_attributes(node: &tree_sitter::Node, content: &[u8]) -> Vec<String> {
338 let mut attributes = Vec::new();
339 let mut cursor = node.walk();
340 for child in node.children(&mut cursor) {
341 if child.kind() == "modifiers" {
342 let mut mod_cursor = child.walk();
343 for modifier in child.children(&mut mod_cursor) {
344 if let Ok(mod_text) = modifier.utf8_text(content) {
345 match mod_text {
346 "template" => attributes.push("template".to_string()),
347 "sealed" => attributes.push("sealed".to_string()),
348 "abstract" => attributes.push("abstract".to_string()),
349 "open" => attributes.push("open".to_string()),
350 "final" => attributes.push("final".to_string()),
351 "inner" => attributes.push("inner".to_string()),
352 "value" => attributes.push("value".to_string()),
353 _ => {}
354 }
355 }
356 }
357 }
358 }
359 attributes
360 }
361
362 #[allow(dead_code)] fn extract_is_virtual(node: &tree_sitter::Node, content: &[u8]) -> bool {
365 if let Some(spec) = node.child_by_field_name("declaration_specifiers")
366 && let Ok(text) = spec.utf8_text(content)
367 && text.contains("virtual")
368 {
369 return true;
370 }
371
372 if let Ok(text) = node.utf8_text(content)
373 && text.contains("virtual")
374 {
375 return true;
376 }
377
378 if let Some(parent) = node.parent()
379 && (parent.kind() == "field_declaration" || parent.kind() == "declaration")
380 && let Ok(text) = parent.utf8_text(content)
381 && text.contains("virtual")
382 {
383 return true;
384 }
385
386 false
387 }
388
389 #[allow(dead_code)] fn extract_function_attributes(node: &tree_sitter::Node, content: &[u8]) -> Vec<String> {
392 let mut attributes = Vec::new();
393 for node_ref in [
394 node.child_by_field_name("declaration_specifiers"),
395 node.parent(),
396 ]
397 .into_iter()
398 .flatten()
399 {
400 if let Ok(text) = node_ref.utf8_text(content) {
401 for keyword in [
402 "virtual",
403 "inline",
404 "constexpr",
405 "operator",
406 "override",
407 "static",
408 ] {
409 if text.contains(keyword) && !attributes.contains(&keyword.to_string()) {
410 attributes.push(keyword.to_string());
411 }
412 }
413 }
414 }
415
416 if let Ok(text) = node.utf8_text(content) {
417 for keyword in [
418 "virtual",
419 "inline",
420 "constexpr",
421 "operator",
422 "override",
423 "static",
424 ] {
425 if text.contains(keyword) && !attributes.contains(&keyword.to_string()) {
426 attributes.push(keyword.to_string());
427 }
428 }
429 }
430
431 attributes
432 }
433}
434
435impl GraphBuilder for CppGraphBuilder {
436 fn language(&self) -> Language {
437 Language::Cpp
438 }
439
440 fn build_graph(
441 &self,
442 tree: &Tree,
443 content: &[u8],
444 file: &Path,
445 staging: &mut StagingGraph,
446 ) -> GraphResult<()> {
447 let mut budget = BuildBudget::new(file);
448 self.build_graph_with_budget(tree, content, file, staging, &mut budget)
449 }
450}
451
452fn extract_namespace_map(
464 node: Node,
465 content: &[u8],
466 budget: &mut BuildBudget,
467) -> GraphResult<HashMap<std::ops::Range<usize>, String>> {
468 let mut map = HashMap::new();
469
470 let recursion_limits = sqry_core::config::RecursionLimits::load_or_default()
472 .expect("Failed to load recursion limits");
473 let file_ops_depth = recursion_limits
474 .effective_file_ops_depth()
475 .expect("Invalid file_ops_depth configuration");
476 let mut guard = sqry_core::query::security::RecursionGuard::new(file_ops_depth)
477 .expect("Failed to create recursion guard");
478
479 extract_namespaces_recursive(node, content, "", &mut map, &mut guard, budget).map_err(|e| {
480 match e {
481 timeout @ GraphBuilderError::BuildTimedOut { .. } => timeout,
482 other => GraphBuilderError::ParseError {
483 span: span_from_node(node),
484 reason: format!("C++ namespace extraction failed: {other}"),
485 },
486 }
487 })?;
488
489 Ok(map)
490}
491
492fn extract_namespaces_recursive(
498 node: Node,
499 content: &[u8],
500 current_ns: &str,
501 map: &mut HashMap<std::ops::Range<usize>, String>,
502 guard: &mut sqry_core::query::security::RecursionGuard,
503 budget: &mut BuildBudget,
504) -> GraphResult<()> {
505 budget.checkpoint("cpp:extract_namespace_map")?;
506 guard.enter().map_err(|e| GraphBuilderError::ParseError {
507 span: span_from_node(node),
508 reason: format!("C++ namespace extraction hit recursion limit: {e}"),
509 })?;
510
511 if node.kind() == "namespace_definition" {
512 let ns_name = if let Some(name_node) = node.child_by_field_name("name") {
514 extract_identifier(name_node, content)
515 } else {
516 String::from("anonymous")
518 };
519
520 let new_ns = if current_ns.is_empty() {
522 format!("{ns_name}::")
523 } else {
524 format!("{current_ns}{ns_name}::")
525 };
526
527 if let Some(body) = node.child_by_field_name("body") {
529 let range = body.start_byte()..body.end_byte();
530 map.insert(range, new_ns.clone());
531
532 let mut cursor = body.walk();
534 for child in body.children(&mut cursor) {
535 extract_namespaces_recursive(child, content, &new_ns, map, guard, budget)?;
536 }
537 }
538 } else {
539 let mut cursor = node.walk();
541 for child in node.children(&mut cursor) {
542 extract_namespaces_recursive(child, content, current_ns, map, guard, budget)?;
543 }
544 }
545
546 guard.exit();
547 Ok(())
548}
549
550fn extract_identifier(node: Node, content: &[u8]) -> String {
552 node.utf8_text(content).unwrap_or("").to_string()
553}
554
555fn find_namespace_for_offset(
557 byte_offset: usize,
558 namespace_map: &HashMap<std::ops::Range<usize>, String>,
559) -> String {
560 let mut matching_ranges: Vec<_> = namespace_map
562 .iter()
563 .filter(|(range, _)| range.contains(&byte_offset))
564 .collect();
565
566 matching_ranges.sort_by_key(|(range, _)| range.end - range.start);
568
569 matching_ranges
571 .first()
572 .map_or("", |(_, ns)| ns.as_str())
573 .to_string()
574}
575
576fn extract_cpp_contexts(
583 node: Node,
584 content: &[u8],
585 namespace_map: &HashMap<std::ops::Range<usize>, String>,
586 budget: &mut BuildBudget,
587) -> GraphResult<Vec<FunctionContext>> {
588 let mut contexts = Vec::new();
589 let mut class_stack = Vec::new();
590
591 let recursion_limits = sqry_core::config::RecursionLimits::load_or_default()
593 .expect("Failed to load recursion limits");
594 let file_ops_depth = recursion_limits
595 .effective_file_ops_depth()
596 .expect("Invalid file_ops_depth configuration");
597 let mut guard = sqry_core::query::security::RecursionGuard::new(file_ops_depth)
598 .expect("Failed to create recursion guard");
599
600 extract_contexts_recursive(
601 node,
602 content,
603 namespace_map,
604 &mut contexts,
605 &mut class_stack,
606 &mut guard,
607 budget,
608 )
609 .map_err(|e| match e {
610 timeout @ GraphBuilderError::BuildTimedOut { .. } => timeout,
611 other => GraphBuilderError::ParseError {
612 span: span_from_node(node),
613 reason: format!("C++ context extraction failed: {other}"),
614 },
615 })?;
616
617 Ok(contexts)
618}
619
620fn extract_contexts_recursive(
625 node: Node,
626 content: &[u8],
627 namespace_map: &HashMap<std::ops::Range<usize>, String>,
628 contexts: &mut Vec<FunctionContext>,
629 class_stack: &mut Vec<String>,
630 guard: &mut sqry_core::query::security::RecursionGuard,
631 budget: &mut BuildBudget,
632) -> GraphResult<()> {
633 budget.checkpoint("cpp:extract_contexts")?;
634 guard.enter().map_err(|e| GraphBuilderError::ParseError {
635 span: span_from_node(node),
636 reason: format!("C++ context extraction hit recursion limit: {e}"),
637 })?;
638
639 match node.kind() {
640 "class_specifier" | "struct_specifier" => {
641 if let Some(name_node) = node.child_by_field_name("name") {
643 let class_name = extract_identifier(name_node, content);
644 class_stack.push(class_name);
645
646 if let Some(body) = node.child_by_field_name("body") {
648 let mut cursor = body.walk();
649 for child in body.children(&mut cursor) {
650 extract_contexts_recursive(
651 child,
652 content,
653 namespace_map,
654 contexts,
655 class_stack,
656 guard,
657 budget,
658 )?;
659 }
660 }
661
662 class_stack.pop();
663 }
664 }
665
666 "function_definition" => {
667 if let Some(declarator) = node.child_by_field_name("declarator") {
669 let (func_name, class_prefix) =
670 extract_function_name_with_class(declarator, content);
671
672 let namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
674 let namespace_stack: Vec<String> = if namespace.is_empty() {
675 Vec::new()
676 } else {
677 namespace
678 .trim_end_matches("::")
679 .split("::")
680 .map(String::from)
681 .collect()
682 };
683
684 let effective_class_stack: Vec<String> = if !class_stack.is_empty() {
688 class_stack.clone()
689 } else if let Some(ref prefix) = class_prefix {
690 vec![prefix.clone()]
691 } else {
692 Vec::new()
693 };
694
695 let qualified_name =
697 build_qualified_name(&namespace_stack, &effective_class_stack, &func_name);
698
699 let is_static = is_static_function(node, content);
701 let is_virtual = is_virtual_function(node, content);
702 let is_inline = is_inline_function(node, content);
703
704 let return_type = node
706 .child_by_field_name("type")
707 .and_then(|type_node| type_node.utf8_text(content).ok())
708 .map(std::string::ToString::to_string);
709
710 let span = (node.start_byte(), node.end_byte());
712
713 contexts.push(FunctionContext {
714 qualified_name,
715 span,
716 is_static,
717 is_virtual,
718 is_inline,
719 namespace_stack,
720 class_stack: effective_class_stack,
721 return_type,
722 });
723 }
724
725 }
727
728 _ => {
729 let mut cursor = node.walk();
731 for child in node.children(&mut cursor) {
732 extract_contexts_recursive(
733 child,
734 content,
735 namespace_map,
736 contexts,
737 class_stack,
738 guard,
739 budget,
740 )?;
741 }
742 }
743 }
744
745 guard.exit();
746 Ok(())
747}
748
749fn build_qualified_name(namespace_stack: &[String], class_stack: &[String], name: &str) -> String {
754 let mut parts = Vec::new();
755
756 parts.extend(namespace_stack.iter().cloned());
758
759 for class_name in class_stack {
761 parts.push(class_name.clone());
762 }
763
764 parts.push(name.to_string());
766
767 parts.join("::")
768}
769
770fn extract_function_name_with_class(declarator: Node, content: &[u8]) -> (String, Option<String>) {
775 match declarator.kind() {
783 "function_declarator" => {
784 if let Some(declarator_inner) = declarator.child_by_field_name("declarator") {
786 extract_function_name_with_class(declarator_inner, content)
787 } else {
788 (extract_identifier(declarator, content), None)
789 }
790 }
791 "qualified_identifier" => {
792 let name = if let Some(name_node) = declarator.child_by_field_name("name") {
794 extract_identifier(name_node, content)
795 } else {
796 extract_identifier(declarator, content)
797 };
798
799 let class_prefix = declarator
801 .child_by_field_name("scope")
802 .map(|scope_node| extract_identifier(scope_node, content));
803
804 (name, class_prefix)
805 }
806 "field_identifier" | "identifier" | "destructor_name" | "operator_name" => {
807 (extract_identifier(declarator, content), None)
808 }
809 _ => {
810 (extract_identifier(declarator, content), None)
812 }
813 }
814}
815
816#[allow(dead_code)]
818fn extract_function_name(declarator: Node, content: &[u8]) -> String {
819 extract_function_name_with_class(declarator, content).0
820}
821
822fn is_static_function(node: Node, content: &[u8]) -> bool {
824 has_specifier(node, "static", content)
825}
826
827fn is_virtual_function(node: Node, content: &[u8]) -> bool {
829 has_specifier(node, "virtual", content)
830}
831
832fn is_inline_function(node: Node, content: &[u8]) -> bool {
834 has_specifier(node, "inline", content)
835}
836
837fn has_specifier(node: Node, specifier: &str, content: &[u8]) -> bool {
839 let mut cursor = node.walk();
841 for child in node.children(&mut cursor) {
842 if (child.kind() == "storage_class_specifier"
843 || child.kind() == "type_qualifier"
844 || child.kind() == "virtual"
845 || child.kind() == "inline")
846 && let Ok(text) = child.utf8_text(content)
847 && text == specifier
848 {
849 return true;
850 }
851 }
852 false
853}
854
855fn extract_field_and_type_info(
865 node: Node,
866 content: &[u8],
867 namespace_map: &HashMap<std::ops::Range<usize>, String>,
868 budget: &mut BuildBudget,
869) -> GraphResult<(QualifiedNameMap, QualifiedNameMap)> {
870 let mut field_types = HashMap::new();
871 let mut type_map = HashMap::new();
872 let mut class_stack = Vec::new();
873
874 extract_fields_recursive(
875 node,
876 content,
877 namespace_map,
878 &mut field_types,
879 &mut type_map,
880 &mut class_stack,
881 budget,
882 )?;
883
884 Ok((field_types, type_map))
885}
886
887fn extract_fields_recursive(
889 node: Node,
890 content: &[u8],
891 namespace_map: &HashMap<std::ops::Range<usize>, String>,
892 field_types: &mut HashMap<(String, String), String>,
893 type_map: &mut HashMap<(String, String), String>,
894 class_stack: &mut Vec<String>,
895 budget: &mut BuildBudget,
896) -> GraphResult<()> {
897 budget.checkpoint("cpp:extract_fields")?;
898 match node.kind() {
899 "class_specifier" | "struct_specifier" => {
900 if let Some(name_node) = node.child_by_field_name("name") {
902 let class_name = extract_identifier(name_node, content);
903 let namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
904
905 let class_fqn = if class_stack.is_empty() {
907 if namespace.is_empty() {
909 class_name.clone()
910 } else {
911 format!("{}::{}", namespace.trim_end_matches("::"), class_name)
912 }
913 } else {
914 format!("{}::{}", class_stack.last().unwrap(), class_name)
916 };
917
918 class_stack.push(class_fqn.clone());
919
920 let mut cursor = node.walk();
922 for child in node.children(&mut cursor) {
923 extract_fields_recursive(
924 child,
925 content,
926 namespace_map,
927 field_types,
928 type_map,
929 class_stack,
930 budget,
931 )?;
932 }
933
934 class_stack.pop();
935 }
936 }
937
938 "field_declaration" => {
939 if let Some(class_fqn) = class_stack.last() {
941 extract_field_declaration(
942 node,
943 content,
944 class_fqn,
945 namespace_map,
946 field_types,
947 type_map,
948 );
949 }
950 }
951
952 "using_directive" => {
953 extract_using_directive(node, content, namespace_map, type_map);
955 }
956
957 "using_declaration" => {
958 extract_using_declaration(node, content, namespace_map, type_map);
960 }
961
962 _ => {
963 let mut cursor = node.walk();
965 for child in node.children(&mut cursor) {
966 extract_fields_recursive(
967 child,
968 content,
969 namespace_map,
970 field_types,
971 type_map,
972 class_stack,
973 budget,
974 )?;
975 }
976 }
977 }
978
979 Ok(())
980}
981
982fn extract_field_declaration(
984 node: Node,
985 content: &[u8],
986 class_fqn: &str,
987 namespace_map: &HashMap<std::ops::Range<usize>, String>,
988 field_types: &mut HashMap<(String, String), String>,
989 type_map: &HashMap<(String, String), String>,
990) {
991 let mut field_type = None;
996 let mut field_names = Vec::new();
997
998 let mut cursor = node.walk();
999 for child in node.children(&mut cursor) {
1000 match child.kind() {
1001 "type_identifier" | "primitive_type" | "qualified_identifier" | "template_type" => {
1002 field_type = Some(extract_type_name(child, content));
1003 }
1004 "field_identifier" => {
1005 field_names.push(extract_identifier(child, content));
1007 }
1008 "field_declarator"
1009 | "init_declarator"
1010 | "pointer_declarator"
1011 | "reference_declarator"
1012 | "array_declarator" => {
1013 if let Some(name) = extract_field_name(child, content) {
1015 field_names.push(name);
1016 }
1017 }
1018 _ => {}
1019 }
1020 }
1021
1022 if let Some(ftype) = field_type {
1024 let namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
1025 let field_type_fqn = resolve_type_to_fqn(&ftype, &namespace, type_map);
1026
1027 for fname in field_names {
1029 field_types.insert((class_fqn.to_string(), fname), field_type_fqn.clone());
1030 }
1031 }
1032}
1033
1034fn extract_type_name(type_node: Node, content: &[u8]) -> String {
1036 match type_node.kind() {
1037 "type_identifier" | "primitive_type" => extract_identifier(type_node, content),
1038 "qualified_identifier" => {
1039 extract_identifier(type_node, content)
1041 }
1042 "template_type" => {
1043 if let Some(name) = type_node.child_by_field_name("name") {
1045 extract_identifier(name, content)
1046 } else {
1047 extract_identifier(type_node, content)
1048 }
1049 }
1050 _ => {
1051 extract_identifier(type_node, content)
1053 }
1054 }
1055}
1056
1057fn extract_field_name(declarator: Node, content: &[u8]) -> Option<String> {
1059 match declarator.kind() {
1060 "field_declarator" => {
1061 if let Some(declarator_inner) = declarator.child_by_field_name("declarator") {
1063 extract_field_name(declarator_inner, content)
1064 } else {
1065 Some(extract_identifier(declarator, content))
1066 }
1067 }
1068 "field_identifier" | "identifier" => Some(extract_identifier(declarator, content)),
1069 "pointer_declarator" | "reference_declarator" | "array_declarator" => {
1070 if let Some(declarator_inner) = declarator.child_by_field_name("declarator") {
1072 extract_field_name(declarator_inner, content)
1073 } else {
1074 None
1075 }
1076 }
1077 "init_declarator" => {
1078 if let Some(declarator_inner) = declarator.child_by_field_name("declarator") {
1080 extract_field_name(declarator_inner, content)
1081 } else {
1082 None
1083 }
1084 }
1085 _ => None,
1086 }
1087}
1088
1089fn resolve_type_to_fqn(
1091 type_name: &str,
1092 namespace: &str,
1093 type_map: &HashMap<(String, String), String>,
1094) -> String {
1095 if type_name.contains("::") {
1097 return type_name.to_string();
1098 }
1099
1100 let namespace_key = namespace.trim_end_matches("::").to_string();
1102 if let Some(fqn) = type_map.get(&(namespace_key.clone(), type_name.to_string())) {
1103 return fqn.clone();
1104 }
1105
1106 if let Some(fqn) = type_map.get(&(String::new(), type_name.to_string())) {
1108 return fqn.clone();
1109 }
1110
1111 type_name.to_string()
1113}
1114
1115fn extract_using_directive(
1117 node: Node,
1118 content: &[u8],
1119 namespace_map: &HashMap<std::ops::Range<usize>, String>,
1120 _type_map: &mut HashMap<(String, String), String>,
1121) {
1122 let _namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
1126
1127 if let Some(name_node) = node.child_by_field_name("name") {
1129 let _using_ns = extract_identifier(name_node, content);
1130 }
1134}
1135
1136fn extract_using_declaration(
1141 node: Node,
1142 content: &[u8],
1143 namespace_map: &HashMap<std::ops::Range<usize>, String>,
1144 type_map: &mut HashMap<(String, String), String>,
1145) {
1146 let namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
1147 let namespace_key = namespace.trim_end_matches("::").to_string();
1148
1149 let mut cursor = node.walk();
1151 for child in node.children(&mut cursor) {
1152 if child.kind() == "qualified_identifier" || child.kind() == "identifier" {
1153 let fqn = extract_identifier(child, content);
1154
1155 if let Some(simple_name) = fqn.split("::").last() {
1157 type_map.insert((namespace_key, simple_name.to_string()), fqn);
1159 }
1160 break;
1161 }
1162 }
1163}
1164
1165fn resolve_callee_name(
1177 callee_name: &str,
1178 caller_ctx: &FunctionContext,
1179 _ast_graph: &ASTGraph,
1180) -> String {
1181 if callee_name.starts_with("::") {
1183 return callee_name.trim_start_matches("::").to_string();
1184 }
1185
1186 if callee_name.contains("::") {
1188 if !caller_ctx.namespace_stack.is_empty() {
1190 let namespace_prefix = caller_ctx.namespace_stack.join("::");
1191 return format!("{namespace_prefix}::{callee_name}");
1192 }
1193 return callee_name.to_string();
1194 }
1195
1196 let mut parts = Vec::new();
1198
1199 if !caller_ctx.namespace_stack.is_empty() {
1201 parts.extend(caller_ctx.namespace_stack.iter().cloned());
1202 }
1203
1204 parts.push(callee_name.to_string());
1210
1211 parts.join("::")
1212}
1213
1214fn strip_type_qualifiers(type_text: &str) -> String {
1221 let mut result = type_text.trim().to_string();
1222
1223 result = result.replace("const ", "");
1225 result = result.replace("volatile ", "");
1226 result = result.replace("mutable ", "");
1227 result = result.replace("constexpr ", "");
1228
1229 result = result.replace(" const", "");
1231 result = result.replace(" volatile", "");
1232 result = result.replace(" mutable", "");
1233 result = result.replace(" constexpr", "");
1234
1235 result = result.replace(['*', '&'], "");
1237
1238 result = result.trim().to_string();
1240
1241 if let Some(last_part) = result.split("::").last() {
1243 result = last_part.to_string();
1244 }
1245
1246 if let Some(open_bracket) = result.find('<') {
1248 result = result[..open_bracket].to_string();
1249 }
1250
1251 result.trim().to_string()
1252}
1253
1254#[allow(clippy::unnecessary_wraps)]
1256fn process_field_declaration(
1257 node: Node,
1258 content: &[u8],
1259 class_qualified_name: &str,
1260 visibility: &str,
1261 helper: &mut GraphBuildHelper,
1262) -> GraphResult<()> {
1263 let mut field_type_text = None;
1265 let mut field_names = Vec::new();
1266
1267 let mut cursor = node.walk();
1268 for child in node.children(&mut cursor) {
1269 match child.kind() {
1270 "type_identifier" | "primitive_type" => {
1271 if let Ok(text) = child.utf8_text(content) {
1272 field_type_text = Some(text.to_string());
1273 }
1274 }
1275 "qualified_identifier" => {
1276 if let Ok(text) = child.utf8_text(content) {
1278 field_type_text = Some(text.to_string());
1279 }
1280 }
1281 "template_type" => {
1282 if let Ok(text) = child.utf8_text(content) {
1284 field_type_text = Some(text.to_string());
1285 }
1286 }
1287 "sized_type_specifier" => {
1288 if let Ok(text) = child.utf8_text(content) {
1290 field_type_text = Some(text.to_string());
1291 }
1292 }
1293 "type_qualifier" => {
1294 if field_type_text.is_none()
1296 && let Ok(text) = child.utf8_text(content)
1297 {
1298 field_type_text = Some(text.to_string());
1299 }
1300 }
1301 "auto" => {
1302 field_type_text = Some("auto".to_string());
1304 }
1305 "decltype" => {
1306 if let Ok(text) = child.utf8_text(content) {
1308 field_type_text = Some(text.to_string());
1309 }
1310 }
1311 "struct_specifier" | "class_specifier" | "enum_specifier" | "union_specifier" => {
1312 if let Ok(text) = child.utf8_text(content) {
1314 field_type_text = Some(text.to_string());
1315 }
1316 }
1317 "field_identifier" => {
1318 if let Ok(name) = child.utf8_text(content) {
1319 field_names.push(name.trim().to_string());
1320 }
1321 }
1322 "field_declarator"
1323 | "pointer_declarator"
1324 | "reference_declarator"
1325 | "init_declarator" => {
1326 if let Some(name) = extract_field_name(child, content) {
1328 field_names.push(name);
1329 }
1330 }
1331 _ => {}
1332 }
1333 }
1334
1335 if let Some(type_text) = field_type_text {
1337 let base_type = strip_type_qualifiers(&type_text);
1338
1339 for field_name in field_names {
1340 let field_qualified = format!("{class_qualified_name}::{field_name}");
1341 let span = span_from_node(node);
1342
1343 let var_id = helper.add_node_with_visibility(
1345 &field_qualified,
1346 Some(span),
1347 sqry_core::graph::unified::node::NodeKind::Variable,
1348 Some(visibility),
1349 );
1350
1351 let type_id = helper.add_type(&base_type, None);
1353
1354 helper.add_typeof_edge(var_id, type_id);
1356
1357 helper.add_reference_edge(var_id, type_id);
1359 }
1360 }
1361
1362 Ok(())
1363}
1364
1365#[allow(clippy::unnecessary_wraps)]
1367fn process_global_variable_declaration(
1368 node: Node,
1369 content: &[u8],
1370 namespace_stack: &[String],
1371 helper: &mut GraphBuildHelper,
1372) -> GraphResult<()> {
1373 if node.kind() != "declaration" {
1375 return Ok(());
1376 }
1377
1378 let mut cursor_check = node.walk();
1381 for child in node.children(&mut cursor_check) {
1382 if child.kind() == "function_declarator" {
1383 return Ok(());
1384 }
1385 }
1386
1387 let mut type_text = None;
1389 let mut var_names = Vec::new();
1390
1391 let mut cursor = node.walk();
1392 for child in node.children(&mut cursor) {
1393 match child.kind() {
1394 "type_identifier" | "primitive_type" | "qualified_identifier" | "template_type" => {
1395 if let Ok(text) = child.utf8_text(content) {
1396 type_text = Some(text.to_string());
1397 }
1398 }
1399 "init_declarator" => {
1400 if let Some(declarator) = child.child_by_field_name("declarator")
1402 && let Some(name) = extract_declarator_name(declarator, content)
1403 {
1404 var_names.push(name);
1405 }
1406 }
1407 "pointer_declarator" | "reference_declarator" => {
1408 if let Some(name) = extract_declarator_name(child, content) {
1409 var_names.push(name);
1410 }
1411 }
1412 "identifier" => {
1413 if let Ok(name) = child.utf8_text(content) {
1415 var_names.push(name.to_string());
1416 }
1417 }
1418 _ => {}
1419 }
1420 }
1421
1422 if let Some(type_text) = type_text {
1423 let base_type = strip_type_qualifiers(&type_text);
1424
1425 for var_name in var_names {
1426 let qualified = if namespace_stack.is_empty() {
1428 var_name.clone()
1429 } else {
1430 format!("{}::{}", namespace_stack.join("::"), var_name)
1431 };
1432
1433 let span = span_from_node(node);
1434
1435 let var_id = helper.add_node_with_visibility(
1437 &qualified,
1438 Some(span),
1439 sqry_core::graph::unified::node::NodeKind::Variable,
1440 Some("public"),
1441 );
1442
1443 let type_id = helper.add_type(&base_type, None);
1445
1446 helper.add_typeof_edge(var_id, type_id);
1448 helper.add_reference_edge(var_id, type_id);
1449 }
1450 }
1451
1452 Ok(())
1453}
1454
1455fn extract_declarator_name(node: Node, content: &[u8]) -> Option<String> {
1457 match node.kind() {
1458 "identifier" => {
1459 if let Ok(name) = node.utf8_text(content) {
1460 Some(name.to_string())
1461 } else {
1462 None
1463 }
1464 }
1465 "pointer_declarator" | "reference_declarator" | "array_declarator" => {
1466 if let Some(inner) = node.child_by_field_name("declarator") {
1468 extract_declarator_name(inner, content)
1469 } else {
1470 let mut cursor = node.walk();
1472 for child in node.children(&mut cursor) {
1473 if child.kind() == "identifier"
1474 && let Ok(name) = child.utf8_text(content)
1475 {
1476 return Some(name.to_string());
1477 }
1478 }
1479 None
1480 }
1481 }
1482 "init_declarator" => {
1483 if let Some(inner) = node.child_by_field_name("declarator") {
1485 extract_declarator_name(inner, content)
1486 } else {
1487 None
1488 }
1489 }
1490 "field_declarator" => {
1491 if let Some(inner) = node.child_by_field_name("declarator") {
1493 extract_declarator_name(inner, content)
1494 } else {
1495 if let Ok(name) = node.utf8_text(content) {
1497 Some(name.to_string())
1498 } else {
1499 None
1500 }
1501 }
1502 }
1503 _ => None,
1504 }
1505}
1506
1507#[allow(clippy::too_many_arguments)]
1509fn walk_class_body(
1510 body_node: Node,
1511 content: &[u8],
1512 class_qualified_name: &str,
1513 is_struct: bool,
1514 ast_graph: &ASTGraph,
1515 helper: &mut GraphBuildHelper,
1516 seen_includes: &mut HashSet<String>,
1517 namespace_stack: &mut Vec<String>,
1518 class_stack: &mut Vec<String>,
1519 ffi_registry: &FfiRegistry,
1520 pure_virtual_registry: &PureVirtualRegistry,
1521 budget: &mut BuildBudget,
1522) -> GraphResult<()> {
1523 let mut current_visibility = if is_struct { "public" } else { "private" };
1525
1526 let mut cursor = body_node.walk();
1527 for child in body_node.children(&mut cursor) {
1528 budget.checkpoint("cpp:walk_class_body")?;
1529 match child.kind() {
1530 "access_specifier" => {
1531 if let Ok(text) = child.utf8_text(content) {
1533 let spec = text.trim().trim_end_matches(':').trim();
1534 current_visibility = spec;
1535 }
1536 }
1537 "field_declaration" => {
1538 process_field_declaration(
1540 child,
1541 content,
1542 class_qualified_name,
1543 current_visibility,
1544 helper,
1545 )?;
1546 }
1547 "function_definition" => {
1548 if let Some(context) = ast_graph.context_for_start(child.start_byte()) {
1551 let span = span_from_node(child);
1552 helper.add_method_with_signature(
1553 &context.qualified_name,
1554 Some(span),
1555 false, context.is_static,
1557 Some(current_visibility),
1558 context.return_type.as_deref(),
1559 );
1560 }
1561 walk_tree_for_graph(
1563 child,
1564 content,
1565 ast_graph,
1566 helper,
1567 seen_includes,
1568 namespace_stack,
1569 class_stack,
1570 ffi_registry,
1571 pure_virtual_registry,
1572 budget,
1573 )?;
1574 }
1575 _ => {
1576 walk_tree_for_graph(
1578 child,
1579 content,
1580 ast_graph,
1581 helper,
1582 seen_includes,
1583 namespace_stack,
1584 class_stack,
1585 ffi_registry,
1586 pure_virtual_registry,
1587 budget,
1588 )?;
1589 }
1590 }
1591 }
1592
1593 Ok(())
1594}
1595
1596#[allow(clippy::too_many_arguments)]
1598#[allow(clippy::too_many_lines)] fn walk_tree_for_graph(
1600 node: Node,
1601 content: &[u8],
1602 ast_graph: &ASTGraph,
1603 helper: &mut GraphBuildHelper,
1604 seen_includes: &mut HashSet<String>,
1605 namespace_stack: &mut Vec<String>,
1606 class_stack: &mut Vec<String>,
1607 ffi_registry: &FfiRegistry,
1608 pure_virtual_registry: &PureVirtualRegistry,
1609 budget: &mut BuildBudget,
1610) -> GraphResult<()> {
1611 budget.checkpoint("cpp:walk_tree_for_graph")?;
1612 match node.kind() {
1613 "preproc_include" => {
1614 build_import_edge(node, content, helper, seen_includes)?;
1616 }
1617 "linkage_specification" => {
1618 build_ffi_block_for_staging(node, content, helper, namespace_stack);
1620 }
1621 "namespace_definition" => {
1622 if let Some(name_node) = node.child_by_field_name("name")
1624 && let Ok(ns_name) = name_node.utf8_text(content)
1625 {
1626 namespace_stack.push(ns_name.trim().to_string());
1627
1628 let mut cursor = node.walk();
1630 for child in node.children(&mut cursor) {
1631 walk_tree_for_graph(
1632 child,
1633 content,
1634 ast_graph,
1635 helper,
1636 seen_includes,
1637 namespace_stack,
1638 class_stack,
1639 ffi_registry,
1640 pure_virtual_registry,
1641 budget,
1642 )?;
1643 }
1644
1645 namespace_stack.pop();
1646 return Ok(());
1647 }
1648 }
1649 "class_specifier" | "struct_specifier" => {
1650 if let Some(name_node) = node.child_by_field_name("name")
1652 && let Ok(class_name) = name_node.utf8_text(content)
1653 {
1654 let class_name = class_name.trim();
1655 let span = span_from_node(node);
1656 let is_struct = node.kind() == "struct_specifier";
1657
1658 let qualified_class =
1660 build_qualified_name(namespace_stack, class_stack, class_name);
1661
1662 let visibility = "public";
1664 let class_id = if is_struct {
1665 helper.add_struct_with_visibility(
1666 &qualified_class,
1667 Some(span),
1668 Some(visibility),
1669 )
1670 } else {
1671 helper.add_class_with_visibility(&qualified_class, Some(span), Some(visibility))
1672 };
1673
1674 build_inheritance_and_implements_edges(
1677 node,
1678 content,
1679 &qualified_class,
1680 class_id,
1681 helper,
1682 namespace_stack,
1683 pure_virtual_registry,
1684 )?;
1685
1686 if class_stack.is_empty() {
1689 let module_id = helper.add_module(FILE_MODULE_NAME, None);
1690 helper.add_export_edge(module_id, class_id);
1691 }
1692
1693 class_stack.push(class_name.to_string());
1695
1696 if let Some(body) = node.child_by_field_name("body") {
1699 walk_class_body(
1700 body,
1701 content,
1702 &qualified_class,
1703 is_struct,
1704 ast_graph,
1705 helper,
1706 seen_includes,
1707 namespace_stack,
1708 class_stack,
1709 ffi_registry,
1710 pure_virtual_registry,
1711 budget,
1712 )?;
1713 }
1714
1715 class_stack.pop();
1716 return Ok(());
1717 }
1718 }
1719 "enum_specifier" => {
1720 if let Some(name_node) = node.child_by_field_name("name")
1721 && let Ok(enum_name) = name_node.utf8_text(content)
1722 {
1723 let enum_name = enum_name.trim();
1724 let span = span_from_node(node);
1725 let qualified_enum = build_qualified_name(namespace_stack, class_stack, enum_name);
1726 let enum_id = helper.add_enum(&qualified_enum, Some(span));
1727
1728 if class_stack.is_empty() {
1729 let module_id = helper.add_module(FILE_MODULE_NAME, None);
1730 helper.add_export_edge(module_id, enum_id);
1731 }
1732 }
1733 }
1734 "function_definition" => {
1735 if !class_stack.is_empty() {
1739 let mut cursor = node.walk();
1742 for child in node.children(&mut cursor) {
1743 walk_tree_for_graph(
1744 child,
1745 content,
1746 ast_graph,
1747 helper,
1748 seen_includes,
1749 namespace_stack,
1750 class_stack,
1751 ffi_registry,
1752 pure_virtual_registry,
1753 budget,
1754 )?;
1755 }
1756 return Ok(());
1757 }
1758
1759 if let Some(context) = ast_graph.context_for_start(node.start_byte()) {
1761 let span = span_from_node(node);
1762
1763 if context.class_stack.is_empty() {
1765 let visibility = if context.is_static {
1768 "private"
1769 } else {
1770 "public"
1771 };
1772 let fn_id = helper.add_function_with_signature(
1773 &context.qualified_name,
1774 Some(span),
1775 false, false, Some(visibility),
1778 context.return_type.as_deref(),
1779 );
1780
1781 if !context.is_static {
1783 let module_id = helper.add_module(FILE_MODULE_NAME, None);
1784 helper.add_export_edge(module_id, fn_id);
1785 }
1786 } else {
1787 helper.add_method_with_signature(
1792 &context.qualified_name,
1793 Some(span),
1794 false, context.is_static,
1796 Some("public"), context.return_type.as_deref(),
1798 );
1799 }
1800 }
1801 }
1802 "call_expression" => {
1803 if let Ok(Some((caller_qname, callee_qname, argument_count, span))) =
1805 build_call_for_staging(ast_graph, node, content)
1806 {
1807 let caller_function_id = helper.ensure_function(&caller_qname, None, false, false);
1809 let argument_count = u8::try_from(argument_count).unwrap_or(u8::MAX);
1810
1811 let is_unqualified = !callee_qname.contains("::");
1814 if is_unqualified {
1815 if let Some((ffi_qualified, ffi_convention)) = ffi_registry.get(&callee_qname) {
1816 let ffi_target_id =
1818 helper.ensure_function(ffi_qualified, None, false, true);
1819 helper.add_ffi_edge(caller_function_id, ffi_target_id, *ffi_convention);
1820 } else {
1821 let target_function_id =
1823 helper.ensure_function(&callee_qname, None, false, false);
1824 helper.add_call_edge_full_with_span(
1825 caller_function_id,
1826 target_function_id,
1827 argument_count,
1828 false,
1829 vec![span],
1830 );
1831 }
1832 } else {
1833 let target_function_id =
1835 helper.ensure_function(&callee_qname, None, false, false);
1836 helper.add_call_edge_full_with_span(
1837 caller_function_id,
1838 target_function_id,
1839 argument_count,
1840 false,
1841 vec![span],
1842 );
1843 }
1844 }
1845 }
1846 "declaration" => {
1847 if class_stack.is_empty() {
1850 process_global_variable_declaration(node, content, namespace_stack, helper)?;
1851 }
1852 }
1853 _ => {}
1854 }
1855
1856 let mut cursor = node.walk();
1858 for child in node.children(&mut cursor) {
1859 walk_tree_for_graph(
1860 child,
1861 content,
1862 ast_graph,
1863 helper,
1864 seen_includes,
1865 namespace_stack,
1866 class_stack,
1867 ffi_registry,
1868 pure_virtual_registry,
1869 budget,
1870 )?;
1871 }
1872
1873 Ok(())
1874}
1875
1876fn build_call_for_staging(
1878 ast_graph: &ASTGraph,
1879 call_node: Node<'_>,
1880 content: &[u8],
1881) -> GraphResult<Option<(String, String, usize, Span)>> {
1882 let call_context = ast_graph.find_enclosing(call_node.start_byte());
1884 let caller_qualified_name = if let Some(ctx) = call_context {
1885 ctx.qualified_name.clone()
1886 } else {
1887 return Ok(None);
1889 };
1890
1891 let Some(function_node) = call_node.child_by_field_name("function") else {
1892 return Ok(None);
1893 };
1894
1895 let callee_text = function_node
1896 .utf8_text(content)
1897 .map_err(|_| GraphBuilderError::ParseError {
1898 span: span_from_node(call_node),
1899 reason: "failed to read call expression".to_string(),
1900 })?
1901 .trim();
1902
1903 if callee_text.is_empty() {
1904 return Ok(None);
1905 }
1906
1907 let target_qualified_name = if let Some(ctx) = call_context {
1909 resolve_callee_name(callee_text, ctx, ast_graph)
1910 } else {
1911 callee_text.to_string()
1912 };
1913
1914 let span = span_from_node(call_node);
1915 let argument_count = count_arguments(call_node);
1916
1917 Ok(Some((
1918 caller_qualified_name,
1919 target_qualified_name,
1920 argument_count,
1921 span,
1922 )))
1923}
1924
1925fn build_import_edge(
1932 include_node: Node<'_>,
1933 content: &[u8],
1934 helper: &mut GraphBuildHelper,
1935 seen_includes: &mut HashSet<String>,
1936) -> GraphResult<()> {
1937 let path_node = include_node.child_by_field_name("path").or_else(|| {
1939 let mut cursor = include_node.walk();
1941 include_node.children(&mut cursor).find(|child| {
1942 matches!(
1943 child.kind(),
1944 "system_lib_string" | "string_literal" | "string_content"
1945 )
1946 })
1947 });
1948
1949 let Some(path_node) = path_node else {
1950 return Ok(());
1951 };
1952
1953 let include_path = path_node
1954 .utf8_text(content)
1955 .map_err(|_| GraphBuilderError::ParseError {
1956 span: span_from_node(include_node),
1957 reason: "failed to read include path".to_string(),
1958 })?
1959 .trim();
1960
1961 if include_path.is_empty() {
1962 return Ok(());
1963 }
1964
1965 let is_system_include = include_path.starts_with('<') && include_path.ends_with('>');
1967 let cleaned_path = if is_system_include {
1968 include_path.trim_start_matches('<').trim_end_matches('>')
1970 } else {
1971 include_path.trim_start_matches('"').trim_end_matches('"')
1973 };
1974
1975 if cleaned_path.is_empty() {
1976 return Ok(());
1977 }
1978
1979 if !seen_includes.insert(cleaned_path.to_string()) {
1981 return Ok(()); }
1983
1984 let file_module_id = helper.add_module("<file>", None);
1986
1987 let span = span_from_node(include_node);
1989 let import_id = helper.add_import(cleaned_path, Some(span));
1990
1991 helper.add_import_edge(file_module_id, import_id);
1994
1995 Ok(())
1996}
1997
1998fn collect_ffi_declarations(
2009 node: Node<'_>,
2010 content: &[u8],
2011 ffi_registry: &mut FfiRegistry,
2012 budget: &mut BuildBudget,
2013) -> GraphResult<()> {
2014 budget.checkpoint("cpp:collect_ffi_declarations")?;
2015 if node.kind() == "linkage_specification" {
2016 let abi = extract_ffi_abi(node, content);
2018 let convention = abi_to_convention(&abi);
2019
2020 if let Some(body_node) = node.child_by_field_name("body") {
2022 collect_ffi_from_body(body_node, content, &abi, convention, ffi_registry);
2023 }
2024 }
2025
2026 let mut cursor = node.walk();
2028 for child in node.children(&mut cursor) {
2029 collect_ffi_declarations(child, content, ffi_registry, budget)?;
2030 }
2031
2032 Ok(())
2033}
2034
2035fn collect_ffi_from_body(
2037 body_node: Node<'_>,
2038 content: &[u8],
2039 abi: &str,
2040 convention: FfiConvention,
2041 ffi_registry: &mut FfiRegistry,
2042) {
2043 match body_node.kind() {
2044 "declaration_list" => {
2045 let mut cursor = body_node.walk();
2047 for decl in body_node.children(&mut cursor) {
2048 if decl.kind() == "declaration"
2049 && let Some(fn_name) = extract_ffi_function_name(decl, content)
2050 {
2051 let qualified = format!("extern::{abi}::{fn_name}");
2052 ffi_registry.insert(fn_name, (qualified, convention));
2053 }
2054 }
2055 }
2056 "declaration" => {
2057 if let Some(fn_name) = extract_ffi_function_name(body_node, content) {
2059 let qualified = format!("extern::{abi}::{fn_name}");
2060 ffi_registry.insert(fn_name, (qualified, convention));
2061 }
2062 }
2063 _ => {}
2064 }
2065}
2066
2067fn extract_ffi_function_name(decl_node: Node<'_>, content: &[u8]) -> Option<String> {
2069 if let Some(declarator_node) = decl_node.child_by_field_name("declarator") {
2071 return extract_function_name_from_declarator(declarator_node, content);
2072 }
2073 None
2074}
2075
2076fn extract_function_name_from_declarator(node: Node<'_>, content: &[u8]) -> Option<String> {
2078 match node.kind() {
2079 "function_declarator" => {
2080 if let Some(inner) = node.child_by_field_name("declarator") {
2082 return extract_function_name_from_declarator(inner, content);
2083 }
2084 }
2085 "identifier" => {
2086 if let Ok(name) = node.utf8_text(content) {
2088 let name = name.trim();
2089 if !name.is_empty() {
2090 return Some(name.to_string());
2091 }
2092 }
2093 }
2094 "pointer_declarator" | "reference_declarator" => {
2095 if let Some(inner) = node.child_by_field_name("declarator") {
2097 return extract_function_name_from_declarator(inner, content);
2098 }
2099 }
2100 "parenthesized_declarator" => {
2101 let mut cursor = node.walk();
2103 for child in node.children(&mut cursor) {
2104 if let Some(name) = extract_function_name_from_declarator(child, content) {
2105 return Some(name);
2106 }
2107 }
2108 }
2109 _ => {}
2110 }
2111 None
2112}
2113
2114fn extract_ffi_abi(node: Node<'_>, content: &[u8]) -> String {
2118 if let Some(value_node) = node.child_by_field_name("value")
2120 && value_node.kind() == "string_literal"
2121 {
2122 let mut cursor = value_node.walk();
2124 for child in value_node.children(&mut cursor) {
2125 if child.kind() == "string_content"
2126 && let Ok(text) = child.utf8_text(content)
2127 {
2128 let trimmed = text.trim();
2129 if !trimmed.is_empty() {
2130 return trimmed.to_string();
2131 }
2132 }
2133 }
2134 }
2135 "C".to_string()
2137}
2138
2139fn abi_to_convention(abi: &str) -> FfiConvention {
2141 match abi.to_lowercase().as_str() {
2142 "system" => FfiConvention::System,
2143 "stdcall" => FfiConvention::Stdcall,
2144 "fastcall" => FfiConvention::Fastcall,
2145 "cdecl" => FfiConvention::Cdecl,
2146 _ => FfiConvention::C, }
2148}
2149
2150fn build_ffi_block_for_staging(
2154 node: Node<'_>,
2155 content: &[u8],
2156 helper: &mut GraphBuildHelper,
2157 namespace_stack: &[String],
2158) {
2159 let abi = extract_ffi_abi(node, content);
2161
2162 if let Some(body_node) = node.child_by_field_name("body") {
2164 build_ffi_from_body(body_node, content, &abi, helper, namespace_stack);
2165 }
2166}
2167
2168fn build_ffi_from_body(
2170 body_node: Node<'_>,
2171 content: &[u8],
2172 abi: &str,
2173 helper: &mut GraphBuildHelper,
2174 namespace_stack: &[String],
2175) {
2176 match body_node.kind() {
2177 "declaration_list" => {
2178 let mut cursor = body_node.walk();
2180 for decl in body_node.children(&mut cursor) {
2181 if decl.kind() == "declaration"
2182 && let Some(fn_name) = extract_ffi_function_name(decl, content)
2183 {
2184 let span = span_from_node(decl);
2185 let qualified = if namespace_stack.is_empty() {
2187 format!("extern::{abi}::{fn_name}")
2188 } else {
2189 format!("{}::extern::{abi}::{fn_name}", namespace_stack.join("::"))
2190 };
2191 helper.add_function(
2193 &qualified,
2194 Some(span),
2195 false, true, );
2198 }
2199 }
2200 }
2201 "declaration" => {
2202 if let Some(fn_name) = extract_ffi_function_name(body_node, content) {
2204 let span = span_from_node(body_node);
2205 let qualified = if namespace_stack.is_empty() {
2206 format!("extern::{abi}::{fn_name}")
2207 } else {
2208 format!("{}::extern::{abi}::{fn_name}", namespace_stack.join("::"))
2209 };
2210 helper.add_function(&qualified, Some(span), false, true);
2211 }
2212 }
2213 _ => {}
2214 }
2215}
2216
2217fn collect_pure_virtual_interfaces(
2227 node: Node<'_>,
2228 content: &[u8],
2229 registry: &mut PureVirtualRegistry,
2230 budget: &mut BuildBudget,
2231) -> GraphResult<()> {
2232 budget.checkpoint("cpp:collect_pure_virtual_interfaces")?;
2233 if matches!(node.kind(), "class_specifier" | "struct_specifier")
2234 && let Some(name_node) = node.child_by_field_name("name")
2235 && let Ok(class_name) = name_node.utf8_text(content)
2236 {
2237 let class_name = class_name.trim();
2238 if !class_name.is_empty() && has_pure_virtual_methods(node, content) {
2239 registry.insert(class_name.to_string());
2240 }
2241 }
2242
2243 let mut cursor = node.walk();
2245 for child in node.children(&mut cursor) {
2246 collect_pure_virtual_interfaces(child, content, registry, budget)?;
2247 }
2248
2249 Ok(())
2250}
2251
2252fn has_pure_virtual_methods(class_node: Node<'_>, content: &[u8]) -> bool {
2256 if let Some(body) = class_node.child_by_field_name("body") {
2257 let mut cursor = body.walk();
2258 for child in body.children(&mut cursor) {
2259 if child.kind() == "field_declaration" && is_pure_virtual_declaration(child, content) {
2261 return true;
2262 }
2263 }
2264 }
2265 false
2266}
2267
2268fn is_pure_virtual_declaration(decl_node: Node<'_>, content: &[u8]) -> bool {
2270 let mut has_virtual = false;
2271 let mut has_pure_specifier = false;
2272
2273 let mut cursor = decl_node.walk();
2275 for child in decl_node.children(&mut cursor) {
2276 match child.kind() {
2277 "virtual" => {
2278 has_virtual = true;
2279 }
2280 "number_literal" => {
2281 if let Ok(text) = child.utf8_text(content)
2284 && text.trim() == "0"
2285 {
2286 has_pure_specifier = true;
2287 }
2288 }
2289 _ => {}
2290 }
2291 }
2292
2293 has_virtual && has_pure_specifier
2294}
2295
2296fn build_inheritance_and_implements_edges(
2302 class_node: Node<'_>,
2303 content: &[u8],
2304 _qualified_class_name: &str,
2305 child_id: sqry_core::graph::unified::node::NodeId,
2306 helper: &mut GraphBuildHelper,
2307 namespace_stack: &[String],
2308 pure_virtual_registry: &PureVirtualRegistry,
2309) -> GraphResult<()> {
2310 let mut cursor = class_node.walk();
2312 let base_clause = class_node
2313 .children(&mut cursor)
2314 .find(|child| child.kind() == "base_class_clause");
2315
2316 let Some(base_clause) = base_clause else {
2317 return Ok(()); };
2319
2320 let mut clause_cursor = base_clause.walk();
2322 for child in base_clause.children(&mut clause_cursor) {
2323 match child.kind() {
2324 "type_identifier" => {
2325 let base_name = child
2326 .utf8_text(content)
2327 .map_err(|_| GraphBuilderError::ParseError {
2328 span: span_from_node(child),
2329 reason: "failed to read base class name".to_string(),
2330 })?
2331 .trim();
2332
2333 if !base_name.is_empty() {
2334 let qualified_base = if namespace_stack.is_empty() {
2336 base_name.to_string()
2337 } else {
2338 format!("{}::{}", namespace_stack.join("::"), base_name)
2339 };
2340
2341 if pure_virtual_registry.contains(base_name) {
2343 let interface_id = helper.add_interface(&qualified_base, None);
2345 helper.add_implements_edge(child_id, interface_id);
2346 } else {
2347 let parent_id = helper.add_class(&qualified_base, None);
2349 helper.add_inherits_edge(child_id, parent_id);
2350 }
2351 }
2352 }
2353 "qualified_identifier" => {
2354 let base_name = child
2356 .utf8_text(content)
2357 .map_err(|_| GraphBuilderError::ParseError {
2358 span: span_from_node(child),
2359 reason: "failed to read base class name".to_string(),
2360 })?
2361 .trim();
2362
2363 if !base_name.is_empty() {
2364 let simple_name = base_name.rsplit("::").next().unwrap_or(base_name);
2366
2367 if pure_virtual_registry.contains(simple_name) {
2368 let interface_id = helper.add_interface(base_name, None);
2369 helper.add_implements_edge(child_id, interface_id);
2370 } else {
2371 let parent_id = helper.add_class(base_name, None);
2372 helper.add_inherits_edge(child_id, parent_id);
2373 }
2374 }
2375 }
2376 "template_type" => {
2377 if let Some(template_name_node) = child.child_by_field_name("name")
2379 && let Ok(base_name) = template_name_node.utf8_text(content)
2380 {
2381 let base_name = base_name.trim();
2382 if !base_name.is_empty() {
2383 let qualified_base =
2384 if base_name.contains("::") || namespace_stack.is_empty() {
2385 base_name.to_string()
2386 } else {
2387 format!("{}::{}", namespace_stack.join("::"), base_name)
2388 };
2389
2390 if pure_virtual_registry.contains(base_name) {
2393 let interface_id = helper.add_interface(&qualified_base, None);
2394 helper.add_implements_edge(child_id, interface_id);
2395 } else {
2396 let parent_id = helper.add_class(&qualified_base, None);
2397 helper.add_inherits_edge(child_id, parent_id);
2398 }
2399 }
2400 }
2401 }
2402 _ => {
2403 }
2405 }
2406 }
2407
2408 Ok(())
2409}
2410
2411fn span_from_node(node: Node<'_>) -> Span {
2412 let start = node.start_position();
2413 let end = node.end_position();
2414 Span::new(
2415 sqry_core::graph::node::Position::new(start.row, start.column),
2416 sqry_core::graph::node::Position::new(end.row, end.column),
2417 )
2418}
2419
2420fn count_arguments(node: Node<'_>) -> usize {
2421 node.child_by_field_name("arguments").map_or(0, |args| {
2422 let mut count = 0;
2423 let mut cursor = args.walk();
2424 for child in args.children(&mut cursor) {
2425 if !matches!(child.kind(), "(" | ")" | ",") {
2426 count += 1;
2427 }
2428 }
2429 count
2430 })
2431}
2432
2433#[cfg(test)]
2434mod tests {
2435 use super::*;
2436 use sqry_core::graph::unified::build::test_helpers::{
2437 assert_has_node, assert_has_node_with_kind, collect_call_edges,
2438 };
2439 use sqry_core::graph::unified::node::NodeKind;
2440 use tree_sitter::Parser;
2441
2442 fn parse_cpp(source: &str) -> Tree {
2443 let mut parser = Parser::new();
2444 parser
2445 .set_language(&tree_sitter_cpp::LANGUAGE.into())
2446 .expect("Failed to set Cpp language");
2447 parser
2448 .parse(source.as_bytes(), None)
2449 .expect("Failed to parse Cpp source")
2450 }
2451
2452 fn test_budget() -> BuildBudget {
2453 BuildBudget::new(Path::new("test.cpp"))
2454 }
2455
2456 fn extract_namespace_map_for_test(
2457 tree: &Tree,
2458 source: &str,
2459 ) -> HashMap<std::ops::Range<usize>, String> {
2460 let mut budget = test_budget();
2461 extract_namespace_map(tree.root_node(), source.as_bytes(), &mut budget)
2462 .expect("namespace extraction should succeed in tests")
2463 }
2464
2465 fn extract_cpp_contexts_for_test(
2466 tree: &Tree,
2467 source: &str,
2468 namespace_map: &HashMap<std::ops::Range<usize>, String>,
2469 ) -> Vec<FunctionContext> {
2470 let mut budget = test_budget();
2471 extract_cpp_contexts(
2472 tree.root_node(),
2473 source.as_bytes(),
2474 namespace_map,
2475 &mut budget,
2476 )
2477 .expect("context extraction should succeed in tests")
2478 }
2479
2480 fn extract_field_and_type_info_for_test(
2481 tree: &Tree,
2482 source: &str,
2483 namespace_map: &HashMap<std::ops::Range<usize>, String>,
2484 ) -> (QualifiedNameMap, QualifiedNameMap) {
2485 let mut budget = test_budget();
2486 extract_field_and_type_info(
2487 tree.root_node(),
2488 source.as_bytes(),
2489 namespace_map,
2490 &mut budget,
2491 )
2492 .expect("field/type extraction should succeed in tests")
2493 }
2494
2495 #[test]
2496 fn test_build_graph_times_out_with_expired_budget() {
2497 let source = r"
2498 namespace demo {
2499 class Service {
2500 public:
2501 void process() {}
2502 };
2503 }
2504 ";
2505 let tree = parse_cpp(source);
2506 let builder = CppGraphBuilder::new();
2507 let mut staging = StagingGraph::new();
2508 let mut budget = BuildBudget::already_expired(Path::new("timeout.cpp"));
2509
2510 let err = builder
2511 .build_graph_with_budget(
2512 &tree,
2513 source.as_bytes(),
2514 Path::new("timeout.cpp"),
2515 &mut staging,
2516 &mut budget,
2517 )
2518 .expect_err("expired budget should force timeout");
2519
2520 match err {
2521 GraphBuilderError::BuildTimedOut {
2522 file,
2523 phase,
2524 timeout_ms,
2525 } => {
2526 assert_eq!(file, PathBuf::from("timeout.cpp"));
2527 assert_eq!(phase, "cpp:extract_namespace_map");
2528 assert_eq!(timeout_ms, 1_000);
2529 }
2530 other => panic!("expected BuildTimedOut, got {other:?}"),
2531 }
2532 }
2533
2534 #[test]
2535 fn test_extract_class() {
2536 let source = "class User { }";
2537 let tree = parse_cpp(source);
2538 let mut staging = StagingGraph::new();
2539 let builder = CppGraphBuilder::new();
2540
2541 let result = builder.build_graph(
2542 &tree,
2543 source.as_bytes(),
2544 Path::new("test.cpp"),
2545 &mut staging,
2546 );
2547
2548 assert!(result.is_ok());
2549 assert_has_node_with_kind(&staging, "User", NodeKind::Class);
2550 }
2551
2552 #[test]
2553 fn test_extract_template_class() {
2554 let source = r"
2555 template <typename T>
2556 class Person {
2557 public:
2558 T name;
2559 T age;
2560 };
2561 ";
2562 let tree = parse_cpp(source);
2563 let mut staging = StagingGraph::new();
2564 let builder = CppGraphBuilder::new();
2565
2566 let result = builder.build_graph(
2567 &tree,
2568 source.as_bytes(),
2569 Path::new("test.cpp"),
2570 &mut staging,
2571 );
2572
2573 assert!(result.is_ok());
2574 assert_has_node_with_kind(&staging, "Person", NodeKind::Class);
2575 }
2576
2577 #[test]
2578 fn test_extract_function() {
2579 let source = r#"
2580 #include <cstdio>
2581 void hello() {
2582 std::printf("Hello");
2583 }
2584 "#;
2585 let tree = parse_cpp(source);
2586 let mut staging = StagingGraph::new();
2587 let builder = CppGraphBuilder::new();
2588
2589 let result = builder.build_graph(
2590 &tree,
2591 source.as_bytes(),
2592 Path::new("test.cpp"),
2593 &mut staging,
2594 );
2595
2596 assert!(result.is_ok());
2597 assert_has_node_with_kind(&staging, "hello", NodeKind::Function);
2598 }
2599
2600 #[test]
2601 fn test_extract_virtual_function() {
2602 let source = r"
2603 class Service {
2604 public:
2605 virtual void fetchData() {}
2606 };
2607 ";
2608 let tree = parse_cpp(source);
2609 let mut staging = StagingGraph::new();
2610 let builder = CppGraphBuilder::new();
2611
2612 let result = builder.build_graph(
2613 &tree,
2614 source.as_bytes(),
2615 Path::new("test.cpp"),
2616 &mut staging,
2617 );
2618
2619 assert!(result.is_ok());
2620 assert_has_node(&staging, "fetchData");
2621 }
2622
2623 #[test]
2624 fn test_extract_call_edge() {
2625 let source = r"
2626 void greet() {}
2627
2628 int main() {
2629 greet();
2630 return 0;
2631 }
2632 ";
2633 let tree = parse_cpp(source);
2634 let mut staging = StagingGraph::new();
2635 let builder = CppGraphBuilder::new();
2636
2637 let result = builder.build_graph(
2638 &tree,
2639 source.as_bytes(),
2640 Path::new("test.cpp"),
2641 &mut staging,
2642 );
2643
2644 assert!(result.is_ok());
2645 assert_has_node(&staging, "main");
2646 assert_has_node(&staging, "greet");
2647 let calls = collect_call_edges(&staging);
2648 assert!(!calls.is_empty());
2649 }
2650
2651 #[test]
2652 fn test_extract_member_call_edge() {
2653 let source = r"
2654 class Service {
2655 public:
2656 void helper() {}
2657 };
2658
2659 int main() {
2660 Service svc;
2661 svc.helper();
2662 return 0;
2663 }
2664 ";
2665 let tree = parse_cpp(source);
2666 let mut staging = StagingGraph::new();
2667 let builder = CppGraphBuilder::new();
2668
2669 let result = builder.build_graph(
2670 &tree,
2671 source.as_bytes(),
2672 Path::new("member.cpp"),
2673 &mut staging,
2674 );
2675
2676 assert!(result.is_ok());
2677 assert_has_node(&staging, "main");
2678 assert_has_node(&staging, "helper");
2679 let calls = collect_call_edges(&staging);
2680 assert!(!calls.is_empty());
2681 }
2682
2683 #[test]
2684 fn test_extract_namespace_map_simple() {
2685 let source = r"
2686 namespace demo {
2687 void func() {}
2688 }
2689 ";
2690 let tree = parse_cpp(source);
2691 let namespace_map = extract_namespace_map_for_test(&tree, source);
2692
2693 assert_eq!(namespace_map.len(), 1);
2695
2696 let (_, ns_prefix) = namespace_map.iter().next().unwrap();
2698 assert_eq!(ns_prefix, "demo::");
2699 }
2700
2701 #[test]
2702 fn test_extract_namespace_map_nested() {
2703 let source = r"
2704 namespace outer {
2705 namespace inner {
2706 void func() {}
2707 }
2708 }
2709 ";
2710 let tree = parse_cpp(source);
2711 let namespace_map = extract_namespace_map_for_test(&tree, source);
2712
2713 assert!(namespace_map.len() >= 2);
2715
2716 let ns_values: Vec<&String> = namespace_map.values().collect();
2718 assert!(ns_values.iter().any(|v| v.as_str() == "outer::"));
2719 assert!(ns_values.iter().any(|v| v.as_str() == "outer::inner::"));
2720 }
2721
2722 #[test]
2723 fn test_extract_namespace_map_multiple() {
2724 let source = r"
2725 namespace first {
2726 void func1() {}
2727 }
2728 namespace second {
2729 void func2() {}
2730 }
2731 ";
2732 let tree = parse_cpp(source);
2733 let namespace_map = extract_namespace_map_for_test(&tree, source);
2734
2735 assert_eq!(namespace_map.len(), 2);
2737
2738 let ns_values: Vec<&String> = namespace_map.values().collect();
2739 assert!(ns_values.iter().any(|v| v.as_str() == "first::"));
2740 assert!(ns_values.iter().any(|v| v.as_str() == "second::"));
2741 }
2742
2743 #[test]
2744 fn test_find_namespace_for_offset() {
2745 let source = r"
2746 namespace demo {
2747 void func() {}
2748 }
2749 ";
2750 let tree = parse_cpp(source);
2751 let namespace_map = extract_namespace_map_for_test(&tree, source);
2752
2753 let func_offset = source.find("func").unwrap();
2755 let ns = find_namespace_for_offset(func_offset, &namespace_map);
2756 assert_eq!(ns, "demo::");
2757
2758 let ns = find_namespace_for_offset(0, &namespace_map);
2760 assert_eq!(ns, "");
2761 }
2762
2763 #[test]
2764 fn test_extract_cpp_contexts_free_function() {
2765 let source = r"
2766 void helper() {}
2767 ";
2768 let tree = parse_cpp(source);
2769 let namespace_map = extract_namespace_map_for_test(&tree, source);
2770 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
2771
2772 assert_eq!(contexts.len(), 1);
2773 assert_eq!(contexts[0].qualified_name, "helper");
2774 assert!(!contexts[0].is_static);
2775 assert!(!contexts[0].is_virtual);
2776 }
2777
2778 #[test]
2779 fn test_extract_cpp_contexts_namespace_function() {
2780 let source = r"
2781 namespace demo {
2782 void helper() {}
2783 }
2784 ";
2785 let tree = parse_cpp(source);
2786 let namespace_map = extract_namespace_map_for_test(&tree, source);
2787 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
2788
2789 assert_eq!(contexts.len(), 1);
2790 assert_eq!(contexts[0].qualified_name, "demo::helper");
2791 assert_eq!(contexts[0].namespace_stack, vec!["demo"]);
2792 }
2793
2794 #[test]
2795 fn test_extract_cpp_contexts_class_method() {
2796 let source = r"
2797 class Service {
2798 public:
2799 void process() {}
2800 };
2801 ";
2802 let tree = parse_cpp(source);
2803 let namespace_map = extract_namespace_map_for_test(&tree, source);
2804 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
2805
2806 assert_eq!(contexts.len(), 1);
2807 assert_eq!(contexts[0].qualified_name, "Service::process");
2808 assert_eq!(contexts[0].class_stack, vec!["Service"]);
2809 }
2810
2811 #[test]
2812 fn test_extract_cpp_contexts_namespace_and_class() {
2813 let source = r"
2814 namespace demo {
2815 class Service {
2816 public:
2817 void process() {}
2818 };
2819 }
2820 ";
2821 let tree = parse_cpp(source);
2822 let namespace_map = extract_namespace_map_for_test(&tree, source);
2823 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
2824
2825 assert_eq!(contexts.len(), 1);
2826 assert_eq!(contexts[0].qualified_name, "demo::Service::process");
2827 assert_eq!(contexts[0].namespace_stack, vec!["demo"]);
2828 assert_eq!(contexts[0].class_stack, vec!["Service"]);
2829 }
2830
2831 #[test]
2832 fn test_extract_cpp_contexts_static_method() {
2833 let source = r"
2834 class Repository {
2835 public:
2836 static void save() {}
2837 };
2838 ";
2839 let tree = parse_cpp(source);
2840 let namespace_map = extract_namespace_map_for_test(&tree, source);
2841 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
2842
2843 assert_eq!(contexts.len(), 1);
2844 assert_eq!(contexts[0].qualified_name, "Repository::save");
2845 assert!(contexts[0].is_static);
2846 }
2847
2848 #[test]
2849 fn test_extract_cpp_contexts_virtual_method() {
2850 let source = r"
2851 class Base {
2852 public:
2853 virtual void render() {}
2854 };
2855 ";
2856 let tree = parse_cpp(source);
2857 let namespace_map = extract_namespace_map_for_test(&tree, source);
2858 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
2859
2860 assert_eq!(contexts.len(), 1);
2861 assert_eq!(contexts[0].qualified_name, "Base::render");
2862 assert!(contexts[0].is_virtual);
2863 }
2864
2865 #[test]
2866 fn test_extract_cpp_contexts_inline_function() {
2867 let source = r"
2868 inline void helper() {}
2869 ";
2870 let tree = parse_cpp(source);
2871 let namespace_map = extract_namespace_map_for_test(&tree, source);
2872 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
2873
2874 assert_eq!(contexts.len(), 1);
2875 assert_eq!(contexts[0].qualified_name, "helper");
2876 assert!(contexts[0].is_inline);
2877 }
2878
2879 #[test]
2880 fn test_extract_cpp_contexts_out_of_line_definition() {
2881 let source = r"
2882 namespace demo {
2883 class Service {
2884 public:
2885 int process(int v);
2886 };
2887
2888 inline int Service::process(int v) {
2889 return v;
2890 }
2891 }
2892 ";
2893 let tree = parse_cpp(source);
2894 let namespace_map = extract_namespace_map_for_test(&tree, source);
2895 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
2896
2897 assert_eq!(contexts.len(), 1);
2899 assert_eq!(contexts[0].qualified_name, "demo::Service::process");
2900 assert!(contexts[0].is_inline);
2901 }
2902
2903 #[test]
2904 fn test_extract_field_types_simple() {
2905 let source = r"
2906 class Service {
2907 public:
2908 Repository repo;
2909 };
2910 ";
2911 let tree = parse_cpp(source);
2912 let namespace_map = extract_namespace_map_for_test(&tree, source);
2913 let (field_types, _type_map) =
2914 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
2915
2916 assert_eq!(field_types.len(), 1);
2918 assert_eq!(
2919 field_types.get(&("Service".to_string(), "repo".to_string())),
2920 Some(&"Repository".to_string())
2921 );
2922 }
2923
2924 #[test]
2925 fn test_extract_field_types_namespace() {
2926 let source = r"
2927 namespace demo {
2928 class Service {
2929 public:
2930 Repository repo;
2931 };
2932 }
2933 ";
2934 let tree = parse_cpp(source);
2935 let namespace_map = extract_namespace_map_for_test(&tree, source);
2936 let (field_types, _type_map) =
2937 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
2938
2939 assert_eq!(field_types.len(), 1);
2941 assert_eq!(
2942 field_types.get(&("demo::Service".to_string(), "repo".to_string())),
2943 Some(&"Repository".to_string())
2944 );
2945 }
2946
2947 #[test]
2948 fn test_extract_field_types_no_collision() {
2949 let source = r"
2950 class ServiceA {
2951 public:
2952 Repository repo;
2953 };
2954
2955 class ServiceB {
2956 public:
2957 Repository repo;
2958 };
2959 ";
2960 let tree = parse_cpp(source);
2961 let namespace_map = extract_namespace_map_for_test(&tree, source);
2962 let (field_types, _type_map) =
2963 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
2964
2965 assert_eq!(field_types.len(), 2);
2967 assert_eq!(
2968 field_types.get(&("ServiceA".to_string(), "repo".to_string())),
2969 Some(&"Repository".to_string())
2970 );
2971 assert_eq!(
2972 field_types.get(&("ServiceB".to_string(), "repo".to_string())),
2973 Some(&"Repository".to_string())
2974 );
2975 }
2976
2977 #[test]
2978 fn test_extract_using_declaration() {
2979 let source = r"
2980 using std::vector;
2981
2982 class Service {
2983 public:
2984 vector data;
2985 };
2986 ";
2987 let tree = parse_cpp(source);
2988 let namespace_map = extract_namespace_map_for_test(&tree, source);
2989 let (field_types, type_map) =
2990 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
2991
2992 assert_eq!(field_types.len(), 1);
2994 assert_eq!(
2995 field_types.get(&("Service".to_string(), "data".to_string())),
2996 Some(&"std::vector".to_string()),
2997 "Field type should resolve 'vector' to 'std::vector' via using declaration"
2998 );
2999
3000 assert_eq!(
3002 type_map.get(&(String::new(), "vector".to_string())),
3003 Some(&"std::vector".to_string()),
3004 "Using declaration should map 'vector' to 'std::vector' in type_map"
3005 );
3006 }
3007
3008 #[test]
3009 fn test_extract_field_types_pointer() {
3010 let source = r"
3011 class Service {
3012 public:
3013 Repository* repo;
3014 };
3015 ";
3016 let tree = parse_cpp(source);
3017 let namespace_map = extract_namespace_map_for_test(&tree, source);
3018 let (field_types, _type_map) =
3019 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3020
3021 assert_eq!(field_types.len(), 1);
3023 assert_eq!(
3024 field_types.get(&("Service".to_string(), "repo".to_string())),
3025 Some(&"Repository".to_string())
3026 );
3027 }
3028
3029 #[test]
3030 fn test_extract_field_types_multiple_declarators() {
3031 let source = r"
3032 class Service {
3033 public:
3034 Repository repo_a, repo_b, repo_c;
3035 };
3036 ";
3037 let tree = parse_cpp(source);
3038 let namespace_map = extract_namespace_map_for_test(&tree, source);
3039 let (field_types, _type_map) =
3040 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3041
3042 assert_eq!(field_types.len(), 3);
3044 assert_eq!(
3045 field_types.get(&("Service".to_string(), "repo_a".to_string())),
3046 Some(&"Repository".to_string())
3047 );
3048 assert_eq!(
3049 field_types.get(&("Service".to_string(), "repo_b".to_string())),
3050 Some(&"Repository".to_string())
3051 );
3052 assert_eq!(
3053 field_types.get(&("Service".to_string(), "repo_c".to_string())),
3054 Some(&"Repository".to_string())
3055 );
3056 }
3057
3058 #[test]
3059 fn test_extract_field_types_nested_struct_with_parent_field() {
3060 let source = r"
3063 namespace demo {
3064 struct Outer {
3065 int outer_field;
3066 struct Inner {
3067 int inner_field;
3068 };
3069 Inner nested_instance;
3070 };
3071 }
3072 ";
3073 let tree = parse_cpp(source);
3074 let namespace_map = extract_namespace_map_for_test(&tree, source);
3075 let (field_types, _type_map) =
3076 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3077
3078 assert!(
3081 field_types.len() >= 2,
3082 "Expected at least outer_field and nested_instance"
3083 );
3084
3085 assert_eq!(
3087 field_types.get(&("demo::Outer".to_string(), "outer_field".to_string())),
3088 Some(&"int".to_string())
3089 );
3090
3091 assert_eq!(
3093 field_types.get(&("demo::Outer".to_string(), "nested_instance".to_string())),
3094 Some(&"Inner".to_string())
3095 );
3096
3097 if field_types.contains_key(&("demo::Outer::Inner".to_string(), "inner_field".to_string()))
3099 {
3100 assert_eq!(
3102 field_types.get(&("demo::Outer::Inner".to_string(), "inner_field".to_string())),
3103 Some(&"int".to_string()),
3104 "Inner class fields must use parent-qualified FQN 'demo::Outer::Inner'"
3105 );
3106 }
3107 }
3108}