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() - Duration::from_secs(60),
85 checkpoints: BUDGET_CHECK_INTERVAL - 1,
86 }
87 }
88
89 fn checkpoint(&mut self, phase: &'static str) -> GraphResult<()> {
90 self.checkpoints = self.checkpoints.wrapping_add(1);
91 if self.checkpoints.is_multiple_of(BUDGET_CHECK_INTERVAL)
92 && self.started_at.elapsed() > self.phase_timeout
93 {
94 return Err(GraphBuilderError::BuildTimedOut {
95 file: self.file.clone(),
96 phase,
97 timeout_ms: self.phase_timeout.as_millis() as u64,
98 });
99 }
100 Ok(())
101 }
102}
103
104#[allow(dead_code)] trait SpanExt {
107 fn from_node(node: &tree_sitter::Node) -> Self;
108}
109
110impl SpanExt for Span {
111 fn from_node(node: &tree_sitter::Node) -> Self {
112 Span::new(
113 Position::new(node.start_position().row, node.start_position().column),
114 Position::new(node.end_position().row, node.end_position().column),
115 )
116 }
117}
118
119#[derive(Debug)]
131struct ASTGraph {
132 contexts: Vec<FunctionContext>,
134 context_start_index: HashMap<usize, usize>,
136
137 #[allow(dead_code)]
143 field_types: QualifiedNameMap,
144
145 #[allow(dead_code)]
152 type_map: QualifiedNameMap,
153
154 #[allow(dead_code)]
158 namespace_map: HashMap<std::ops::Range<usize>, String>,
159}
160
161impl ASTGraph {
162 fn from_tree(root: Node, content: &[u8], budget: &mut BuildBudget) -> GraphResult<Self> {
164 let namespace_map = extract_namespace_map(root, content, budget)?;
166
167 let mut contexts = extract_cpp_contexts(root, content, &namespace_map, budget)?;
169 contexts.sort_by_key(|ctx| ctx.span.0);
170 let context_start_index = contexts
171 .iter()
172 .enumerate()
173 .map(|(idx, ctx)| (ctx.span.0, idx))
174 .collect();
175
176 let (field_types, type_map) =
178 extract_field_and_type_info(root, content, &namespace_map, budget)?;
179
180 Ok(Self {
181 contexts,
182 context_start_index,
183 field_types,
184 type_map,
185 namespace_map,
186 })
187 }
188
189 fn find_enclosing(&self, byte_pos: usize) -> Option<&FunctionContext> {
195 let insertion_point = self.contexts.partition_point(|ctx| ctx.span.0 <= byte_pos);
196 if insertion_point == 0 {
197 return None;
198 }
199
200 let candidate = &self.contexts[insertion_point - 1];
201 (byte_pos < candidate.span.1).then_some(candidate)
202 }
203
204 fn context_for_start(&self, start_byte: usize) -> Option<&FunctionContext> {
205 self.context_start_index
206 .get(&start_byte)
207 .and_then(|idx| self.contexts.get(*idx))
208 }
209}
210
211#[derive(Debug, Clone)]
213struct FunctionContext {
214 qualified_name: String,
216 span: (usize, usize),
218 is_static: bool,
221 #[allow(dead_code)]
224 is_virtual: bool,
225 #[allow(dead_code)]
228 is_inline: bool,
229 namespace_stack: Vec<String>,
231 #[allow(dead_code)] class_stack: Vec<String>,
235 return_type: Option<String>,
237}
238
239impl FunctionContext {
240 #[allow(dead_code)] fn qualified_name(&self) -> &str {
242 &self.qualified_name
243 }
244}
245
246#[derive(Debug, Default, Clone, Copy)]
270pub struct CppGraphBuilder;
271
272impl CppGraphBuilder {
273 #[must_use]
275 pub fn new() -> Self {
276 Self
277 }
278
279 fn build_graph_with_budget(
280 &self,
281 tree: &Tree,
282 content: &[u8],
283 file: &Path,
284 staging: &mut StagingGraph,
285 budget: &mut BuildBudget,
286 ) -> GraphResult<()> {
287 let mut helper = GraphBuildHelper::new(staging, file, Language::Cpp);
289
290 let ast_graph = ASTGraph::from_tree(tree.root_node(), content, budget)?;
292
293 let mut seen_includes: HashSet<String> = HashSet::new();
295
296 let mut namespace_stack: Vec<String> = Vec::new();
298 let mut class_stack: Vec<String> = Vec::new();
299
300 let mut ffi_registry = FfiRegistry::new();
303 collect_ffi_declarations(tree.root_node(), content, &mut ffi_registry, budget)?;
304
305 let mut pure_virtual_registry = PureVirtualRegistry::new();
307 collect_pure_virtual_interfaces(
308 tree.root_node(),
309 content,
310 &mut pure_virtual_registry,
311 budget,
312 )?;
313
314 walk_tree_for_graph(
316 tree.root_node(),
317 content,
318 &ast_graph,
319 &mut helper,
320 &mut seen_includes,
321 &mut namespace_stack,
322 &mut class_stack,
323 &ffi_registry,
324 &pure_virtual_registry,
325 budget,
326 )?;
327
328 Ok(())
329 }
330
331 #[allow(dead_code)] fn extract_class_attributes(node: &tree_sitter::Node, content: &[u8]) -> Vec<String> {
334 let mut attributes = Vec::new();
335 let mut cursor = node.walk();
336 for child in node.children(&mut cursor) {
337 if child.kind() == "modifiers" {
338 let mut mod_cursor = child.walk();
339 for modifier in child.children(&mut mod_cursor) {
340 if let Ok(mod_text) = modifier.utf8_text(content) {
341 match mod_text {
342 "template" => attributes.push("template".to_string()),
343 "sealed" => attributes.push("sealed".to_string()),
344 "abstract" => attributes.push("abstract".to_string()),
345 "open" => attributes.push("open".to_string()),
346 "final" => attributes.push("final".to_string()),
347 "inner" => attributes.push("inner".to_string()),
348 "value" => attributes.push("value".to_string()),
349 _ => {}
350 }
351 }
352 }
353 }
354 }
355 attributes
356 }
357
358 #[allow(dead_code)] fn extract_is_virtual(node: &tree_sitter::Node, content: &[u8]) -> bool {
361 if let Some(spec) = node.child_by_field_name("declaration_specifiers")
362 && let Ok(text) = spec.utf8_text(content)
363 && text.contains("virtual")
364 {
365 return true;
366 }
367
368 if let Ok(text) = node.utf8_text(content)
369 && text.contains("virtual")
370 {
371 return true;
372 }
373
374 if let Some(parent) = node.parent()
375 && (parent.kind() == "field_declaration" || parent.kind() == "declaration")
376 && let Ok(text) = parent.utf8_text(content)
377 && text.contains("virtual")
378 {
379 return true;
380 }
381
382 false
383 }
384
385 #[allow(dead_code)] fn extract_function_attributes(node: &tree_sitter::Node, content: &[u8]) -> Vec<String> {
388 let mut attributes = Vec::new();
389 for node_ref in [
390 node.child_by_field_name("declaration_specifiers"),
391 node.parent(),
392 ]
393 .into_iter()
394 .flatten()
395 {
396 if let Ok(text) = node_ref.utf8_text(content) {
397 for keyword in [
398 "virtual",
399 "inline",
400 "constexpr",
401 "operator",
402 "override",
403 "static",
404 ] {
405 if text.contains(keyword) && !attributes.contains(&keyword.to_string()) {
406 attributes.push(keyword.to_string());
407 }
408 }
409 }
410 }
411
412 if let Ok(text) = node.utf8_text(content) {
413 for keyword in [
414 "virtual",
415 "inline",
416 "constexpr",
417 "operator",
418 "override",
419 "static",
420 ] {
421 if text.contains(keyword) && !attributes.contains(&keyword.to_string()) {
422 attributes.push(keyword.to_string());
423 }
424 }
425 }
426
427 attributes
428 }
429}
430
431impl GraphBuilder for CppGraphBuilder {
432 fn language(&self) -> Language {
433 Language::Cpp
434 }
435
436 fn build_graph(
437 &self,
438 tree: &Tree,
439 content: &[u8],
440 file: &Path,
441 staging: &mut StagingGraph,
442 ) -> GraphResult<()> {
443 let mut budget = BuildBudget::new(file);
444 self.build_graph_with_budget(tree, content, file, staging, &mut budget)
445 }
446}
447
448fn extract_namespace_map(
460 node: Node,
461 content: &[u8],
462 budget: &mut BuildBudget,
463) -> GraphResult<HashMap<std::ops::Range<usize>, String>> {
464 let mut map = HashMap::new();
465
466 let recursion_limits = sqry_core::config::RecursionLimits::load_or_default()
468 .expect("Failed to load recursion limits");
469 let file_ops_depth = recursion_limits
470 .effective_file_ops_depth()
471 .expect("Invalid file_ops_depth configuration");
472 let mut guard = sqry_core::query::security::RecursionGuard::new(file_ops_depth)
473 .expect("Failed to create recursion guard");
474
475 extract_namespaces_recursive(node, content, "", &mut map, &mut guard, budget).map_err(|e| {
476 match e {
477 timeout @ GraphBuilderError::BuildTimedOut { .. } => timeout,
478 other => GraphBuilderError::ParseError {
479 span: span_from_node(node),
480 reason: format!("C++ namespace extraction failed: {other}"),
481 },
482 }
483 })?;
484
485 Ok(map)
486}
487
488fn extract_namespaces_recursive(
494 node: Node,
495 content: &[u8],
496 current_ns: &str,
497 map: &mut HashMap<std::ops::Range<usize>, String>,
498 guard: &mut sqry_core::query::security::RecursionGuard,
499 budget: &mut BuildBudget,
500) -> GraphResult<()> {
501 budget.checkpoint("cpp:extract_namespace_map")?;
502 guard.enter().map_err(|e| GraphBuilderError::ParseError {
503 span: span_from_node(node),
504 reason: format!("C++ namespace extraction hit recursion limit: {e}"),
505 })?;
506
507 if node.kind() == "namespace_definition" {
508 let ns_name = if let Some(name_node) = node.child_by_field_name("name") {
510 extract_identifier(name_node, content)
511 } else {
512 String::from("anonymous")
514 };
515
516 let new_ns = if current_ns.is_empty() {
518 format!("{ns_name}::")
519 } else {
520 format!("{current_ns}{ns_name}::")
521 };
522
523 if let Some(body) = node.child_by_field_name("body") {
525 let range = body.start_byte()..body.end_byte();
526 map.insert(range, new_ns.clone());
527
528 let mut cursor = body.walk();
530 for child in body.children(&mut cursor) {
531 extract_namespaces_recursive(child, content, &new_ns, map, guard, budget)?;
532 }
533 }
534 } else {
535 let mut cursor = node.walk();
537 for child in node.children(&mut cursor) {
538 extract_namespaces_recursive(child, content, current_ns, map, guard, budget)?;
539 }
540 }
541
542 guard.exit();
543 Ok(())
544}
545
546fn extract_identifier(node: Node, content: &[u8]) -> String {
548 node.utf8_text(content).unwrap_or("").to_string()
549}
550
551fn find_namespace_for_offset(
553 byte_offset: usize,
554 namespace_map: &HashMap<std::ops::Range<usize>, String>,
555) -> String {
556 let mut matching_ranges: Vec<_> = namespace_map
558 .iter()
559 .filter(|(range, _)| range.contains(&byte_offset))
560 .collect();
561
562 matching_ranges.sort_by_key(|(range, _)| range.end - range.start);
564
565 matching_ranges
567 .first()
568 .map_or("", |(_, ns)| ns.as_str())
569 .to_string()
570}
571
572fn extract_cpp_contexts(
579 node: Node,
580 content: &[u8],
581 namespace_map: &HashMap<std::ops::Range<usize>, String>,
582 budget: &mut BuildBudget,
583) -> GraphResult<Vec<FunctionContext>> {
584 let mut contexts = Vec::new();
585 let mut class_stack = Vec::new();
586
587 let recursion_limits = sqry_core::config::RecursionLimits::load_or_default()
589 .expect("Failed to load recursion limits");
590 let file_ops_depth = recursion_limits
591 .effective_file_ops_depth()
592 .expect("Invalid file_ops_depth configuration");
593 let mut guard = sqry_core::query::security::RecursionGuard::new(file_ops_depth)
594 .expect("Failed to create recursion guard");
595
596 extract_contexts_recursive(
597 node,
598 content,
599 namespace_map,
600 &mut contexts,
601 &mut class_stack,
602 &mut guard,
603 budget,
604 )
605 .map_err(|e| match e {
606 timeout @ GraphBuilderError::BuildTimedOut { .. } => timeout,
607 other => GraphBuilderError::ParseError {
608 span: span_from_node(node),
609 reason: format!("C++ context extraction failed: {other}"),
610 },
611 })?;
612
613 Ok(contexts)
614}
615
616fn extract_contexts_recursive(
621 node: Node,
622 content: &[u8],
623 namespace_map: &HashMap<std::ops::Range<usize>, String>,
624 contexts: &mut Vec<FunctionContext>,
625 class_stack: &mut Vec<String>,
626 guard: &mut sqry_core::query::security::RecursionGuard,
627 budget: &mut BuildBudget,
628) -> GraphResult<()> {
629 budget.checkpoint("cpp:extract_contexts")?;
630 guard.enter().map_err(|e| GraphBuilderError::ParseError {
631 span: span_from_node(node),
632 reason: format!("C++ context extraction hit recursion limit: {e}"),
633 })?;
634
635 match node.kind() {
636 "class_specifier" | "struct_specifier" => {
637 if let Some(name_node) = node.child_by_field_name("name") {
639 let class_name = extract_identifier(name_node, content);
640 class_stack.push(class_name);
641
642 if let Some(body) = node.child_by_field_name("body") {
644 let mut cursor = body.walk();
645 for child in body.children(&mut cursor) {
646 extract_contexts_recursive(
647 child,
648 content,
649 namespace_map,
650 contexts,
651 class_stack,
652 guard,
653 budget,
654 )?;
655 }
656 }
657
658 class_stack.pop();
659 }
660 }
661
662 "function_definition" => {
663 if let Some(declarator) = node.child_by_field_name("declarator") {
665 let (func_name, class_prefix) =
666 extract_function_name_with_class(declarator, content);
667
668 let namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
670 let namespace_stack: Vec<String> = if namespace.is_empty() {
671 Vec::new()
672 } else {
673 namespace
674 .trim_end_matches("::")
675 .split("::")
676 .map(String::from)
677 .collect()
678 };
679
680 let effective_class_stack: Vec<String> = if !class_stack.is_empty() {
684 class_stack.clone()
685 } else if let Some(ref prefix) = class_prefix {
686 vec![prefix.clone()]
687 } else {
688 Vec::new()
689 };
690
691 let qualified_name =
693 build_qualified_name(&namespace_stack, &effective_class_stack, &func_name);
694
695 let is_static = is_static_function(node, content);
697 let is_virtual = is_virtual_function(node, content);
698 let is_inline = is_inline_function(node, content);
699
700 let return_type = node
702 .child_by_field_name("type")
703 .and_then(|type_node| type_node.utf8_text(content).ok())
704 .map(std::string::ToString::to_string);
705
706 let span = (node.start_byte(), node.end_byte());
708
709 contexts.push(FunctionContext {
710 qualified_name,
711 span,
712 is_static,
713 is_virtual,
714 is_inline,
715 namespace_stack,
716 class_stack: effective_class_stack,
717 return_type,
718 });
719 }
720
721 }
723
724 _ => {
725 let mut cursor = node.walk();
727 for child in node.children(&mut cursor) {
728 extract_contexts_recursive(
729 child,
730 content,
731 namespace_map,
732 contexts,
733 class_stack,
734 guard,
735 budget,
736 )?;
737 }
738 }
739 }
740
741 guard.exit();
742 Ok(())
743}
744
745fn build_qualified_name(namespace_stack: &[String], class_stack: &[String], name: &str) -> String {
750 let mut parts = Vec::new();
751
752 parts.extend(namespace_stack.iter().cloned());
754
755 for class_name in class_stack {
757 parts.push(class_name.clone());
758 }
759
760 parts.push(name.to_string());
762
763 parts.join("::")
764}
765
766fn extract_function_name_with_class(declarator: Node, content: &[u8]) -> (String, Option<String>) {
771 match declarator.kind() {
779 "function_declarator" => {
780 if let Some(declarator_inner) = declarator.child_by_field_name("declarator") {
782 extract_function_name_with_class(declarator_inner, content)
783 } else {
784 (extract_identifier(declarator, content), None)
785 }
786 }
787 "qualified_identifier" => {
788 let name = if let Some(name_node) = declarator.child_by_field_name("name") {
790 extract_identifier(name_node, content)
791 } else {
792 extract_identifier(declarator, content)
793 };
794
795 let class_prefix = declarator
797 .child_by_field_name("scope")
798 .map(|scope_node| extract_identifier(scope_node, content));
799
800 (name, class_prefix)
801 }
802 "field_identifier" | "identifier" | "destructor_name" | "operator_name" => {
803 (extract_identifier(declarator, content), None)
804 }
805 _ => {
806 (extract_identifier(declarator, content), None)
808 }
809 }
810}
811
812#[allow(dead_code)]
814fn extract_function_name(declarator: Node, content: &[u8]) -> String {
815 extract_function_name_with_class(declarator, content).0
816}
817
818fn is_static_function(node: Node, content: &[u8]) -> bool {
820 has_specifier(node, "static", content)
821}
822
823fn is_virtual_function(node: Node, content: &[u8]) -> bool {
825 has_specifier(node, "virtual", content)
826}
827
828fn is_inline_function(node: Node, content: &[u8]) -> bool {
830 has_specifier(node, "inline", content)
831}
832
833fn has_specifier(node: Node, specifier: &str, content: &[u8]) -> bool {
835 let mut cursor = node.walk();
837 for child in node.children(&mut cursor) {
838 if (child.kind() == "storage_class_specifier"
839 || child.kind() == "type_qualifier"
840 || child.kind() == "virtual"
841 || child.kind() == "inline")
842 && let Ok(text) = child.utf8_text(content)
843 && text == specifier
844 {
845 return true;
846 }
847 }
848 false
849}
850
851fn extract_field_and_type_info(
861 node: Node,
862 content: &[u8],
863 namespace_map: &HashMap<std::ops::Range<usize>, String>,
864 budget: &mut BuildBudget,
865) -> GraphResult<(QualifiedNameMap, QualifiedNameMap)> {
866 let mut field_types = HashMap::new();
867 let mut type_map = HashMap::new();
868 let mut class_stack = Vec::new();
869
870 extract_fields_recursive(
871 node,
872 content,
873 namespace_map,
874 &mut field_types,
875 &mut type_map,
876 &mut class_stack,
877 budget,
878 )?;
879
880 Ok((field_types, type_map))
881}
882
883fn extract_fields_recursive(
885 node: Node,
886 content: &[u8],
887 namespace_map: &HashMap<std::ops::Range<usize>, String>,
888 field_types: &mut HashMap<(String, String), String>,
889 type_map: &mut HashMap<(String, String), String>,
890 class_stack: &mut Vec<String>,
891 budget: &mut BuildBudget,
892) -> GraphResult<()> {
893 budget.checkpoint("cpp:extract_fields")?;
894 match node.kind() {
895 "class_specifier" | "struct_specifier" => {
896 if let Some(name_node) = node.child_by_field_name("name") {
898 let class_name = extract_identifier(name_node, content);
899 let namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
900
901 let class_fqn = if class_stack.is_empty() {
903 if namespace.is_empty() {
905 class_name.clone()
906 } else {
907 format!("{}::{}", namespace.trim_end_matches("::"), class_name)
908 }
909 } else {
910 format!("{}::{}", class_stack.last().unwrap(), class_name)
912 };
913
914 class_stack.push(class_fqn.clone());
915
916 let mut cursor = node.walk();
918 for child in node.children(&mut cursor) {
919 extract_fields_recursive(
920 child,
921 content,
922 namespace_map,
923 field_types,
924 type_map,
925 class_stack,
926 budget,
927 )?;
928 }
929
930 class_stack.pop();
931 }
932 }
933
934 "field_declaration" => {
935 if let Some(class_fqn) = class_stack.last() {
937 extract_field_declaration(
938 node,
939 content,
940 class_fqn,
941 namespace_map,
942 field_types,
943 type_map,
944 );
945 }
946 }
947
948 "using_directive" => {
949 extract_using_directive(node, content, namespace_map, type_map);
951 }
952
953 "using_declaration" => {
954 extract_using_declaration(node, content, namespace_map, type_map);
956 }
957
958 _ => {
959 let mut cursor = node.walk();
961 for child in node.children(&mut cursor) {
962 extract_fields_recursive(
963 child,
964 content,
965 namespace_map,
966 field_types,
967 type_map,
968 class_stack,
969 budget,
970 )?;
971 }
972 }
973 }
974
975 Ok(())
976}
977
978fn extract_field_declaration(
980 node: Node,
981 content: &[u8],
982 class_fqn: &str,
983 namespace_map: &HashMap<std::ops::Range<usize>, String>,
984 field_types: &mut HashMap<(String, String), String>,
985 type_map: &HashMap<(String, String), String>,
986) {
987 let mut field_type = None;
992 let mut field_names = Vec::new();
993
994 let mut cursor = node.walk();
995 for child in node.children(&mut cursor) {
996 match child.kind() {
997 "type_identifier" | "primitive_type" | "qualified_identifier" | "template_type" => {
998 field_type = Some(extract_type_name(child, content));
999 }
1000 "field_identifier" => {
1001 field_names.push(extract_identifier(child, content));
1003 }
1004 "field_declarator"
1005 | "init_declarator"
1006 | "pointer_declarator"
1007 | "reference_declarator"
1008 | "array_declarator" => {
1009 if let Some(name) = extract_field_name(child, content) {
1011 field_names.push(name);
1012 }
1013 }
1014 _ => {}
1015 }
1016 }
1017
1018 if let Some(ftype) = field_type {
1020 let namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
1021 let field_type_fqn = resolve_type_to_fqn(&ftype, &namespace, type_map);
1022
1023 for fname in field_names {
1025 field_types.insert((class_fqn.to_string(), fname), field_type_fqn.clone());
1026 }
1027 }
1028}
1029
1030fn extract_type_name(type_node: Node, content: &[u8]) -> String {
1032 match type_node.kind() {
1033 "type_identifier" | "primitive_type" => extract_identifier(type_node, content),
1034 "qualified_identifier" => {
1035 extract_identifier(type_node, content)
1037 }
1038 "template_type" => {
1039 if let Some(name) = type_node.child_by_field_name("name") {
1041 extract_identifier(name, content)
1042 } else {
1043 extract_identifier(type_node, content)
1044 }
1045 }
1046 _ => {
1047 extract_identifier(type_node, content)
1049 }
1050 }
1051}
1052
1053fn extract_field_name(declarator: Node, content: &[u8]) -> Option<String> {
1055 match declarator.kind() {
1056 "field_declarator" => {
1057 if let Some(declarator_inner) = declarator.child_by_field_name("declarator") {
1059 extract_field_name(declarator_inner, content)
1060 } else {
1061 Some(extract_identifier(declarator, content))
1062 }
1063 }
1064 "field_identifier" | "identifier" => Some(extract_identifier(declarator, content)),
1065 "pointer_declarator" | "reference_declarator" | "array_declarator" => {
1066 if let Some(declarator_inner) = declarator.child_by_field_name("declarator") {
1068 extract_field_name(declarator_inner, content)
1069 } else {
1070 None
1071 }
1072 }
1073 "init_declarator" => {
1074 if let Some(declarator_inner) = declarator.child_by_field_name("declarator") {
1076 extract_field_name(declarator_inner, content)
1077 } else {
1078 None
1079 }
1080 }
1081 _ => None,
1082 }
1083}
1084
1085fn resolve_type_to_fqn(
1087 type_name: &str,
1088 namespace: &str,
1089 type_map: &HashMap<(String, String), String>,
1090) -> String {
1091 if type_name.contains("::") {
1093 return type_name.to_string();
1094 }
1095
1096 let namespace_key = namespace.trim_end_matches("::").to_string();
1098 if let Some(fqn) = type_map.get(&(namespace_key.clone(), type_name.to_string())) {
1099 return fqn.clone();
1100 }
1101
1102 if let Some(fqn) = type_map.get(&(String::new(), type_name.to_string())) {
1104 return fqn.clone();
1105 }
1106
1107 type_name.to_string()
1109}
1110
1111fn extract_using_directive(
1113 node: Node,
1114 content: &[u8],
1115 namespace_map: &HashMap<std::ops::Range<usize>, String>,
1116 _type_map: &mut HashMap<(String, String), String>,
1117) {
1118 let _namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
1122
1123 if let Some(name_node) = node.child_by_field_name("name") {
1125 let _using_ns = extract_identifier(name_node, content);
1126 }
1130}
1131
1132fn extract_using_declaration(
1137 node: Node,
1138 content: &[u8],
1139 namespace_map: &HashMap<std::ops::Range<usize>, String>,
1140 type_map: &mut HashMap<(String, String), String>,
1141) {
1142 let namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
1143 let namespace_key = namespace.trim_end_matches("::").to_string();
1144
1145 let mut cursor = node.walk();
1147 for child in node.children(&mut cursor) {
1148 if child.kind() == "qualified_identifier" || child.kind() == "identifier" {
1149 let fqn = extract_identifier(child, content);
1150
1151 if let Some(simple_name) = fqn.split("::").last() {
1153 type_map.insert((namespace_key, simple_name.to_string()), fqn);
1155 }
1156 break;
1157 }
1158 }
1159}
1160
1161fn resolve_callee_name(
1173 callee_name: &str,
1174 caller_ctx: &FunctionContext,
1175 _ast_graph: &ASTGraph,
1176) -> String {
1177 if callee_name.starts_with("::") {
1179 return callee_name.trim_start_matches("::").to_string();
1180 }
1181
1182 if callee_name.contains("::") {
1184 if !caller_ctx.namespace_stack.is_empty() {
1186 let namespace_prefix = caller_ctx.namespace_stack.join("::");
1187 return format!("{namespace_prefix}::{callee_name}");
1188 }
1189 return callee_name.to_string();
1190 }
1191
1192 let mut parts = Vec::new();
1194
1195 if !caller_ctx.namespace_stack.is_empty() {
1197 parts.extend(caller_ctx.namespace_stack.iter().cloned());
1198 }
1199
1200 parts.push(callee_name.to_string());
1206
1207 parts.join("::")
1208}
1209
1210fn strip_type_qualifiers(type_text: &str) -> String {
1217 let mut result = type_text.trim().to_string();
1218
1219 result = result.replace("const ", "");
1221 result = result.replace("volatile ", "");
1222 result = result.replace("mutable ", "");
1223 result = result.replace("constexpr ", "");
1224
1225 result = result.replace(" const", "");
1227 result = result.replace(" volatile", "");
1228 result = result.replace(" mutable", "");
1229 result = result.replace(" constexpr", "");
1230
1231 result = result.replace(['*', '&'], "");
1233
1234 result = result.trim().to_string();
1236
1237 if let Some(last_part) = result.split("::").last() {
1239 result = last_part.to_string();
1240 }
1241
1242 if let Some(open_bracket) = result.find('<') {
1244 result = result[..open_bracket].to_string();
1245 }
1246
1247 result.trim().to_string()
1248}
1249
1250#[allow(clippy::unnecessary_wraps)]
1252fn process_field_declaration(
1253 node: Node,
1254 content: &[u8],
1255 class_qualified_name: &str,
1256 visibility: &str,
1257 helper: &mut GraphBuildHelper,
1258) -> GraphResult<()> {
1259 let mut field_type_text = None;
1261 let mut field_names = Vec::new();
1262
1263 let mut cursor = node.walk();
1264 for child in node.children(&mut cursor) {
1265 match child.kind() {
1266 "type_identifier" | "primitive_type" => {
1267 if let Ok(text) = child.utf8_text(content) {
1268 field_type_text = Some(text.to_string());
1269 }
1270 }
1271 "qualified_identifier" => {
1272 if let Ok(text) = child.utf8_text(content) {
1274 field_type_text = Some(text.to_string());
1275 }
1276 }
1277 "template_type" => {
1278 if let Ok(text) = child.utf8_text(content) {
1280 field_type_text = Some(text.to_string());
1281 }
1282 }
1283 "sized_type_specifier" => {
1284 if let Ok(text) = child.utf8_text(content) {
1286 field_type_text = Some(text.to_string());
1287 }
1288 }
1289 "type_qualifier" => {
1290 if field_type_text.is_none()
1292 && let Ok(text) = child.utf8_text(content)
1293 {
1294 field_type_text = Some(text.to_string());
1295 }
1296 }
1297 "auto" => {
1298 field_type_text = Some("auto".to_string());
1300 }
1301 "decltype" => {
1302 if let Ok(text) = child.utf8_text(content) {
1304 field_type_text = Some(text.to_string());
1305 }
1306 }
1307 "struct_specifier" | "class_specifier" | "enum_specifier" | "union_specifier" => {
1308 if let Ok(text) = child.utf8_text(content) {
1310 field_type_text = Some(text.to_string());
1311 }
1312 }
1313 "field_identifier" => {
1314 if let Ok(name) = child.utf8_text(content) {
1315 field_names.push(name.trim().to_string());
1316 }
1317 }
1318 "field_declarator"
1319 | "pointer_declarator"
1320 | "reference_declarator"
1321 | "init_declarator" => {
1322 if let Some(name) = extract_field_name(child, content) {
1324 field_names.push(name);
1325 }
1326 }
1327 _ => {}
1328 }
1329 }
1330
1331 if let Some(type_text) = field_type_text {
1333 let base_type = strip_type_qualifiers(&type_text);
1334
1335 for field_name in field_names {
1336 let field_qualified = format!("{class_qualified_name}::{field_name}");
1337 let span = span_from_node(node);
1338
1339 let var_id = helper.add_node_with_visibility(
1341 &field_qualified,
1342 Some(span),
1343 sqry_core::graph::unified::node::NodeKind::Variable,
1344 Some(visibility),
1345 );
1346
1347 let type_id = helper.add_type(&base_type, None);
1349
1350 helper.add_typeof_edge(var_id, type_id);
1352
1353 helper.add_reference_edge(var_id, type_id);
1355 }
1356 }
1357
1358 Ok(())
1359}
1360
1361#[allow(clippy::unnecessary_wraps)]
1363fn process_global_variable_declaration(
1364 node: Node,
1365 content: &[u8],
1366 namespace_stack: &[String],
1367 helper: &mut GraphBuildHelper,
1368) -> GraphResult<()> {
1369 if node.kind() != "declaration" {
1371 return Ok(());
1372 }
1373
1374 let mut cursor_check = node.walk();
1377 for child in node.children(&mut cursor_check) {
1378 if child.kind() == "function_declarator" {
1379 return Ok(());
1380 }
1381 }
1382
1383 let mut type_text = None;
1385 let mut var_names = Vec::new();
1386
1387 let mut cursor = node.walk();
1388 for child in node.children(&mut cursor) {
1389 match child.kind() {
1390 "type_identifier" | "primitive_type" | "qualified_identifier" | "template_type" => {
1391 if let Ok(text) = child.utf8_text(content) {
1392 type_text = Some(text.to_string());
1393 }
1394 }
1395 "init_declarator" => {
1396 if let Some(declarator) = child.child_by_field_name("declarator")
1398 && let Some(name) = extract_declarator_name(declarator, content)
1399 {
1400 var_names.push(name);
1401 }
1402 }
1403 "pointer_declarator" | "reference_declarator" => {
1404 if let Some(name) = extract_declarator_name(child, content) {
1405 var_names.push(name);
1406 }
1407 }
1408 "identifier" => {
1409 if let Ok(name) = child.utf8_text(content) {
1411 var_names.push(name.to_string());
1412 }
1413 }
1414 _ => {}
1415 }
1416 }
1417
1418 if let Some(type_text) = type_text {
1419 let base_type = strip_type_qualifiers(&type_text);
1420
1421 for var_name in var_names {
1422 let qualified = if namespace_stack.is_empty() {
1424 var_name.clone()
1425 } else {
1426 format!("{}::{}", namespace_stack.join("::"), var_name)
1427 };
1428
1429 let span = span_from_node(node);
1430
1431 let var_id = helper.add_node_with_visibility(
1433 &qualified,
1434 Some(span),
1435 sqry_core::graph::unified::node::NodeKind::Variable,
1436 Some("public"),
1437 );
1438
1439 let type_id = helper.add_type(&base_type, None);
1441
1442 helper.add_typeof_edge(var_id, type_id);
1444 helper.add_reference_edge(var_id, type_id);
1445 }
1446 }
1447
1448 Ok(())
1449}
1450
1451fn extract_declarator_name(node: Node, content: &[u8]) -> Option<String> {
1453 match node.kind() {
1454 "identifier" => {
1455 if let Ok(name) = node.utf8_text(content) {
1456 Some(name.to_string())
1457 } else {
1458 None
1459 }
1460 }
1461 "pointer_declarator" | "reference_declarator" | "array_declarator" => {
1462 if let Some(inner) = node.child_by_field_name("declarator") {
1464 extract_declarator_name(inner, content)
1465 } else {
1466 let mut cursor = node.walk();
1468 for child in node.children(&mut cursor) {
1469 if child.kind() == "identifier"
1470 && let Ok(name) = child.utf8_text(content)
1471 {
1472 return Some(name.to_string());
1473 }
1474 }
1475 None
1476 }
1477 }
1478 "init_declarator" => {
1479 if let Some(inner) = node.child_by_field_name("declarator") {
1481 extract_declarator_name(inner, content)
1482 } else {
1483 None
1484 }
1485 }
1486 "field_declarator" => {
1487 if let Some(inner) = node.child_by_field_name("declarator") {
1489 extract_declarator_name(inner, content)
1490 } else {
1491 if let Ok(name) = node.utf8_text(content) {
1493 Some(name.to_string())
1494 } else {
1495 None
1496 }
1497 }
1498 }
1499 _ => None,
1500 }
1501}
1502
1503#[allow(clippy::too_many_arguments)]
1505fn walk_class_body(
1506 body_node: Node,
1507 content: &[u8],
1508 class_qualified_name: &str,
1509 is_struct: bool,
1510 ast_graph: &ASTGraph,
1511 helper: &mut GraphBuildHelper,
1512 seen_includes: &mut HashSet<String>,
1513 namespace_stack: &mut Vec<String>,
1514 class_stack: &mut Vec<String>,
1515 ffi_registry: &FfiRegistry,
1516 pure_virtual_registry: &PureVirtualRegistry,
1517 budget: &mut BuildBudget,
1518) -> GraphResult<()> {
1519 let mut current_visibility = if is_struct { "public" } else { "private" };
1521
1522 let mut cursor = body_node.walk();
1523 for child in body_node.children(&mut cursor) {
1524 budget.checkpoint("cpp:walk_class_body")?;
1525 match child.kind() {
1526 "access_specifier" => {
1527 if let Ok(text) = child.utf8_text(content) {
1529 let spec = text.trim().trim_end_matches(':').trim();
1530 current_visibility = spec;
1531 }
1532 }
1533 "field_declaration" => {
1534 process_field_declaration(
1536 child,
1537 content,
1538 class_qualified_name,
1539 current_visibility,
1540 helper,
1541 )?;
1542 }
1543 "function_definition" => {
1544 if let Some(context) = ast_graph.context_for_start(child.start_byte()) {
1547 let span = span_from_node(child);
1548 helper.add_method_with_signature(
1549 &context.qualified_name,
1550 Some(span),
1551 false, context.is_static,
1553 Some(current_visibility),
1554 context.return_type.as_deref(),
1555 );
1556 }
1557 walk_tree_for_graph(
1559 child,
1560 content,
1561 ast_graph,
1562 helper,
1563 seen_includes,
1564 namespace_stack,
1565 class_stack,
1566 ffi_registry,
1567 pure_virtual_registry,
1568 budget,
1569 )?;
1570 }
1571 _ => {
1572 walk_tree_for_graph(
1574 child,
1575 content,
1576 ast_graph,
1577 helper,
1578 seen_includes,
1579 namespace_stack,
1580 class_stack,
1581 ffi_registry,
1582 pure_virtual_registry,
1583 budget,
1584 )?;
1585 }
1586 }
1587 }
1588
1589 Ok(())
1590}
1591
1592#[allow(clippy::too_many_arguments)]
1594#[allow(clippy::too_many_lines)] fn walk_tree_for_graph(
1596 node: Node,
1597 content: &[u8],
1598 ast_graph: &ASTGraph,
1599 helper: &mut GraphBuildHelper,
1600 seen_includes: &mut HashSet<String>,
1601 namespace_stack: &mut Vec<String>,
1602 class_stack: &mut Vec<String>,
1603 ffi_registry: &FfiRegistry,
1604 pure_virtual_registry: &PureVirtualRegistry,
1605 budget: &mut BuildBudget,
1606) -> GraphResult<()> {
1607 budget.checkpoint("cpp:walk_tree_for_graph")?;
1608 match node.kind() {
1609 "preproc_include" => {
1610 build_import_edge(node, content, helper, seen_includes)?;
1612 }
1613 "linkage_specification" => {
1614 build_ffi_block_for_staging(node, content, helper, namespace_stack);
1616 }
1617 "namespace_definition" => {
1618 if let Some(name_node) = node.child_by_field_name("name")
1620 && let Ok(ns_name) = name_node.utf8_text(content)
1621 {
1622 namespace_stack.push(ns_name.trim().to_string());
1623
1624 let mut cursor = node.walk();
1626 for child in node.children(&mut cursor) {
1627 walk_tree_for_graph(
1628 child,
1629 content,
1630 ast_graph,
1631 helper,
1632 seen_includes,
1633 namespace_stack,
1634 class_stack,
1635 ffi_registry,
1636 pure_virtual_registry,
1637 budget,
1638 )?;
1639 }
1640
1641 namespace_stack.pop();
1642 return Ok(());
1643 }
1644 }
1645 "class_specifier" | "struct_specifier" => {
1646 if let Some(name_node) = node.child_by_field_name("name")
1648 && let Ok(class_name) = name_node.utf8_text(content)
1649 {
1650 let class_name = class_name.trim();
1651 let span = span_from_node(node);
1652 let is_struct = node.kind() == "struct_specifier";
1653
1654 let qualified_class =
1656 build_qualified_name(namespace_stack, class_stack, class_name);
1657
1658 let visibility = "public";
1660 let class_id = if is_struct {
1661 helper.add_struct_with_visibility(
1662 &qualified_class,
1663 Some(span),
1664 Some(visibility),
1665 )
1666 } else {
1667 helper.add_class_with_visibility(&qualified_class, Some(span), Some(visibility))
1668 };
1669
1670 build_inheritance_and_implements_edges(
1673 node,
1674 content,
1675 &qualified_class,
1676 class_id,
1677 helper,
1678 namespace_stack,
1679 pure_virtual_registry,
1680 )?;
1681
1682 if class_stack.is_empty() {
1685 let module_id = helper.add_module(FILE_MODULE_NAME, None);
1686 helper.add_export_edge(module_id, class_id);
1687 }
1688
1689 class_stack.push(class_name.to_string());
1691
1692 if let Some(body) = node.child_by_field_name("body") {
1695 walk_class_body(
1696 body,
1697 content,
1698 &qualified_class,
1699 is_struct,
1700 ast_graph,
1701 helper,
1702 seen_includes,
1703 namespace_stack,
1704 class_stack,
1705 ffi_registry,
1706 pure_virtual_registry,
1707 budget,
1708 )?;
1709 }
1710
1711 class_stack.pop();
1712 return Ok(());
1713 }
1714 }
1715 "enum_specifier" => {
1716 if let Some(name_node) = node.child_by_field_name("name")
1717 && let Ok(enum_name) = name_node.utf8_text(content)
1718 {
1719 let enum_name = enum_name.trim();
1720 let span = span_from_node(node);
1721 let qualified_enum = build_qualified_name(namespace_stack, class_stack, enum_name);
1722 let enum_id = helper.add_enum(&qualified_enum, Some(span));
1723
1724 if class_stack.is_empty() {
1725 let module_id = helper.add_module(FILE_MODULE_NAME, None);
1726 helper.add_export_edge(module_id, enum_id);
1727 }
1728 }
1729 }
1730 "function_definition" => {
1731 if !class_stack.is_empty() {
1735 let mut cursor = node.walk();
1738 for child in node.children(&mut cursor) {
1739 walk_tree_for_graph(
1740 child,
1741 content,
1742 ast_graph,
1743 helper,
1744 seen_includes,
1745 namespace_stack,
1746 class_stack,
1747 ffi_registry,
1748 pure_virtual_registry,
1749 budget,
1750 )?;
1751 }
1752 return Ok(());
1753 }
1754
1755 if let Some(context) = ast_graph.context_for_start(node.start_byte()) {
1757 let span = span_from_node(node);
1758
1759 if context.class_stack.is_empty() {
1761 let visibility = if context.is_static {
1764 "private"
1765 } else {
1766 "public"
1767 };
1768 let fn_id = helper.add_function_with_signature(
1769 &context.qualified_name,
1770 Some(span),
1771 false, false, Some(visibility),
1774 context.return_type.as_deref(),
1775 );
1776
1777 if !context.is_static {
1779 let module_id = helper.add_module(FILE_MODULE_NAME, None);
1780 helper.add_export_edge(module_id, fn_id);
1781 }
1782 } else {
1783 helper.add_method_with_signature(
1788 &context.qualified_name,
1789 Some(span),
1790 false, context.is_static,
1792 Some("public"), context.return_type.as_deref(),
1794 );
1795 }
1796 }
1797 }
1798 "call_expression" => {
1799 if let Ok(Some((caller_qname, callee_qname, argument_count, span))) =
1801 build_call_for_staging(ast_graph, node, content)
1802 {
1803 let caller_function_id = helper.ensure_function(&caller_qname, None, false, false);
1805 let argument_count = u8::try_from(argument_count).unwrap_or(u8::MAX);
1806
1807 let is_unqualified = !callee_qname.contains("::");
1810 if is_unqualified {
1811 if let Some((ffi_qualified, ffi_convention)) = ffi_registry.get(&callee_qname) {
1812 let ffi_target_id =
1814 helper.ensure_function(ffi_qualified, None, false, true);
1815 helper.add_ffi_edge(caller_function_id, ffi_target_id, *ffi_convention);
1816 } else {
1817 let target_function_id =
1819 helper.ensure_function(&callee_qname, None, false, false);
1820 helper.add_call_edge_full_with_span(
1821 caller_function_id,
1822 target_function_id,
1823 argument_count,
1824 false,
1825 vec![span],
1826 );
1827 }
1828 } else {
1829 let target_function_id =
1831 helper.ensure_function(&callee_qname, None, false, false);
1832 helper.add_call_edge_full_with_span(
1833 caller_function_id,
1834 target_function_id,
1835 argument_count,
1836 false,
1837 vec![span],
1838 );
1839 }
1840 }
1841 }
1842 "declaration" => {
1843 if class_stack.is_empty() {
1846 process_global_variable_declaration(node, content, namespace_stack, helper)?;
1847 }
1848 }
1849 _ => {}
1850 }
1851
1852 let mut cursor = node.walk();
1854 for child in node.children(&mut cursor) {
1855 walk_tree_for_graph(
1856 child,
1857 content,
1858 ast_graph,
1859 helper,
1860 seen_includes,
1861 namespace_stack,
1862 class_stack,
1863 ffi_registry,
1864 pure_virtual_registry,
1865 budget,
1866 )?;
1867 }
1868
1869 Ok(())
1870}
1871
1872fn build_call_for_staging(
1874 ast_graph: &ASTGraph,
1875 call_node: Node<'_>,
1876 content: &[u8],
1877) -> GraphResult<Option<(String, String, usize, Span)>> {
1878 let call_context = ast_graph.find_enclosing(call_node.start_byte());
1880 let caller_qualified_name = if let Some(ctx) = call_context {
1881 ctx.qualified_name.clone()
1882 } else {
1883 return Ok(None);
1885 };
1886
1887 let Some(function_node) = call_node.child_by_field_name("function") else {
1888 return Ok(None);
1889 };
1890
1891 let callee_text = function_node
1892 .utf8_text(content)
1893 .map_err(|_| GraphBuilderError::ParseError {
1894 span: span_from_node(call_node),
1895 reason: "failed to read call expression".to_string(),
1896 })?
1897 .trim();
1898
1899 if callee_text.is_empty() {
1900 return Ok(None);
1901 }
1902
1903 let target_qualified_name = if let Some(ctx) = call_context {
1905 resolve_callee_name(callee_text, ctx, ast_graph)
1906 } else {
1907 callee_text.to_string()
1908 };
1909
1910 let span = span_from_node(call_node);
1911 let argument_count = count_arguments(call_node);
1912
1913 Ok(Some((
1914 caller_qualified_name,
1915 target_qualified_name,
1916 argument_count,
1917 span,
1918 )))
1919}
1920
1921fn build_import_edge(
1928 include_node: Node<'_>,
1929 content: &[u8],
1930 helper: &mut GraphBuildHelper,
1931 seen_includes: &mut HashSet<String>,
1932) -> GraphResult<()> {
1933 let path_node = include_node.child_by_field_name("path").or_else(|| {
1935 let mut cursor = include_node.walk();
1937 include_node.children(&mut cursor).find(|child| {
1938 matches!(
1939 child.kind(),
1940 "system_lib_string" | "string_literal" | "string_content"
1941 )
1942 })
1943 });
1944
1945 let Some(path_node) = path_node else {
1946 return Ok(());
1947 };
1948
1949 let include_path = path_node
1950 .utf8_text(content)
1951 .map_err(|_| GraphBuilderError::ParseError {
1952 span: span_from_node(include_node),
1953 reason: "failed to read include path".to_string(),
1954 })?
1955 .trim();
1956
1957 if include_path.is_empty() {
1958 return Ok(());
1959 }
1960
1961 let is_system_include = include_path.starts_with('<') && include_path.ends_with('>');
1963 let cleaned_path = if is_system_include {
1964 include_path.trim_start_matches('<').trim_end_matches('>')
1966 } else {
1967 include_path.trim_start_matches('"').trim_end_matches('"')
1969 };
1970
1971 if cleaned_path.is_empty() {
1972 return Ok(());
1973 }
1974
1975 if !seen_includes.insert(cleaned_path.to_string()) {
1977 return Ok(()); }
1979
1980 let file_module_id = helper.add_module("<file>", None);
1982
1983 let span = span_from_node(include_node);
1985 let import_id = helper.add_import(cleaned_path, Some(span));
1986
1987 helper.add_import_edge(file_module_id, import_id);
1990
1991 Ok(())
1992}
1993
1994fn collect_ffi_declarations(
2005 node: Node<'_>,
2006 content: &[u8],
2007 ffi_registry: &mut FfiRegistry,
2008 budget: &mut BuildBudget,
2009) -> GraphResult<()> {
2010 budget.checkpoint("cpp:collect_ffi_declarations")?;
2011 if node.kind() == "linkage_specification" {
2012 let abi = extract_ffi_abi(node, content);
2014 let convention = abi_to_convention(&abi);
2015
2016 if let Some(body_node) = node.child_by_field_name("body") {
2018 collect_ffi_from_body(body_node, content, &abi, convention, ffi_registry);
2019 }
2020 }
2021
2022 let mut cursor = node.walk();
2024 for child in node.children(&mut cursor) {
2025 collect_ffi_declarations(child, content, ffi_registry, budget)?;
2026 }
2027
2028 Ok(())
2029}
2030
2031fn collect_ffi_from_body(
2033 body_node: Node<'_>,
2034 content: &[u8],
2035 abi: &str,
2036 convention: FfiConvention,
2037 ffi_registry: &mut FfiRegistry,
2038) {
2039 match body_node.kind() {
2040 "declaration_list" => {
2041 let mut cursor = body_node.walk();
2043 for decl in body_node.children(&mut cursor) {
2044 if decl.kind() == "declaration"
2045 && let Some(fn_name) = extract_ffi_function_name(decl, content)
2046 {
2047 let qualified = format!("extern::{abi}::{fn_name}");
2048 ffi_registry.insert(fn_name, (qualified, convention));
2049 }
2050 }
2051 }
2052 "declaration" => {
2053 if let Some(fn_name) = extract_ffi_function_name(body_node, content) {
2055 let qualified = format!("extern::{abi}::{fn_name}");
2056 ffi_registry.insert(fn_name, (qualified, convention));
2057 }
2058 }
2059 _ => {}
2060 }
2061}
2062
2063fn extract_ffi_function_name(decl_node: Node<'_>, content: &[u8]) -> Option<String> {
2065 if let Some(declarator_node) = decl_node.child_by_field_name("declarator") {
2067 return extract_function_name_from_declarator(declarator_node, content);
2068 }
2069 None
2070}
2071
2072fn extract_function_name_from_declarator(node: Node<'_>, content: &[u8]) -> Option<String> {
2074 match node.kind() {
2075 "function_declarator" => {
2076 if let Some(inner) = node.child_by_field_name("declarator") {
2078 return extract_function_name_from_declarator(inner, content);
2079 }
2080 }
2081 "identifier" => {
2082 if let Ok(name) = node.utf8_text(content) {
2084 let name = name.trim();
2085 if !name.is_empty() {
2086 return Some(name.to_string());
2087 }
2088 }
2089 }
2090 "pointer_declarator" | "reference_declarator" => {
2091 if let Some(inner) = node.child_by_field_name("declarator") {
2093 return extract_function_name_from_declarator(inner, content);
2094 }
2095 }
2096 "parenthesized_declarator" => {
2097 let mut cursor = node.walk();
2099 for child in node.children(&mut cursor) {
2100 if let Some(name) = extract_function_name_from_declarator(child, content) {
2101 return Some(name);
2102 }
2103 }
2104 }
2105 _ => {}
2106 }
2107 None
2108}
2109
2110fn extract_ffi_abi(node: Node<'_>, content: &[u8]) -> String {
2114 if let Some(value_node) = node.child_by_field_name("value")
2116 && value_node.kind() == "string_literal"
2117 {
2118 let mut cursor = value_node.walk();
2120 for child in value_node.children(&mut cursor) {
2121 if child.kind() == "string_content"
2122 && let Ok(text) = child.utf8_text(content)
2123 {
2124 let trimmed = text.trim();
2125 if !trimmed.is_empty() {
2126 return trimmed.to_string();
2127 }
2128 }
2129 }
2130 }
2131 "C".to_string()
2133}
2134
2135fn abi_to_convention(abi: &str) -> FfiConvention {
2137 match abi.to_lowercase().as_str() {
2138 "system" => FfiConvention::System,
2139 "stdcall" => FfiConvention::Stdcall,
2140 "fastcall" => FfiConvention::Fastcall,
2141 "cdecl" => FfiConvention::Cdecl,
2142 _ => FfiConvention::C, }
2144}
2145
2146fn build_ffi_block_for_staging(
2150 node: Node<'_>,
2151 content: &[u8],
2152 helper: &mut GraphBuildHelper,
2153 namespace_stack: &[String],
2154) {
2155 let abi = extract_ffi_abi(node, content);
2157
2158 if let Some(body_node) = node.child_by_field_name("body") {
2160 build_ffi_from_body(body_node, content, &abi, helper, namespace_stack);
2161 }
2162}
2163
2164fn build_ffi_from_body(
2166 body_node: Node<'_>,
2167 content: &[u8],
2168 abi: &str,
2169 helper: &mut GraphBuildHelper,
2170 namespace_stack: &[String],
2171) {
2172 match body_node.kind() {
2173 "declaration_list" => {
2174 let mut cursor = body_node.walk();
2176 for decl in body_node.children(&mut cursor) {
2177 if decl.kind() == "declaration"
2178 && let Some(fn_name) = extract_ffi_function_name(decl, content)
2179 {
2180 let span = span_from_node(decl);
2181 let qualified = if namespace_stack.is_empty() {
2183 format!("extern::{abi}::{fn_name}")
2184 } else {
2185 format!("{}::extern::{abi}::{fn_name}", namespace_stack.join("::"))
2186 };
2187 helper.add_function(
2189 &qualified,
2190 Some(span),
2191 false, true, );
2194 }
2195 }
2196 }
2197 "declaration" => {
2198 if let Some(fn_name) = extract_ffi_function_name(body_node, content) {
2200 let span = span_from_node(body_node);
2201 let qualified = if namespace_stack.is_empty() {
2202 format!("extern::{abi}::{fn_name}")
2203 } else {
2204 format!("{}::extern::{abi}::{fn_name}", namespace_stack.join("::"))
2205 };
2206 helper.add_function(&qualified, Some(span), false, true);
2207 }
2208 }
2209 _ => {}
2210 }
2211}
2212
2213fn collect_pure_virtual_interfaces(
2223 node: Node<'_>,
2224 content: &[u8],
2225 registry: &mut PureVirtualRegistry,
2226 budget: &mut BuildBudget,
2227) -> GraphResult<()> {
2228 budget.checkpoint("cpp:collect_pure_virtual_interfaces")?;
2229 if matches!(node.kind(), "class_specifier" | "struct_specifier")
2230 && let Some(name_node) = node.child_by_field_name("name")
2231 && let Ok(class_name) = name_node.utf8_text(content)
2232 {
2233 let class_name = class_name.trim();
2234 if !class_name.is_empty() && has_pure_virtual_methods(node, content) {
2235 registry.insert(class_name.to_string());
2236 }
2237 }
2238
2239 let mut cursor = node.walk();
2241 for child in node.children(&mut cursor) {
2242 collect_pure_virtual_interfaces(child, content, registry, budget)?;
2243 }
2244
2245 Ok(())
2246}
2247
2248fn has_pure_virtual_methods(class_node: Node<'_>, content: &[u8]) -> bool {
2252 if let Some(body) = class_node.child_by_field_name("body") {
2253 let mut cursor = body.walk();
2254 for child in body.children(&mut cursor) {
2255 if child.kind() == "field_declaration" && is_pure_virtual_declaration(child, content) {
2257 return true;
2258 }
2259 }
2260 }
2261 false
2262}
2263
2264fn is_pure_virtual_declaration(decl_node: Node<'_>, content: &[u8]) -> bool {
2266 let mut has_virtual = false;
2267 let mut has_pure_specifier = false;
2268
2269 let mut cursor = decl_node.walk();
2271 for child in decl_node.children(&mut cursor) {
2272 match child.kind() {
2273 "virtual" => {
2274 has_virtual = true;
2275 }
2276 "number_literal" => {
2277 if let Ok(text) = child.utf8_text(content)
2280 && text.trim() == "0"
2281 {
2282 has_pure_specifier = true;
2283 }
2284 }
2285 _ => {}
2286 }
2287 }
2288
2289 has_virtual && has_pure_specifier
2290}
2291
2292fn build_inheritance_and_implements_edges(
2298 class_node: Node<'_>,
2299 content: &[u8],
2300 _qualified_class_name: &str,
2301 child_id: sqry_core::graph::unified::node::NodeId,
2302 helper: &mut GraphBuildHelper,
2303 namespace_stack: &[String],
2304 pure_virtual_registry: &PureVirtualRegistry,
2305) -> GraphResult<()> {
2306 let mut cursor = class_node.walk();
2308 let base_clause = class_node
2309 .children(&mut cursor)
2310 .find(|child| child.kind() == "base_class_clause");
2311
2312 let Some(base_clause) = base_clause else {
2313 return Ok(()); };
2315
2316 let mut clause_cursor = base_clause.walk();
2318 for child in base_clause.children(&mut clause_cursor) {
2319 match child.kind() {
2320 "type_identifier" => {
2321 let base_name = child
2322 .utf8_text(content)
2323 .map_err(|_| GraphBuilderError::ParseError {
2324 span: span_from_node(child),
2325 reason: "failed to read base class name".to_string(),
2326 })?
2327 .trim();
2328
2329 if !base_name.is_empty() {
2330 let qualified_base = if namespace_stack.is_empty() {
2332 base_name.to_string()
2333 } else {
2334 format!("{}::{}", namespace_stack.join("::"), base_name)
2335 };
2336
2337 if pure_virtual_registry.contains(base_name) {
2339 let interface_id = helper.add_interface(&qualified_base, None);
2341 helper.add_implements_edge(child_id, interface_id);
2342 } else {
2343 let parent_id = helper.add_class(&qualified_base, None);
2345 helper.add_inherits_edge(child_id, parent_id);
2346 }
2347 }
2348 }
2349 "qualified_identifier" => {
2350 let base_name = child
2352 .utf8_text(content)
2353 .map_err(|_| GraphBuilderError::ParseError {
2354 span: span_from_node(child),
2355 reason: "failed to read base class name".to_string(),
2356 })?
2357 .trim();
2358
2359 if !base_name.is_empty() {
2360 let simple_name = base_name.rsplit("::").next().unwrap_or(base_name);
2362
2363 if pure_virtual_registry.contains(simple_name) {
2364 let interface_id = helper.add_interface(base_name, None);
2365 helper.add_implements_edge(child_id, interface_id);
2366 } else {
2367 let parent_id = helper.add_class(base_name, None);
2368 helper.add_inherits_edge(child_id, parent_id);
2369 }
2370 }
2371 }
2372 "template_type" => {
2373 if let Some(template_name_node) = child.child_by_field_name("name")
2375 && let Ok(base_name) = template_name_node.utf8_text(content)
2376 {
2377 let base_name = base_name.trim();
2378 if !base_name.is_empty() {
2379 let qualified_base =
2380 if base_name.contains("::") || namespace_stack.is_empty() {
2381 base_name.to_string()
2382 } else {
2383 format!("{}::{}", namespace_stack.join("::"), base_name)
2384 };
2385
2386 if pure_virtual_registry.contains(base_name) {
2389 let interface_id = helper.add_interface(&qualified_base, None);
2390 helper.add_implements_edge(child_id, interface_id);
2391 } else {
2392 let parent_id = helper.add_class(&qualified_base, None);
2393 helper.add_inherits_edge(child_id, parent_id);
2394 }
2395 }
2396 }
2397 }
2398 _ => {
2399 }
2401 }
2402 }
2403
2404 Ok(())
2405}
2406
2407fn span_from_node(node: Node<'_>) -> Span {
2408 let start = node.start_position();
2409 let end = node.end_position();
2410 Span::new(
2411 sqry_core::graph::node::Position::new(start.row, start.column),
2412 sqry_core::graph::node::Position::new(end.row, end.column),
2413 )
2414}
2415
2416fn count_arguments(node: Node<'_>) -> usize {
2417 node.child_by_field_name("arguments").map_or(0, |args| {
2418 let mut count = 0;
2419 let mut cursor = args.walk();
2420 for child in args.children(&mut cursor) {
2421 if !matches!(child.kind(), "(" | ")" | ",") {
2422 count += 1;
2423 }
2424 }
2425 count
2426 })
2427}
2428
2429#[cfg(test)]
2430mod tests {
2431 use super::*;
2432 use sqry_core::graph::unified::build::test_helpers::{
2433 assert_has_node, assert_has_node_with_kind, collect_call_edges,
2434 };
2435 use sqry_core::graph::unified::node::NodeKind;
2436 use tree_sitter::Parser;
2437
2438 fn parse_cpp(source: &str) -> Tree {
2439 let mut parser = Parser::new();
2440 parser
2441 .set_language(&tree_sitter_cpp::LANGUAGE.into())
2442 .expect("Failed to set Cpp language");
2443 parser
2444 .parse(source.as_bytes(), None)
2445 .expect("Failed to parse Cpp source")
2446 }
2447
2448 fn test_budget() -> BuildBudget {
2449 BuildBudget::new(Path::new("test.cpp"))
2450 }
2451
2452 fn extract_namespace_map_for_test(
2453 tree: &Tree,
2454 source: &str,
2455 ) -> HashMap<std::ops::Range<usize>, String> {
2456 let mut budget = test_budget();
2457 extract_namespace_map(tree.root_node(), source.as_bytes(), &mut budget)
2458 .expect("namespace extraction should succeed in tests")
2459 }
2460
2461 fn extract_cpp_contexts_for_test(
2462 tree: &Tree,
2463 source: &str,
2464 namespace_map: &HashMap<std::ops::Range<usize>, String>,
2465 ) -> Vec<FunctionContext> {
2466 let mut budget = test_budget();
2467 extract_cpp_contexts(
2468 tree.root_node(),
2469 source.as_bytes(),
2470 namespace_map,
2471 &mut budget,
2472 )
2473 .expect("context extraction should succeed in tests")
2474 }
2475
2476 fn extract_field_and_type_info_for_test(
2477 tree: &Tree,
2478 source: &str,
2479 namespace_map: &HashMap<std::ops::Range<usize>, String>,
2480 ) -> (QualifiedNameMap, QualifiedNameMap) {
2481 let mut budget = test_budget();
2482 extract_field_and_type_info(
2483 tree.root_node(),
2484 source.as_bytes(),
2485 namespace_map,
2486 &mut budget,
2487 )
2488 .expect("field/type extraction should succeed in tests")
2489 }
2490
2491 #[test]
2492 fn test_build_graph_times_out_with_expired_budget() {
2493 let source = r"
2494 namespace demo {
2495 class Service {
2496 public:
2497 void process() {}
2498 };
2499 }
2500 ";
2501 let tree = parse_cpp(source);
2502 let builder = CppGraphBuilder::new();
2503 let mut staging = StagingGraph::new();
2504 let mut budget = BuildBudget::already_expired(Path::new("timeout.cpp"));
2505
2506 let err = builder
2507 .build_graph_with_budget(
2508 &tree,
2509 source.as_bytes(),
2510 Path::new("timeout.cpp"),
2511 &mut staging,
2512 &mut budget,
2513 )
2514 .expect_err("expired budget should force timeout");
2515
2516 match err {
2517 GraphBuilderError::BuildTimedOut {
2518 file,
2519 phase,
2520 timeout_ms,
2521 } => {
2522 assert_eq!(file, PathBuf::from("timeout.cpp"));
2523 assert_eq!(phase, "cpp:extract_namespace_map");
2524 assert_eq!(timeout_ms, 1_000);
2525 }
2526 other => panic!("expected BuildTimedOut, got {other:?}"),
2527 }
2528 }
2529
2530 #[test]
2531 fn test_extract_class() {
2532 let source = "class User { }";
2533 let tree = parse_cpp(source);
2534 let mut staging = StagingGraph::new();
2535 let builder = CppGraphBuilder::new();
2536
2537 let result = builder.build_graph(
2538 &tree,
2539 source.as_bytes(),
2540 Path::new("test.cpp"),
2541 &mut staging,
2542 );
2543
2544 assert!(result.is_ok());
2545 assert_has_node_with_kind(&staging, "User", NodeKind::Class);
2546 }
2547
2548 #[test]
2549 fn test_extract_template_class() {
2550 let source = r"
2551 template <typename T>
2552 class Person {
2553 public:
2554 T name;
2555 T age;
2556 };
2557 ";
2558 let tree = parse_cpp(source);
2559 let mut staging = StagingGraph::new();
2560 let builder = CppGraphBuilder::new();
2561
2562 let result = builder.build_graph(
2563 &tree,
2564 source.as_bytes(),
2565 Path::new("test.cpp"),
2566 &mut staging,
2567 );
2568
2569 assert!(result.is_ok());
2570 assert_has_node_with_kind(&staging, "Person", NodeKind::Class);
2571 }
2572
2573 #[test]
2574 fn test_extract_function() {
2575 let source = r#"
2576 #include <cstdio>
2577 void hello() {
2578 std::printf("Hello");
2579 }
2580 "#;
2581 let tree = parse_cpp(source);
2582 let mut staging = StagingGraph::new();
2583 let builder = CppGraphBuilder::new();
2584
2585 let result = builder.build_graph(
2586 &tree,
2587 source.as_bytes(),
2588 Path::new("test.cpp"),
2589 &mut staging,
2590 );
2591
2592 assert!(result.is_ok());
2593 assert_has_node_with_kind(&staging, "hello", NodeKind::Function);
2594 }
2595
2596 #[test]
2597 fn test_extract_virtual_function() {
2598 let source = r"
2599 class Service {
2600 public:
2601 virtual void fetchData() {}
2602 };
2603 ";
2604 let tree = parse_cpp(source);
2605 let mut staging = StagingGraph::new();
2606 let builder = CppGraphBuilder::new();
2607
2608 let result = builder.build_graph(
2609 &tree,
2610 source.as_bytes(),
2611 Path::new("test.cpp"),
2612 &mut staging,
2613 );
2614
2615 assert!(result.is_ok());
2616 assert_has_node(&staging, "fetchData");
2617 }
2618
2619 #[test]
2620 fn test_extract_call_edge() {
2621 let source = r"
2622 void greet() {}
2623
2624 int main() {
2625 greet();
2626 return 0;
2627 }
2628 ";
2629 let tree = parse_cpp(source);
2630 let mut staging = StagingGraph::new();
2631 let builder = CppGraphBuilder::new();
2632
2633 let result = builder.build_graph(
2634 &tree,
2635 source.as_bytes(),
2636 Path::new("test.cpp"),
2637 &mut staging,
2638 );
2639
2640 assert!(result.is_ok());
2641 assert_has_node(&staging, "main");
2642 assert_has_node(&staging, "greet");
2643 let calls = collect_call_edges(&staging);
2644 assert!(!calls.is_empty());
2645 }
2646
2647 #[test]
2648 fn test_extract_member_call_edge() {
2649 let source = r"
2650 class Service {
2651 public:
2652 void helper() {}
2653 };
2654
2655 int main() {
2656 Service svc;
2657 svc.helper();
2658 return 0;
2659 }
2660 ";
2661 let tree = parse_cpp(source);
2662 let mut staging = StagingGraph::new();
2663 let builder = CppGraphBuilder::new();
2664
2665 let result = builder.build_graph(
2666 &tree,
2667 source.as_bytes(),
2668 Path::new("member.cpp"),
2669 &mut staging,
2670 );
2671
2672 assert!(result.is_ok());
2673 assert_has_node(&staging, "main");
2674 assert_has_node(&staging, "helper");
2675 let calls = collect_call_edges(&staging);
2676 assert!(!calls.is_empty());
2677 }
2678
2679 #[test]
2680 fn test_extract_namespace_map_simple() {
2681 let source = r"
2682 namespace demo {
2683 void func() {}
2684 }
2685 ";
2686 let tree = parse_cpp(source);
2687 let namespace_map = extract_namespace_map_for_test(&tree, source);
2688
2689 assert_eq!(namespace_map.len(), 1);
2691
2692 let (_, ns_prefix) = namespace_map.iter().next().unwrap();
2694 assert_eq!(ns_prefix, "demo::");
2695 }
2696
2697 #[test]
2698 fn test_extract_namespace_map_nested() {
2699 let source = r"
2700 namespace outer {
2701 namespace inner {
2702 void func() {}
2703 }
2704 }
2705 ";
2706 let tree = parse_cpp(source);
2707 let namespace_map = extract_namespace_map_for_test(&tree, source);
2708
2709 assert!(namespace_map.len() >= 2);
2711
2712 let ns_values: Vec<&String> = namespace_map.values().collect();
2714 assert!(ns_values.iter().any(|v| v.as_str() == "outer::"));
2715 assert!(ns_values.iter().any(|v| v.as_str() == "outer::inner::"));
2716 }
2717
2718 #[test]
2719 fn test_extract_namespace_map_multiple() {
2720 let source = r"
2721 namespace first {
2722 void func1() {}
2723 }
2724 namespace second {
2725 void func2() {}
2726 }
2727 ";
2728 let tree = parse_cpp(source);
2729 let namespace_map = extract_namespace_map_for_test(&tree, source);
2730
2731 assert_eq!(namespace_map.len(), 2);
2733
2734 let ns_values: Vec<&String> = namespace_map.values().collect();
2735 assert!(ns_values.iter().any(|v| v.as_str() == "first::"));
2736 assert!(ns_values.iter().any(|v| v.as_str() == "second::"));
2737 }
2738
2739 #[test]
2740 fn test_find_namespace_for_offset() {
2741 let source = r"
2742 namespace demo {
2743 void func() {}
2744 }
2745 ";
2746 let tree = parse_cpp(source);
2747 let namespace_map = extract_namespace_map_for_test(&tree, source);
2748
2749 let func_offset = source.find("func").unwrap();
2751 let ns = find_namespace_for_offset(func_offset, &namespace_map);
2752 assert_eq!(ns, "demo::");
2753
2754 let ns = find_namespace_for_offset(0, &namespace_map);
2756 assert_eq!(ns, "");
2757 }
2758
2759 #[test]
2760 fn test_extract_cpp_contexts_free_function() {
2761 let source = r"
2762 void helper() {}
2763 ";
2764 let tree = parse_cpp(source);
2765 let namespace_map = extract_namespace_map_for_test(&tree, source);
2766 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
2767
2768 assert_eq!(contexts.len(), 1);
2769 assert_eq!(contexts[0].qualified_name, "helper");
2770 assert!(!contexts[0].is_static);
2771 assert!(!contexts[0].is_virtual);
2772 }
2773
2774 #[test]
2775 fn test_extract_cpp_contexts_namespace_function() {
2776 let source = r"
2777 namespace demo {
2778 void helper() {}
2779 }
2780 ";
2781 let tree = parse_cpp(source);
2782 let namespace_map = extract_namespace_map_for_test(&tree, source);
2783 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
2784
2785 assert_eq!(contexts.len(), 1);
2786 assert_eq!(contexts[0].qualified_name, "demo::helper");
2787 assert_eq!(contexts[0].namespace_stack, vec!["demo"]);
2788 }
2789
2790 #[test]
2791 fn test_extract_cpp_contexts_class_method() {
2792 let source = r"
2793 class Service {
2794 public:
2795 void process() {}
2796 };
2797 ";
2798 let tree = parse_cpp(source);
2799 let namespace_map = extract_namespace_map_for_test(&tree, source);
2800 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
2801
2802 assert_eq!(contexts.len(), 1);
2803 assert_eq!(contexts[0].qualified_name, "Service::process");
2804 assert_eq!(contexts[0].class_stack, vec!["Service"]);
2805 }
2806
2807 #[test]
2808 fn test_extract_cpp_contexts_namespace_and_class() {
2809 let source = r"
2810 namespace demo {
2811 class Service {
2812 public:
2813 void process() {}
2814 };
2815 }
2816 ";
2817 let tree = parse_cpp(source);
2818 let namespace_map = extract_namespace_map_for_test(&tree, source);
2819 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
2820
2821 assert_eq!(contexts.len(), 1);
2822 assert_eq!(contexts[0].qualified_name, "demo::Service::process");
2823 assert_eq!(contexts[0].namespace_stack, vec!["demo"]);
2824 assert_eq!(contexts[0].class_stack, vec!["Service"]);
2825 }
2826
2827 #[test]
2828 fn test_extract_cpp_contexts_static_method() {
2829 let source = r"
2830 class Repository {
2831 public:
2832 static void save() {}
2833 };
2834 ";
2835 let tree = parse_cpp(source);
2836 let namespace_map = extract_namespace_map_for_test(&tree, source);
2837 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
2838
2839 assert_eq!(contexts.len(), 1);
2840 assert_eq!(contexts[0].qualified_name, "Repository::save");
2841 assert!(contexts[0].is_static);
2842 }
2843
2844 #[test]
2845 fn test_extract_cpp_contexts_virtual_method() {
2846 let source = r"
2847 class Base {
2848 public:
2849 virtual void render() {}
2850 };
2851 ";
2852 let tree = parse_cpp(source);
2853 let namespace_map = extract_namespace_map_for_test(&tree, source);
2854 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
2855
2856 assert_eq!(contexts.len(), 1);
2857 assert_eq!(contexts[0].qualified_name, "Base::render");
2858 assert!(contexts[0].is_virtual);
2859 }
2860
2861 #[test]
2862 fn test_extract_cpp_contexts_inline_function() {
2863 let source = r"
2864 inline void helper() {}
2865 ";
2866 let tree = parse_cpp(source);
2867 let namespace_map = extract_namespace_map_for_test(&tree, source);
2868 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
2869
2870 assert_eq!(contexts.len(), 1);
2871 assert_eq!(contexts[0].qualified_name, "helper");
2872 assert!(contexts[0].is_inline);
2873 }
2874
2875 #[test]
2876 fn test_extract_cpp_contexts_out_of_line_definition() {
2877 let source = r"
2878 namespace demo {
2879 class Service {
2880 public:
2881 int process(int v);
2882 };
2883
2884 inline int Service::process(int v) {
2885 return v;
2886 }
2887 }
2888 ";
2889 let tree = parse_cpp(source);
2890 let namespace_map = extract_namespace_map_for_test(&tree, source);
2891 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
2892
2893 assert_eq!(contexts.len(), 1);
2895 assert_eq!(contexts[0].qualified_name, "demo::Service::process");
2896 assert!(contexts[0].is_inline);
2897 }
2898
2899 #[test]
2900 fn test_extract_field_types_simple() {
2901 let source = r"
2902 class Service {
2903 public:
2904 Repository repo;
2905 };
2906 ";
2907 let tree = parse_cpp(source);
2908 let namespace_map = extract_namespace_map_for_test(&tree, source);
2909 let (field_types, _type_map) =
2910 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
2911
2912 assert_eq!(field_types.len(), 1);
2914 assert_eq!(
2915 field_types.get(&("Service".to_string(), "repo".to_string())),
2916 Some(&"Repository".to_string())
2917 );
2918 }
2919
2920 #[test]
2921 fn test_extract_field_types_namespace() {
2922 let source = r"
2923 namespace demo {
2924 class Service {
2925 public:
2926 Repository repo;
2927 };
2928 }
2929 ";
2930 let tree = parse_cpp(source);
2931 let namespace_map = extract_namespace_map_for_test(&tree, source);
2932 let (field_types, _type_map) =
2933 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
2934
2935 assert_eq!(field_types.len(), 1);
2937 assert_eq!(
2938 field_types.get(&("demo::Service".to_string(), "repo".to_string())),
2939 Some(&"Repository".to_string())
2940 );
2941 }
2942
2943 #[test]
2944 fn test_extract_field_types_no_collision() {
2945 let source = r"
2946 class ServiceA {
2947 public:
2948 Repository repo;
2949 };
2950
2951 class ServiceB {
2952 public:
2953 Repository repo;
2954 };
2955 ";
2956 let tree = parse_cpp(source);
2957 let namespace_map = extract_namespace_map_for_test(&tree, source);
2958 let (field_types, _type_map) =
2959 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
2960
2961 assert_eq!(field_types.len(), 2);
2963 assert_eq!(
2964 field_types.get(&("ServiceA".to_string(), "repo".to_string())),
2965 Some(&"Repository".to_string())
2966 );
2967 assert_eq!(
2968 field_types.get(&("ServiceB".to_string(), "repo".to_string())),
2969 Some(&"Repository".to_string())
2970 );
2971 }
2972
2973 #[test]
2974 fn test_extract_using_declaration() {
2975 let source = r"
2976 using std::vector;
2977
2978 class Service {
2979 public:
2980 vector data;
2981 };
2982 ";
2983 let tree = parse_cpp(source);
2984 let namespace_map = extract_namespace_map_for_test(&tree, source);
2985 let (field_types, type_map) =
2986 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
2987
2988 assert_eq!(field_types.len(), 1);
2990 assert_eq!(
2991 field_types.get(&("Service".to_string(), "data".to_string())),
2992 Some(&"std::vector".to_string()),
2993 "Field type should resolve 'vector' to 'std::vector' via using declaration"
2994 );
2995
2996 assert_eq!(
2998 type_map.get(&(String::new(), "vector".to_string())),
2999 Some(&"std::vector".to_string()),
3000 "Using declaration should map 'vector' to 'std::vector' in type_map"
3001 );
3002 }
3003
3004 #[test]
3005 fn test_extract_field_types_pointer() {
3006 let source = r"
3007 class Service {
3008 public:
3009 Repository* repo;
3010 };
3011 ";
3012 let tree = parse_cpp(source);
3013 let namespace_map = extract_namespace_map_for_test(&tree, source);
3014 let (field_types, _type_map) =
3015 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3016
3017 assert_eq!(field_types.len(), 1);
3019 assert_eq!(
3020 field_types.get(&("Service".to_string(), "repo".to_string())),
3021 Some(&"Repository".to_string())
3022 );
3023 }
3024
3025 #[test]
3026 fn test_extract_field_types_multiple_declarators() {
3027 let source = r"
3028 class Service {
3029 public:
3030 Repository repo_a, repo_b, repo_c;
3031 };
3032 ";
3033 let tree = parse_cpp(source);
3034 let namespace_map = extract_namespace_map_for_test(&tree, source);
3035 let (field_types, _type_map) =
3036 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3037
3038 assert_eq!(field_types.len(), 3);
3040 assert_eq!(
3041 field_types.get(&("Service".to_string(), "repo_a".to_string())),
3042 Some(&"Repository".to_string())
3043 );
3044 assert_eq!(
3045 field_types.get(&("Service".to_string(), "repo_b".to_string())),
3046 Some(&"Repository".to_string())
3047 );
3048 assert_eq!(
3049 field_types.get(&("Service".to_string(), "repo_c".to_string())),
3050 Some(&"Repository".to_string())
3051 );
3052 }
3053
3054 #[test]
3055 fn test_extract_field_types_nested_struct_with_parent_field() {
3056 let source = r"
3059 namespace demo {
3060 struct Outer {
3061 int outer_field;
3062 struct Inner {
3063 int inner_field;
3064 };
3065 Inner nested_instance;
3066 };
3067 }
3068 ";
3069 let tree = parse_cpp(source);
3070 let namespace_map = extract_namespace_map_for_test(&tree, source);
3071 let (field_types, _type_map) =
3072 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3073
3074 assert!(
3077 field_types.len() >= 2,
3078 "Expected at least outer_field and nested_instance"
3079 );
3080
3081 assert_eq!(
3083 field_types.get(&("demo::Outer".to_string(), "outer_field".to_string())),
3084 Some(&"int".to_string())
3085 );
3086
3087 assert_eq!(
3089 field_types.get(&("demo::Outer".to_string(), "nested_instance".to_string())),
3090 Some(&"Inner".to_string())
3091 );
3092
3093 if field_types.contains_key(&("demo::Outer::Inner".to_string(), "inner_field".to_string()))
3095 {
3096 assert_eq!(
3098 field_types.get(&("demo::Outer::Inner".to_string(), "inner_field".to_string())),
3099 Some(&"int".to_string()),
3100 "Inner class fields must use parent-qualified FQN 'demo::Outer::Inner'"
3101 );
3102 }
3103 }
3104}