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