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, clippy::too_many_lines)]
1271fn process_field_declaration(
1272 node: Node,
1273 content: &[u8],
1274 class_qualified_name: &str,
1275 visibility: &str,
1276 helper: &mut GraphBuildHelper,
1277) -> GraphResult<()> {
1278 let mut field_type_text = None;
1280 let mut field_names = Vec::new();
1281 let mut is_static_kw = false;
1284 let mut is_const = false;
1285 let mut is_constexpr = false;
1286
1287 let mut cursor = node.walk();
1288 for child in node.children(&mut cursor) {
1289 match child.kind() {
1290 "type_identifier" | "primitive_type" => {
1291 if let Ok(text) = child.utf8_text(content) {
1292 field_type_text = Some(text.to_string());
1293 }
1294 }
1295 "qualified_identifier" => {
1296 if let Ok(text) = child.utf8_text(content) {
1298 field_type_text = Some(text.to_string());
1299 }
1300 }
1301 "template_type" => {
1302 if let Ok(text) = child.utf8_text(content) {
1304 field_type_text = Some(text.to_string());
1305 }
1306 }
1307 "sized_type_specifier" => {
1308 if let Ok(text) = child.utf8_text(content) {
1310 field_type_text = Some(text.to_string());
1311 }
1312 }
1313 "type_qualifier" => {
1314 if let Ok(text) = child.utf8_text(content) {
1321 let trimmed = text.trim();
1322 if trimmed == "const" {
1323 is_const = true;
1324 } else if trimmed == "constexpr" {
1325 is_constexpr = true;
1326 }
1327 if field_type_text.is_none() {
1328 field_type_text = Some(text.to_string());
1329 }
1330 }
1331 }
1332 "storage_class_specifier" => {
1333 if let Ok(text) = child.utf8_text(content) {
1336 let trimmed = text.trim();
1337 if trimmed == "static" {
1338 is_static_kw = true;
1339 } else if trimmed == "constexpr" {
1340 is_constexpr = true;
1341 }
1342 }
1343 }
1344 "auto" => {
1345 field_type_text = Some("auto".to_string());
1347 }
1348 "decltype" => {
1349 if let Ok(text) = child.utf8_text(content) {
1351 field_type_text = Some(text.to_string());
1352 }
1353 }
1354 "struct_specifier" | "class_specifier" | "enum_specifier" | "union_specifier" => {
1355 if let Ok(text) = child.utf8_text(content) {
1357 field_type_text = Some(text.to_string());
1358 }
1359 }
1360 "field_identifier" => {
1361 if let Ok(name) = child.utf8_text(content) {
1362 field_names.push(name.trim().to_string());
1363 }
1364 }
1365 "field_declarator"
1366 | "pointer_declarator"
1367 | "reference_declarator"
1368 | "init_declarator" => {
1369 if let Some(name) = extract_field_name(child, content) {
1371 field_names.push(name);
1372 }
1373 }
1374 _ => {}
1375 }
1376 }
1377
1378 if let Some(type_text) = field_type_text {
1380 let base_type = strip_type_qualifiers(&type_text);
1381 let is_constant = is_const || is_constexpr;
1382
1383 for field_name in field_names {
1384 let field_qualified = format!("{class_qualified_name}.{field_name}");
1387 let span = span_from_node(node);
1388
1389 let field_id = if is_constant {
1393 helper.add_constant_with_name_static_and_visibility(
1394 &field_name,
1395 &field_qualified,
1396 Some(span),
1397 is_static_kw,
1398 Some(visibility),
1399 )
1400 } else {
1401 helper.add_property_with_name_static_and_visibility(
1402 &field_name,
1403 &field_qualified,
1404 Some(span),
1405 is_static_kw,
1406 Some(visibility),
1407 )
1408 };
1409
1410 let type_id = helper.add_type(&base_type, None);
1412
1413 helper.add_typeof_edge_with_context(
1415 field_id,
1416 type_id,
1417 Some(sqry_core::graph::unified::edge::kind::TypeOfContext::Field),
1418 None,
1419 Some(&field_name),
1420 );
1421
1422 helper.add_reference_edge(field_id, type_id);
1425 }
1426 }
1427
1428 Ok(())
1429}
1430
1431#[allow(clippy::unnecessary_wraps)]
1433fn process_global_variable_declaration(
1434 node: Node,
1435 content: &[u8],
1436 namespace_stack: &[String],
1437 helper: &mut GraphBuildHelper,
1438) -> GraphResult<()> {
1439 if node.kind() != "declaration" {
1441 return Ok(());
1442 }
1443
1444 let mut cursor_check = node.walk();
1447 for child in node.children(&mut cursor_check) {
1448 if child.kind() == "function_declarator" {
1449 return Ok(());
1450 }
1451 }
1452
1453 let mut type_text = None;
1455 let mut var_names = Vec::new();
1456
1457 let mut cursor = node.walk();
1458 for child in node.children(&mut cursor) {
1459 match child.kind() {
1460 "type_identifier" | "primitive_type" | "qualified_identifier" | "template_type" => {
1461 if let Ok(text) = child.utf8_text(content) {
1462 type_text = Some(text.to_string());
1463 }
1464 }
1465 "init_declarator" => {
1466 if let Some(declarator) = child.child_by_field_name("declarator")
1468 && let Some(name) = extract_declarator_name(declarator, content)
1469 {
1470 var_names.push(name);
1471 }
1472 }
1473 "pointer_declarator" | "reference_declarator" => {
1474 if let Some(name) = extract_declarator_name(child, content) {
1475 var_names.push(name);
1476 }
1477 }
1478 "identifier" => {
1479 if let Ok(name) = child.utf8_text(content) {
1481 var_names.push(name.to_string());
1482 }
1483 }
1484 _ => {}
1485 }
1486 }
1487
1488 if let Some(type_text) = type_text {
1489 let base_type = strip_type_qualifiers(&type_text);
1490
1491 for var_name in var_names {
1492 let qualified = if namespace_stack.is_empty() {
1494 var_name.clone()
1495 } else {
1496 format!("{}::{}", namespace_stack.join("::"), var_name)
1497 };
1498
1499 let span = span_from_node(node);
1500
1501 let var_id = helper.add_node_with_visibility(
1503 &qualified,
1504 Some(span),
1505 sqry_core::graph::unified::node::NodeKind::Variable,
1506 Some("public"),
1507 );
1508
1509 let type_id = helper.add_type(&base_type, None);
1511
1512 helper.add_typeof_edge(var_id, type_id);
1514 helper.add_reference_edge(var_id, type_id);
1515 }
1516 }
1517
1518 Ok(())
1519}
1520
1521fn extract_declarator_name(node: Node, content: &[u8]) -> Option<String> {
1523 match node.kind() {
1524 "identifier" => {
1525 if let Ok(name) = node.utf8_text(content) {
1526 Some(name.to_string())
1527 } else {
1528 None
1529 }
1530 }
1531 "pointer_declarator" | "reference_declarator" | "array_declarator" => {
1532 if let Some(inner) = node.child_by_field_name("declarator") {
1534 extract_declarator_name(inner, content)
1535 } else {
1536 let mut cursor = node.walk();
1538 for child in node.children(&mut cursor) {
1539 if child.kind() == "identifier"
1540 && let Ok(name) = child.utf8_text(content)
1541 {
1542 return Some(name.to_string());
1543 }
1544 }
1545 None
1546 }
1547 }
1548 "init_declarator" => {
1549 if let Some(inner) = node.child_by_field_name("declarator") {
1551 extract_declarator_name(inner, content)
1552 } else {
1553 None
1554 }
1555 }
1556 "field_declarator" => {
1557 if let Some(inner) = node.child_by_field_name("declarator") {
1559 extract_declarator_name(inner, content)
1560 } else {
1561 if let Ok(name) = node.utf8_text(content) {
1563 Some(name.to_string())
1564 } else {
1565 None
1566 }
1567 }
1568 }
1569 _ => None,
1570 }
1571}
1572
1573#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
1575fn walk_class_body(
1576 body_node: Node,
1577 content: &[u8],
1578 class_qualified_name: &str,
1579 is_struct: bool,
1580 ast_graph: &ASTGraph,
1581 helper: &mut GraphBuildHelper,
1582 seen_includes: &mut HashSet<String>,
1583 namespace_stack: &mut Vec<String>,
1584 class_stack: &mut Vec<String>,
1585 ffi_registry: &FfiRegistry,
1586 pure_virtual_registry: &PureVirtualRegistry,
1587 budget: &mut BuildBudget,
1588) -> GraphResult<()> {
1589 let mut current_visibility = if is_struct { "public" } else { "private" };
1591
1592 let mut cursor = body_node.walk();
1593 for child in body_node.children(&mut cursor) {
1594 budget.checkpoint("cpp:walk_class_body")?;
1595 match child.kind() {
1596 "access_specifier" => {
1597 if let Ok(text) = child.utf8_text(content) {
1599 let spec = text.trim().trim_end_matches(':').trim();
1600 current_visibility = spec;
1601 }
1602 }
1603 "field_declaration" => {
1604 let mut handled_nested = false;
1620 let mut inner_cursor = child.walk();
1621 for inner in child.children(&mut inner_cursor) {
1622 let kind = inner.kind();
1623 if !matches!(
1624 kind,
1625 "class_specifier"
1626 | "struct_specifier"
1627 | "union_specifier"
1628 | "enum_specifier"
1629 ) {
1630 continue;
1631 }
1632
1633 let is_struct_or_union = matches!(kind, "struct_specifier" | "union_specifier");
1634
1635 if let Some(name_node) = inner.child_by_field_name("name") {
1636 if let Ok(inner_name) = name_node.utf8_text(content) {
1648 let inner_name = inner_name.trim();
1649 let nested_qualified = format!("{class_qualified_name}::{inner_name}");
1650 let nested_span = span_from_node(inner);
1651
1652 match kind {
1656 "enum_specifier" => {
1657 helper.add_enum_with_visibility(
1661 &nested_qualified,
1662 Some(nested_span),
1663 Some(current_visibility),
1664 );
1665 }
1666 _ => {
1667 let nested_id = if is_struct_or_union {
1668 helper.add_struct_with_visibility(
1669 &nested_qualified,
1670 Some(nested_span),
1671 Some(current_visibility),
1672 )
1673 } else {
1674 helper.add_class_with_visibility(
1675 &nested_qualified,
1676 Some(nested_span),
1677 Some(current_visibility),
1678 )
1679 };
1680 build_inheritance_and_implements_edges(
1681 inner,
1682 content,
1683 &nested_qualified,
1684 nested_id,
1685 helper,
1686 namespace_stack,
1687 pure_virtual_registry,
1688 )?;
1689 }
1690 }
1691
1692 if matches!(
1697 kind,
1698 "class_specifier" | "struct_specifier" | "union_specifier"
1699 ) && let Some(body) = inner.child_by_field_name("body")
1700 {
1701 walk_class_body(
1702 body,
1703 content,
1704 &nested_qualified,
1705 is_struct_or_union,
1706 ast_graph,
1707 helper,
1708 seen_includes,
1709 namespace_stack,
1710 class_stack,
1711 ffi_registry,
1712 pure_virtual_registry,
1713 budget,
1714 )?;
1715 }
1716 handled_nested = true;
1717 }
1718 } else if let Some(body) = inner.child_by_field_name("body") {
1719 let mut anon_cursor = body.walk();
1724 for anon_child in body.children(&mut anon_cursor) {
1725 if anon_child.kind() == "field_declaration" {
1726 process_field_declaration(
1727 anon_child,
1728 content,
1729 class_qualified_name,
1730 current_visibility,
1731 helper,
1732 )?;
1733 }
1734 }
1735 handled_nested = true;
1736 }
1737 }
1738
1739 let _ = handled_nested;
1752 process_field_declaration(
1753 child,
1754 content,
1755 class_qualified_name,
1756 current_visibility,
1757 helper,
1758 )?;
1759 }
1760 "function_definition" => {
1761 if let Some(context) = ast_graph.context_for_start(child.start_byte()) {
1764 let span = span_from_node(child);
1765 helper.add_method_with_signature(
1766 &context.qualified_name,
1767 Some(span),
1768 false, context.is_static,
1770 Some(current_visibility),
1771 context.return_type.as_deref(),
1772 );
1773 }
1774 walk_tree_for_graph(
1776 child,
1777 content,
1778 ast_graph,
1779 helper,
1780 seen_includes,
1781 namespace_stack,
1782 class_stack,
1783 ffi_registry,
1784 pure_virtual_registry,
1785 budget,
1786 )?;
1787 }
1788 _ => {
1789 walk_tree_for_graph(
1791 child,
1792 content,
1793 ast_graph,
1794 helper,
1795 seen_includes,
1796 namespace_stack,
1797 class_stack,
1798 ffi_registry,
1799 pure_virtual_registry,
1800 budget,
1801 )?;
1802 }
1803 }
1804 }
1805
1806 Ok(())
1807}
1808
1809#[allow(clippy::too_many_arguments)]
1811#[allow(clippy::too_many_lines)] fn walk_tree_for_graph(
1813 node: Node,
1814 content: &[u8],
1815 ast_graph: &ASTGraph,
1816 helper: &mut GraphBuildHelper,
1817 seen_includes: &mut HashSet<String>,
1818 namespace_stack: &mut Vec<String>,
1819 class_stack: &mut Vec<String>,
1820 ffi_registry: &FfiRegistry,
1821 pure_virtual_registry: &PureVirtualRegistry,
1822 budget: &mut BuildBudget,
1823) -> GraphResult<()> {
1824 budget.checkpoint("cpp:walk_tree_for_graph")?;
1825 match node.kind() {
1826 "preproc_include" => {
1827 build_import_edge(node, content, helper, seen_includes)?;
1829 }
1830 "linkage_specification" => {
1831 build_ffi_block_for_staging(node, content, helper, namespace_stack);
1833 }
1834 "namespace_definition" => {
1835 if let Some(name_node) = node.child_by_field_name("name")
1837 && let Ok(ns_name) = name_node.utf8_text(content)
1838 {
1839 namespace_stack.push(ns_name.trim().to_string());
1840
1841 let mut cursor = node.walk();
1843 for child in node.children(&mut cursor) {
1844 walk_tree_for_graph(
1845 child,
1846 content,
1847 ast_graph,
1848 helper,
1849 seen_includes,
1850 namespace_stack,
1851 class_stack,
1852 ffi_registry,
1853 pure_virtual_registry,
1854 budget,
1855 )?;
1856 }
1857
1858 namespace_stack.pop();
1859 return Ok(());
1860 }
1861 }
1862 "class_specifier" | "struct_specifier" | "union_specifier" => {
1863 if let Some(name_node) = node.child_by_field_name("name")
1865 && let Ok(class_name) = name_node.utf8_text(content)
1866 {
1867 let class_name = class_name.trim();
1868 let span = span_from_node(node);
1869 let is_struct = matches!(node.kind(), "struct_specifier" | "union_specifier");
1872
1873 let qualified_class =
1875 build_qualified_name(namespace_stack, class_stack, class_name);
1876
1877 let visibility = "public";
1879 let class_id = if is_struct {
1880 helper.add_struct_with_visibility(
1881 &qualified_class,
1882 Some(span),
1883 Some(visibility),
1884 )
1885 } else {
1886 helper.add_class_with_visibility(&qualified_class, Some(span), Some(visibility))
1887 };
1888
1889 build_inheritance_and_implements_edges(
1892 node,
1893 content,
1894 &qualified_class,
1895 class_id,
1896 helper,
1897 namespace_stack,
1898 pure_virtual_registry,
1899 )?;
1900
1901 if class_stack.is_empty() {
1904 let module_id = helper.add_module(FILE_MODULE_NAME, None);
1905 helper.add_export_edge(module_id, class_id);
1906 }
1907
1908 class_stack.push(class_name.to_string());
1910
1911 if let Some(body) = node.child_by_field_name("body") {
1914 walk_class_body(
1915 body,
1916 content,
1917 &qualified_class,
1918 is_struct,
1919 ast_graph,
1920 helper,
1921 seen_includes,
1922 namespace_stack,
1923 class_stack,
1924 ffi_registry,
1925 pure_virtual_registry,
1926 budget,
1927 )?;
1928 }
1929
1930 class_stack.pop();
1931 return Ok(());
1932 }
1933 }
1934 "enum_specifier" => {
1935 if let Some(name_node) = node.child_by_field_name("name")
1936 && let Ok(enum_name) = name_node.utf8_text(content)
1937 {
1938 let enum_name = enum_name.trim();
1939 let span = span_from_node(node);
1940 let qualified_enum = build_qualified_name(namespace_stack, class_stack, enum_name);
1941 let enum_id = helper.add_enum(&qualified_enum, Some(span));
1942
1943 if class_stack.is_empty() {
1944 let module_id = helper.add_module(FILE_MODULE_NAME, None);
1945 helper.add_export_edge(module_id, enum_id);
1946 }
1947 }
1948 }
1949 "function_definition" => {
1950 if !class_stack.is_empty() {
1954 let mut cursor = node.walk();
1957 for child in node.children(&mut cursor) {
1958 walk_tree_for_graph(
1959 child,
1960 content,
1961 ast_graph,
1962 helper,
1963 seen_includes,
1964 namespace_stack,
1965 class_stack,
1966 ffi_registry,
1967 pure_virtual_registry,
1968 budget,
1969 )?;
1970 }
1971 return Ok(());
1972 }
1973
1974 if let Some(context) = ast_graph.context_for_start(node.start_byte()) {
1976 let span = span_from_node(node);
1977
1978 if context.class_stack.is_empty() {
1980 let visibility = if context.is_static {
1983 "private"
1984 } else {
1985 "public"
1986 };
1987 let fn_id = helper.add_function_with_signature(
1988 &context.qualified_name,
1989 Some(span),
1990 false, false, Some(visibility),
1993 context.return_type.as_deref(),
1994 );
1995
1996 if !context.is_static {
1998 let module_id = helper.add_module(FILE_MODULE_NAME, None);
1999 helper.add_export_edge(module_id, fn_id);
2000 }
2001 } else {
2002 helper.add_method_with_signature(
2007 &context.qualified_name,
2008 Some(span),
2009 false, context.is_static,
2011 Some("public"), context.return_type.as_deref(),
2013 );
2014 }
2015 }
2016 }
2017 "call_expression" => {
2018 if let Ok(Some((caller_qname, callee_qname, argument_count, span))) =
2020 build_call_for_staging(ast_graph, node, content)
2021 {
2022 let caller_function_id =
2024 helper.ensure_callee(&caller_qname, span, CalleeKindHint::Function);
2025 let argument_count = u8::try_from(argument_count).unwrap_or(u8::MAX);
2026
2027 let is_unqualified = !callee_qname.contains("::");
2030 if is_unqualified {
2031 if let Some((ffi_qualified, ffi_convention)) = ffi_registry.get(&callee_qname) {
2032 let ffi_target_id =
2034 helper.ensure_callee(ffi_qualified, span, CalleeKindHint::Function);
2035 helper.add_ffi_edge(caller_function_id, ffi_target_id, *ffi_convention);
2036 } else {
2037 let target_function_id =
2039 helper.ensure_callee(&callee_qname, span, CalleeKindHint::Function);
2040 helper.add_call_edge_full_with_span(
2041 caller_function_id,
2042 target_function_id,
2043 argument_count,
2044 false,
2045 vec![span],
2046 );
2047 }
2048 } else {
2049 let target_function_id =
2051 helper.ensure_callee(&callee_qname, span, CalleeKindHint::Function);
2052 helper.add_call_edge_full_with_span(
2053 caller_function_id,
2054 target_function_id,
2055 argument_count,
2056 false,
2057 vec![span],
2058 );
2059 }
2060 }
2061 }
2062 "declaration" => {
2063 if class_stack.is_empty() {
2066 process_global_variable_declaration(node, content, namespace_stack, helper)?;
2067 }
2068 }
2069 _ => {}
2070 }
2071
2072 let mut cursor = node.walk();
2074 for child in node.children(&mut cursor) {
2075 walk_tree_for_graph(
2076 child,
2077 content,
2078 ast_graph,
2079 helper,
2080 seen_includes,
2081 namespace_stack,
2082 class_stack,
2083 ffi_registry,
2084 pure_virtual_registry,
2085 budget,
2086 )?;
2087 }
2088
2089 Ok(())
2090}
2091
2092fn build_call_for_staging(
2094 ast_graph: &ASTGraph,
2095 call_node: Node<'_>,
2096 content: &[u8],
2097) -> GraphResult<Option<(String, String, usize, Span)>> {
2098 let call_context = ast_graph.find_enclosing(call_node.start_byte());
2100 let caller_qualified_name = if let Some(ctx) = call_context {
2101 ctx.qualified_name.clone()
2102 } else {
2103 return Ok(None);
2105 };
2106
2107 let Some(function_node) = call_node.child_by_field_name("function") else {
2108 return Ok(None);
2109 };
2110
2111 let callee_text = function_node
2112 .utf8_text(content)
2113 .map_err(|_| GraphBuilderError::ParseError {
2114 span: span_from_node(call_node),
2115 reason: "failed to read call expression".to_string(),
2116 })?
2117 .trim();
2118
2119 if callee_text.is_empty() {
2120 return Ok(None);
2121 }
2122
2123 let target_qualified_name = if let Some(ctx) = call_context {
2125 resolve_callee_name(callee_text, ctx, ast_graph)
2126 } else {
2127 callee_text.to_string()
2128 };
2129
2130 let span = span_from_node(call_node);
2131 let argument_count = count_arguments(call_node);
2132
2133 Ok(Some((
2134 caller_qualified_name,
2135 target_qualified_name,
2136 argument_count,
2137 span,
2138 )))
2139}
2140
2141fn build_import_edge(
2148 include_node: Node<'_>,
2149 content: &[u8],
2150 helper: &mut GraphBuildHelper,
2151 seen_includes: &mut HashSet<String>,
2152) -> GraphResult<()> {
2153 let path_node = include_node.child_by_field_name("path").or_else(|| {
2155 let mut cursor = include_node.walk();
2157 include_node.children(&mut cursor).find(|child| {
2158 matches!(
2159 child.kind(),
2160 "system_lib_string" | "string_literal" | "string_content"
2161 )
2162 })
2163 });
2164
2165 let Some(path_node) = path_node else {
2166 return Ok(());
2167 };
2168
2169 let include_path = path_node
2170 .utf8_text(content)
2171 .map_err(|_| GraphBuilderError::ParseError {
2172 span: span_from_node(include_node),
2173 reason: "failed to read include path".to_string(),
2174 })?
2175 .trim();
2176
2177 if include_path.is_empty() {
2178 return Ok(());
2179 }
2180
2181 let is_system_include = include_path.starts_with('<') && include_path.ends_with('>');
2183 let cleaned_path = if is_system_include {
2184 include_path.trim_start_matches('<').trim_end_matches('>')
2186 } else {
2187 include_path.trim_start_matches('"').trim_end_matches('"')
2189 };
2190
2191 if cleaned_path.is_empty() {
2192 return Ok(());
2193 }
2194
2195 if !seen_includes.insert(cleaned_path.to_string()) {
2197 return Ok(()); }
2199
2200 let file_module_id = helper.add_module("<file>", None);
2202
2203 let span = span_from_node(include_node);
2205 let import_id = helper.add_import(cleaned_path, Some(span));
2206
2207 helper.add_import_edge(file_module_id, import_id);
2210
2211 Ok(())
2212}
2213
2214fn collect_ffi_declarations(
2225 node: Node<'_>,
2226 content: &[u8],
2227 ffi_registry: &mut FfiRegistry,
2228 budget: &mut BuildBudget,
2229) -> GraphResult<()> {
2230 budget.checkpoint("cpp:collect_ffi_declarations")?;
2231 if node.kind() == "linkage_specification" {
2232 let abi = extract_ffi_abi(node, content);
2234 let convention = abi_to_convention(&abi);
2235
2236 if let Some(body_node) = node.child_by_field_name("body") {
2238 collect_ffi_from_body(body_node, content, &abi, convention, ffi_registry);
2239 }
2240 }
2241
2242 let mut cursor = node.walk();
2244 for child in node.children(&mut cursor) {
2245 collect_ffi_declarations(child, content, ffi_registry, budget)?;
2246 }
2247
2248 Ok(())
2249}
2250
2251fn collect_ffi_from_body(
2253 body_node: Node<'_>,
2254 content: &[u8],
2255 abi: &str,
2256 convention: FfiConvention,
2257 ffi_registry: &mut FfiRegistry,
2258) {
2259 match body_node.kind() {
2260 "declaration_list" => {
2261 let mut cursor = body_node.walk();
2263 for decl in body_node.children(&mut cursor) {
2264 if decl.kind() == "declaration"
2265 && let Some(fn_name) = extract_ffi_function_name(decl, content)
2266 {
2267 let qualified = format!("extern::{abi}::{fn_name}");
2268 ffi_registry.insert(fn_name, (qualified, convention));
2269 }
2270 }
2271 }
2272 "declaration" => {
2273 if let Some(fn_name) = extract_ffi_function_name(body_node, content) {
2275 let qualified = format!("extern::{abi}::{fn_name}");
2276 ffi_registry.insert(fn_name, (qualified, convention));
2277 }
2278 }
2279 _ => {}
2280 }
2281}
2282
2283fn extract_ffi_function_name(decl_node: Node<'_>, content: &[u8]) -> Option<String> {
2285 if let Some(declarator_node) = decl_node.child_by_field_name("declarator") {
2287 return extract_function_name_from_declarator(declarator_node, content);
2288 }
2289 None
2290}
2291
2292fn extract_function_name_from_declarator(node: Node<'_>, content: &[u8]) -> Option<String> {
2294 match node.kind() {
2295 "function_declarator" => {
2296 if let Some(inner) = node.child_by_field_name("declarator") {
2298 return extract_function_name_from_declarator(inner, content);
2299 }
2300 }
2301 "identifier" => {
2302 if let Ok(name) = node.utf8_text(content) {
2304 let name = name.trim();
2305 if !name.is_empty() {
2306 return Some(name.to_string());
2307 }
2308 }
2309 }
2310 "pointer_declarator" | "reference_declarator" => {
2311 if let Some(inner) = node.child_by_field_name("declarator") {
2313 return extract_function_name_from_declarator(inner, content);
2314 }
2315 }
2316 "parenthesized_declarator" => {
2317 let mut cursor = node.walk();
2319 for child in node.children(&mut cursor) {
2320 if let Some(name) = extract_function_name_from_declarator(child, content) {
2321 return Some(name);
2322 }
2323 }
2324 }
2325 _ => {}
2326 }
2327 None
2328}
2329
2330fn extract_ffi_abi(node: Node<'_>, content: &[u8]) -> String {
2334 if let Some(value_node) = node.child_by_field_name("value")
2336 && value_node.kind() == "string_literal"
2337 {
2338 let mut cursor = value_node.walk();
2340 for child in value_node.children(&mut cursor) {
2341 if child.kind() == "string_content"
2342 && let Ok(text) = child.utf8_text(content)
2343 {
2344 let trimmed = text.trim();
2345 if !trimmed.is_empty() {
2346 return trimmed.to_string();
2347 }
2348 }
2349 }
2350 }
2351 "C".to_string()
2353}
2354
2355fn abi_to_convention(abi: &str) -> FfiConvention {
2357 match abi.to_lowercase().as_str() {
2358 "system" => FfiConvention::System,
2359 "stdcall" => FfiConvention::Stdcall,
2360 "fastcall" => FfiConvention::Fastcall,
2361 "cdecl" => FfiConvention::Cdecl,
2362 _ => FfiConvention::C, }
2364}
2365
2366fn build_ffi_block_for_staging(
2370 node: Node<'_>,
2371 content: &[u8],
2372 helper: &mut GraphBuildHelper,
2373 namespace_stack: &[String],
2374) {
2375 let abi = extract_ffi_abi(node, content);
2377
2378 if let Some(body_node) = node.child_by_field_name("body") {
2380 build_ffi_from_body(body_node, content, &abi, helper, namespace_stack);
2381 }
2382}
2383
2384fn build_ffi_from_body(
2386 body_node: Node<'_>,
2387 content: &[u8],
2388 abi: &str,
2389 helper: &mut GraphBuildHelper,
2390 namespace_stack: &[String],
2391) {
2392 match body_node.kind() {
2393 "declaration_list" => {
2394 let mut cursor = body_node.walk();
2396 for decl in body_node.children(&mut cursor) {
2397 if decl.kind() == "declaration"
2398 && let Some(fn_name) = extract_ffi_function_name(decl, content)
2399 {
2400 let span = span_from_node(decl);
2401 let qualified = if namespace_stack.is_empty() {
2403 format!("extern::{abi}::{fn_name}")
2404 } else {
2405 format!("{}::extern::{abi}::{fn_name}", namespace_stack.join("::"))
2406 };
2407 helper.add_function(
2409 &qualified,
2410 Some(span),
2411 false, true, );
2414 }
2415 }
2416 }
2417 "declaration" => {
2418 if let Some(fn_name) = extract_ffi_function_name(body_node, content) {
2420 let span = span_from_node(body_node);
2421 let qualified = if namespace_stack.is_empty() {
2422 format!("extern::{abi}::{fn_name}")
2423 } else {
2424 format!("{}::extern::{abi}::{fn_name}", namespace_stack.join("::"))
2425 };
2426 helper.add_function(&qualified, Some(span), false, true);
2427 }
2428 }
2429 _ => {}
2430 }
2431}
2432
2433fn collect_pure_virtual_interfaces(
2443 node: Node<'_>,
2444 content: &[u8],
2445 registry: &mut PureVirtualRegistry,
2446 budget: &mut BuildBudget,
2447) -> GraphResult<()> {
2448 budget.checkpoint("cpp:collect_pure_virtual_interfaces")?;
2449 if matches!(node.kind(), "class_specifier" | "struct_specifier")
2450 && let Some(name_node) = node.child_by_field_name("name")
2451 && let Ok(class_name) = name_node.utf8_text(content)
2452 {
2453 let class_name = class_name.trim();
2454 if !class_name.is_empty() && has_pure_virtual_methods(node, content) {
2455 registry.insert(class_name.to_string());
2456 }
2457 }
2458
2459 let mut cursor = node.walk();
2461 for child in node.children(&mut cursor) {
2462 collect_pure_virtual_interfaces(child, content, registry, budget)?;
2463 }
2464
2465 Ok(())
2466}
2467
2468fn has_pure_virtual_methods(class_node: Node<'_>, content: &[u8]) -> bool {
2472 if let Some(body) = class_node.child_by_field_name("body") {
2473 let mut cursor = body.walk();
2474 for child in body.children(&mut cursor) {
2475 if child.kind() == "field_declaration" && is_pure_virtual_declaration(child, content) {
2477 return true;
2478 }
2479 }
2480 }
2481 false
2482}
2483
2484fn is_pure_virtual_declaration(decl_node: Node<'_>, content: &[u8]) -> bool {
2486 let mut has_virtual = false;
2487 let mut has_pure_specifier = false;
2488
2489 let mut cursor = decl_node.walk();
2491 for child in decl_node.children(&mut cursor) {
2492 match child.kind() {
2493 "virtual" => {
2494 has_virtual = true;
2495 }
2496 "number_literal" => {
2497 if let Ok(text) = child.utf8_text(content)
2500 && text.trim() == "0"
2501 {
2502 has_pure_specifier = true;
2503 }
2504 }
2505 _ => {}
2506 }
2507 }
2508
2509 has_virtual && has_pure_specifier
2510}
2511
2512fn build_inheritance_and_implements_edges(
2518 class_node: Node<'_>,
2519 content: &[u8],
2520 _qualified_class_name: &str,
2521 child_id: sqry_core::graph::unified::node::NodeId,
2522 helper: &mut GraphBuildHelper,
2523 namespace_stack: &[String],
2524 pure_virtual_registry: &PureVirtualRegistry,
2525) -> GraphResult<()> {
2526 let mut cursor = class_node.walk();
2528 let base_clause = class_node
2529 .children(&mut cursor)
2530 .find(|child| child.kind() == "base_class_clause");
2531
2532 let Some(base_clause) = base_clause else {
2533 return Ok(()); };
2535
2536 let mut clause_cursor = base_clause.walk();
2538 for child in base_clause.children(&mut clause_cursor) {
2539 match child.kind() {
2540 "type_identifier" => {
2541 let base_name = child
2542 .utf8_text(content)
2543 .map_err(|_| GraphBuilderError::ParseError {
2544 span: span_from_node(child),
2545 reason: "failed to read base class name".to_string(),
2546 })?
2547 .trim();
2548
2549 if !base_name.is_empty() {
2550 let qualified_base = if namespace_stack.is_empty() {
2552 base_name.to_string()
2553 } else {
2554 format!("{}::{}", namespace_stack.join("::"), base_name)
2555 };
2556
2557 if pure_virtual_registry.contains(base_name) {
2559 let interface_id = helper.add_interface(&qualified_base, None);
2561 helper.add_implements_edge(child_id, interface_id);
2562 } else {
2563 let parent_id = helper.add_class(&qualified_base, None);
2565 helper.add_inherits_edge(child_id, parent_id);
2566 }
2567 }
2568 }
2569 "qualified_identifier" => {
2570 let base_name = child
2572 .utf8_text(content)
2573 .map_err(|_| GraphBuilderError::ParseError {
2574 span: span_from_node(child),
2575 reason: "failed to read base class name".to_string(),
2576 })?
2577 .trim();
2578
2579 if !base_name.is_empty() {
2580 let simple_name = base_name.rsplit("::").next().unwrap_or(base_name);
2582
2583 if pure_virtual_registry.contains(simple_name) {
2584 let interface_id = helper.add_interface(base_name, None);
2585 helper.add_implements_edge(child_id, interface_id);
2586 } else {
2587 let parent_id = helper.add_class(base_name, None);
2588 helper.add_inherits_edge(child_id, parent_id);
2589 }
2590 }
2591 }
2592 "template_type" => {
2593 if let Some(template_name_node) = child.child_by_field_name("name")
2595 && let Ok(base_name) = template_name_node.utf8_text(content)
2596 {
2597 let base_name = base_name.trim();
2598 if !base_name.is_empty() {
2599 let qualified_base =
2600 if base_name.contains("::") || namespace_stack.is_empty() {
2601 base_name.to_string()
2602 } else {
2603 format!("{}::{}", namespace_stack.join("::"), base_name)
2604 };
2605
2606 if pure_virtual_registry.contains(base_name) {
2609 let interface_id = helper.add_interface(&qualified_base, None);
2610 helper.add_implements_edge(child_id, interface_id);
2611 } else {
2612 let parent_id = helper.add_class(&qualified_base, None);
2613 helper.add_inherits_edge(child_id, parent_id);
2614 }
2615 }
2616 }
2617 }
2618 _ => {
2619 }
2621 }
2622 }
2623
2624 Ok(())
2625}
2626
2627fn span_from_node(node: Node<'_>) -> Span {
2628 let start = node.start_position();
2629 let end = node.end_position();
2630 Span::new(
2631 sqry_core::graph::node::Position::new(start.row, start.column),
2632 sqry_core::graph::node::Position::new(end.row, end.column),
2633 )
2634}
2635
2636fn count_arguments(node: Node<'_>) -> usize {
2637 node.child_by_field_name("arguments").map_or(0, |args| {
2638 let mut count = 0;
2639 let mut cursor = args.walk();
2640 for child in args.children(&mut cursor) {
2641 if !matches!(child.kind(), "(" | ")" | ",") {
2642 count += 1;
2643 }
2644 }
2645 count
2646 })
2647}
2648
2649#[cfg(test)]
2650mod tests {
2651 use super::*;
2652 use sqry_core::graph::unified::build::test_helpers::{
2653 assert_has_node, assert_has_node_with_kind, assert_has_node_with_kind_exact,
2654 collect_call_edges,
2655 };
2656 use sqry_core::graph::unified::node::NodeKind;
2657 use tree_sitter::Parser;
2658
2659 fn parse_cpp(source: &str) -> Tree {
2660 let mut parser = Parser::new();
2661 parser
2662 .set_language(&tree_sitter_cpp::LANGUAGE.into())
2663 .expect("Failed to set Cpp language");
2664 parser
2665 .parse(source.as_bytes(), None)
2666 .expect("Failed to parse Cpp source")
2667 }
2668
2669 fn test_budget() -> BuildBudget {
2670 BuildBudget::new(Path::new("test.cpp"))
2671 }
2672
2673 fn extract_namespace_map_for_test(
2674 tree: &Tree,
2675 source: &str,
2676 ) -> HashMap<std::ops::Range<usize>, String> {
2677 let mut budget = test_budget();
2678 extract_namespace_map(tree.root_node(), source.as_bytes(), &mut budget)
2679 .expect("namespace extraction should succeed in tests")
2680 }
2681
2682 fn extract_cpp_contexts_for_test(
2683 tree: &Tree,
2684 source: &str,
2685 namespace_map: &HashMap<std::ops::Range<usize>, String>,
2686 ) -> Vec<FunctionContext> {
2687 let mut budget = test_budget();
2688 extract_cpp_contexts(
2689 tree.root_node(),
2690 source.as_bytes(),
2691 namespace_map,
2692 &mut budget,
2693 )
2694 .expect("context extraction should succeed in tests")
2695 }
2696
2697 fn extract_field_and_type_info_for_test(
2698 tree: &Tree,
2699 source: &str,
2700 namespace_map: &HashMap<std::ops::Range<usize>, String>,
2701 ) -> (QualifiedNameMap, QualifiedNameMap) {
2702 let mut budget = test_budget();
2703 extract_field_and_type_info(
2704 tree.root_node(),
2705 source.as_bytes(),
2706 namespace_map,
2707 &mut budget,
2708 )
2709 .expect("field/type extraction should succeed in tests")
2710 }
2711
2712 #[test]
2713 fn test_build_graph_times_out_with_expired_budget() {
2714 let source = r"
2715 namespace demo {
2716 class Service {
2717 public:
2718 void process() {}
2719 };
2720 }
2721 ";
2722 let tree = parse_cpp(source);
2723 let builder = CppGraphBuilder::new();
2724 let mut staging = StagingGraph::new();
2725 let mut budget = BuildBudget::already_expired(Path::new("timeout.cpp"));
2726
2727 let err = builder
2728 .build_graph_with_budget(
2729 &tree,
2730 source.as_bytes(),
2731 Path::new("timeout.cpp"),
2732 &mut staging,
2733 &mut budget,
2734 )
2735 .expect_err("expired budget should force timeout");
2736
2737 match err {
2738 GraphBuilderError::BuildTimedOut {
2739 file,
2740 phase,
2741 timeout_ms,
2742 } => {
2743 assert_eq!(file, PathBuf::from("timeout.cpp"));
2744 assert_eq!(phase, "cpp:extract_namespace_map");
2745 assert_eq!(timeout_ms, 1_000);
2746 }
2747 other => panic!("expected BuildTimedOut, got {other:?}"),
2748 }
2749 }
2750
2751 #[test]
2752 fn test_extract_class() {
2753 let source = "class User { }";
2754 let tree = parse_cpp(source);
2755 let mut staging = StagingGraph::new();
2756 let builder = CppGraphBuilder::new();
2757
2758 let result = builder.build_graph(
2759 &tree,
2760 source.as_bytes(),
2761 Path::new("test.cpp"),
2762 &mut staging,
2763 );
2764
2765 assert!(result.is_ok());
2766 assert_has_node_with_kind(&staging, "User", NodeKind::Class);
2767 }
2768
2769 #[test]
2770 fn test_extract_template_class() {
2771 let source = r"
2772 template <typename T>
2773 class Person {
2774 public:
2775 T name;
2776 T age;
2777 };
2778 ";
2779 let tree = parse_cpp(source);
2780 let mut staging = StagingGraph::new();
2781 let builder = CppGraphBuilder::new();
2782
2783 let result = builder.build_graph(
2784 &tree,
2785 source.as_bytes(),
2786 Path::new("test.cpp"),
2787 &mut staging,
2788 );
2789
2790 assert!(result.is_ok());
2791 assert_has_node_with_kind(&staging, "Person", NodeKind::Class);
2792 }
2793
2794 #[test]
2795 fn test_nested_named_types_emit_nodes() {
2796 let source = r"
2801 class Outer {
2802 public:
2803 class Inner { int z; };
2804 struct InnerS { int w; };
2805 union InnerU { int i; float f; };
2806 enum class InnerE { A, B };
2807 class L1 { public: class L2 { int q; }; };
2808 };
2809 namespace ns {
2810 class NsOuter { public: class NsInner { int n; }; };
2811 }
2812 ";
2813 let staging = build_cpp(source);
2814
2815 assert_has_node_with_kind_exact(&staging, "Outer::Inner", NodeKind::Class);
2817 assert_has_node_with_kind_exact(&staging, "Outer::InnerS", NodeKind::Struct);
2818 assert_has_node_with_kind_exact(&staging, "Outer::InnerU", NodeKind::Struct);
2820 assert_has_node_with_kind_exact(&staging, "Outer::InnerE", NodeKind::Enum);
2821 assert_has_node_with_kind_exact(&staging, "Outer::L1", NodeKind::Class);
2823 assert_has_node_with_kind_exact(&staging, "Outer::L1::L2", NodeKind::Class);
2824 assert_has_node_with_kind_exact(&staging, "ns::NsOuter", NodeKind::Class);
2826 assert_has_node_with_kind_exact(&staging, "ns::NsOuter::NsInner", NodeKind::Class);
2827
2828 assert_has_node_with_kind_exact(&staging, "Outer::Inner.z", NodeKind::Property);
2831 assert_has_node_with_kind_exact(&staging, "Outer::L1::L2.q", NodeKind::Property);
2832 assert_has_node_with_kind_exact(&staging, "ns::NsOuter::NsInner.n", NodeKind::Property);
2833 }
2834
2835 #[test]
2836 fn test_nested_enum_carries_enclosing_visibility() {
2837 let source = r"
2841 class Outer {
2842 private:
2843 enum class Secret { A, B };
2844 public:
2845 enum class Pub { X, Y };
2846 };
2847 ";
2848 let staging = build_cpp(source);
2849
2850 let secret = cpp_find_added_node(&staging, "Outer::Secret")
2851 .expect("nested enum Outer::Secret must be staged");
2852 assert_eq!(secret.kind, NodeKind::Enum, "Secret must be an Enum node");
2853 let secret_vis = staging.resolve_local_string(
2854 secret
2855 .visibility
2856 .expect("nested enum must carry a visibility id"),
2857 );
2858 assert_eq!(
2859 secret_vis,
2860 Some("private"),
2861 "nested enum under `private:` must be private"
2862 );
2863
2864 let pub_enum = cpp_find_added_node(&staging, "Outer::Pub")
2865 .expect("nested enum Outer::Pub must be staged");
2866 let pub_vis = staging.resolve_local_string(
2867 pub_enum
2868 .visibility
2869 .expect("nested enum must carry a visibility id"),
2870 );
2871 assert_eq!(
2872 pub_vis,
2873 Some("public"),
2874 "nested enum under `public:` must be public"
2875 );
2876 }
2877
2878 #[test]
2879 fn test_nested_class_emits_inheritance_edge() {
2880 let source = r"
2884 struct Base { virtual ~Base(); };
2885 class Outer {
2886 public:
2887 class Derived : public Base {};
2888 };
2889 ";
2890 let staging = build_cpp(source);
2891
2892 let derived_id = cpp_find_added_node_id(&staging, "Outer::Derived", NodeKind::Class)
2893 .expect("nested Derived class node must be staged");
2894
2895 let has_inherits = staging.operations().iter().any(|op| {
2896 matches!(
2897 op,
2898 StagingOp::AddEdge {
2899 source: src,
2900 kind: EdgeKind::Inherits,
2901 ..
2902 } if *src == derived_id
2903 )
2904 });
2905 assert!(
2906 has_inherits,
2907 "nested Derived must emit an Inherits edge to its base"
2908 );
2909 }
2910
2911 #[test]
2912 fn test_top_level_union_emits_struct_node() {
2913 let source = "union Value { int i; float f; };";
2917 let staging = build_cpp(source);
2918 assert_has_node_with_kind_exact(&staging, "Value", NodeKind::Struct);
2919 }
2920
2921 #[test]
2922 fn test_extract_function() {
2923 let source = r#"
2924 #include <cstdio>
2925 void hello() {
2926 std::printf("Hello");
2927 }
2928 "#;
2929 let tree = parse_cpp(source);
2930 let mut staging = StagingGraph::new();
2931 let builder = CppGraphBuilder::new();
2932
2933 let result = builder.build_graph(
2934 &tree,
2935 source.as_bytes(),
2936 Path::new("test.cpp"),
2937 &mut staging,
2938 );
2939
2940 assert!(result.is_ok());
2941 assert_has_node_with_kind(&staging, "hello", NodeKind::Function);
2942 }
2943
2944 #[test]
2945 fn test_extract_virtual_function() {
2946 let source = r"
2947 class Service {
2948 public:
2949 virtual void fetchData() {}
2950 };
2951 ";
2952 let tree = parse_cpp(source);
2953 let mut staging = StagingGraph::new();
2954 let builder = CppGraphBuilder::new();
2955
2956 let result = builder.build_graph(
2957 &tree,
2958 source.as_bytes(),
2959 Path::new("test.cpp"),
2960 &mut staging,
2961 );
2962
2963 assert!(result.is_ok());
2964 assert_has_node(&staging, "fetchData");
2965 }
2966
2967 #[test]
2968 fn test_extract_call_edge() {
2969 let source = r"
2970 void greet() {}
2971
2972 int main() {
2973 greet();
2974 return 0;
2975 }
2976 ";
2977 let tree = parse_cpp(source);
2978 let mut staging = StagingGraph::new();
2979 let builder = CppGraphBuilder::new();
2980
2981 let result = builder.build_graph(
2982 &tree,
2983 source.as_bytes(),
2984 Path::new("test.cpp"),
2985 &mut staging,
2986 );
2987
2988 assert!(result.is_ok());
2989 assert_has_node(&staging, "main");
2990 assert_has_node(&staging, "greet");
2991 let calls = collect_call_edges(&staging);
2992 assert!(!calls.is_empty());
2993 }
2994
2995 #[test]
2996 fn test_extract_member_call_edge() {
2997 let source = r"
2998 class Service {
2999 public:
3000 void helper() {}
3001 };
3002
3003 int main() {
3004 Service svc;
3005 svc.helper();
3006 return 0;
3007 }
3008 ";
3009 let tree = parse_cpp(source);
3010 let mut staging = StagingGraph::new();
3011 let builder = CppGraphBuilder::new();
3012
3013 let result = builder.build_graph(
3014 &tree,
3015 source.as_bytes(),
3016 Path::new("member.cpp"),
3017 &mut staging,
3018 );
3019
3020 assert!(result.is_ok());
3021 assert_has_node(&staging, "main");
3022 assert_has_node(&staging, "helper");
3023 let calls = collect_call_edges(&staging);
3024 assert!(!calls.is_empty());
3025 }
3026
3027 #[test]
3028 fn test_extract_namespace_map_simple() {
3029 let source = r"
3030 namespace demo {
3031 void func() {}
3032 }
3033 ";
3034 let tree = parse_cpp(source);
3035 let namespace_map = extract_namespace_map_for_test(&tree, source);
3036
3037 assert_eq!(namespace_map.len(), 1);
3039
3040 let (_, ns_prefix) = namespace_map.iter().next().unwrap();
3042 assert_eq!(ns_prefix, "demo::");
3043 }
3044
3045 #[test]
3046 fn test_extract_namespace_map_nested() {
3047 let source = r"
3048 namespace outer {
3049 namespace inner {
3050 void func() {}
3051 }
3052 }
3053 ";
3054 let tree = parse_cpp(source);
3055 let namespace_map = extract_namespace_map_for_test(&tree, source);
3056
3057 assert!(namespace_map.len() >= 2);
3059
3060 let ns_values: Vec<&String> = namespace_map.values().collect();
3062 assert!(ns_values.iter().any(|v| v.as_str() == "outer::"));
3063 assert!(ns_values.iter().any(|v| v.as_str() == "outer::inner::"));
3064 }
3065
3066 #[test]
3067 fn test_extract_namespace_map_multiple() {
3068 let source = r"
3069 namespace first {
3070 void func1() {}
3071 }
3072 namespace second {
3073 void func2() {}
3074 }
3075 ";
3076 let tree = parse_cpp(source);
3077 let namespace_map = extract_namespace_map_for_test(&tree, source);
3078
3079 assert_eq!(namespace_map.len(), 2);
3081
3082 let ns_values: Vec<&String> = namespace_map.values().collect();
3083 assert!(ns_values.iter().any(|v| v.as_str() == "first::"));
3084 assert!(ns_values.iter().any(|v| v.as_str() == "second::"));
3085 }
3086
3087 #[test]
3088 fn test_find_namespace_for_offset() {
3089 let source = r"
3090 namespace demo {
3091 void func() {}
3092 }
3093 ";
3094 let tree = parse_cpp(source);
3095 let namespace_map = extract_namespace_map_for_test(&tree, source);
3096
3097 let func_offset = source.find("func").unwrap();
3099 let ns = find_namespace_for_offset(func_offset, &namespace_map);
3100 assert_eq!(ns, "demo::");
3101
3102 let ns = find_namespace_for_offset(0, &namespace_map);
3104 assert_eq!(ns, "");
3105 }
3106
3107 #[test]
3108 fn test_extract_cpp_contexts_free_function() {
3109 let source = r"
3110 void helper() {}
3111 ";
3112 let tree = parse_cpp(source);
3113 let namespace_map = extract_namespace_map_for_test(&tree, source);
3114 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3115
3116 assert_eq!(contexts.len(), 1);
3117 assert_eq!(contexts[0].qualified_name, "helper");
3118 assert!(!contexts[0].is_static);
3119 assert!(!contexts[0].is_virtual);
3120 }
3121
3122 #[test]
3123 fn test_extract_cpp_contexts_namespace_function() {
3124 let source = r"
3125 namespace demo {
3126 void helper() {}
3127 }
3128 ";
3129 let tree = parse_cpp(source);
3130 let namespace_map = extract_namespace_map_for_test(&tree, source);
3131 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3132
3133 assert_eq!(contexts.len(), 1);
3134 assert_eq!(contexts[0].qualified_name, "demo::helper");
3135 assert_eq!(contexts[0].namespace_stack, vec!["demo"]);
3136 }
3137
3138 #[test]
3139 fn test_extract_cpp_contexts_class_method() {
3140 let source = r"
3141 class Service {
3142 public:
3143 void process() {}
3144 };
3145 ";
3146 let tree = parse_cpp(source);
3147 let namespace_map = extract_namespace_map_for_test(&tree, source);
3148 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3149
3150 assert_eq!(contexts.len(), 1);
3151 assert_eq!(contexts[0].qualified_name, "Service::process");
3152 assert_eq!(contexts[0].class_stack, vec!["Service"]);
3153 }
3154
3155 #[test]
3156 fn test_extract_cpp_contexts_namespace_and_class() {
3157 let source = r"
3158 namespace demo {
3159 class Service {
3160 public:
3161 void process() {}
3162 };
3163 }
3164 ";
3165 let tree = parse_cpp(source);
3166 let namespace_map = extract_namespace_map_for_test(&tree, source);
3167 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3168
3169 assert_eq!(contexts.len(), 1);
3170 assert_eq!(contexts[0].qualified_name, "demo::Service::process");
3171 assert_eq!(contexts[0].namespace_stack, vec!["demo"]);
3172 assert_eq!(contexts[0].class_stack, vec!["Service"]);
3173 }
3174
3175 #[test]
3176 fn test_extract_cpp_contexts_static_method() {
3177 let source = r"
3178 class Repository {
3179 public:
3180 static void save() {}
3181 };
3182 ";
3183 let tree = parse_cpp(source);
3184 let namespace_map = extract_namespace_map_for_test(&tree, source);
3185 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3186
3187 assert_eq!(contexts.len(), 1);
3188 assert_eq!(contexts[0].qualified_name, "Repository::save");
3189 assert!(contexts[0].is_static);
3190 }
3191
3192 #[test]
3193 fn test_extract_cpp_contexts_virtual_method() {
3194 let source = r"
3195 class Base {
3196 public:
3197 virtual void render() {}
3198 };
3199 ";
3200 let tree = parse_cpp(source);
3201 let namespace_map = extract_namespace_map_for_test(&tree, source);
3202 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3203
3204 assert_eq!(contexts.len(), 1);
3205 assert_eq!(contexts[0].qualified_name, "Base::render");
3206 assert!(contexts[0].is_virtual);
3207 }
3208
3209 #[test]
3210 fn test_extract_cpp_contexts_inline_function() {
3211 let source = r"
3212 inline void helper() {}
3213 ";
3214 let tree = parse_cpp(source);
3215 let namespace_map = extract_namespace_map_for_test(&tree, source);
3216 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3217
3218 assert_eq!(contexts.len(), 1);
3219 assert_eq!(contexts[0].qualified_name, "helper");
3220 assert!(contexts[0].is_inline);
3221 }
3222
3223 #[test]
3224 fn test_extract_cpp_contexts_out_of_line_definition() {
3225 let source = r"
3226 namespace demo {
3227 class Service {
3228 public:
3229 int process(int v);
3230 };
3231
3232 inline int Service::process(int v) {
3233 return v;
3234 }
3235 }
3236 ";
3237 let tree = parse_cpp(source);
3238 let namespace_map = extract_namespace_map_for_test(&tree, source);
3239 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3240
3241 assert_eq!(contexts.len(), 1);
3243 assert_eq!(contexts[0].qualified_name, "demo::Service::process");
3244 assert!(contexts[0].is_inline);
3245 }
3246
3247 #[test]
3248 fn test_extract_field_types_simple() {
3249 let source = r"
3250 class Service {
3251 public:
3252 Repository repo;
3253 };
3254 ";
3255 let tree = parse_cpp(source);
3256 let namespace_map = extract_namespace_map_for_test(&tree, source);
3257 let (field_types, _type_map) =
3258 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3259
3260 assert_eq!(field_types.len(), 1);
3262 assert_eq!(
3263 field_types.get(&("Service".to_string(), "repo".to_string())),
3264 Some(&"Repository".to_string())
3265 );
3266 }
3267
3268 #[test]
3269 fn test_extract_field_types_namespace() {
3270 let source = r"
3271 namespace demo {
3272 class Service {
3273 public:
3274 Repository repo;
3275 };
3276 }
3277 ";
3278 let tree = parse_cpp(source);
3279 let namespace_map = extract_namespace_map_for_test(&tree, source);
3280 let (field_types, _type_map) =
3281 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3282
3283 assert_eq!(field_types.len(), 1);
3285 assert_eq!(
3286 field_types.get(&("demo::Service".to_string(), "repo".to_string())),
3287 Some(&"Repository".to_string())
3288 );
3289 }
3290
3291 #[test]
3292 fn test_extract_field_types_no_collision() {
3293 let source = r"
3294 class ServiceA {
3295 public:
3296 Repository repo;
3297 };
3298
3299 class ServiceB {
3300 public:
3301 Repository repo;
3302 };
3303 ";
3304 let tree = parse_cpp(source);
3305 let namespace_map = extract_namespace_map_for_test(&tree, source);
3306 let (field_types, _type_map) =
3307 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3308
3309 assert_eq!(field_types.len(), 2);
3311 assert_eq!(
3312 field_types.get(&("ServiceA".to_string(), "repo".to_string())),
3313 Some(&"Repository".to_string())
3314 );
3315 assert_eq!(
3316 field_types.get(&("ServiceB".to_string(), "repo".to_string())),
3317 Some(&"Repository".to_string())
3318 );
3319 }
3320
3321 #[test]
3322 fn test_extract_using_declaration() {
3323 let source = r"
3324 using std::vector;
3325
3326 class Service {
3327 public:
3328 vector data;
3329 };
3330 ";
3331 let tree = parse_cpp(source);
3332 let namespace_map = extract_namespace_map_for_test(&tree, source);
3333 let (field_types, type_map) =
3334 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3335
3336 assert_eq!(field_types.len(), 1);
3338 assert_eq!(
3339 field_types.get(&("Service".to_string(), "data".to_string())),
3340 Some(&"std::vector".to_string()),
3341 "Field type should resolve 'vector' to 'std::vector' via using declaration"
3342 );
3343
3344 assert_eq!(
3346 type_map.get(&(String::new(), "vector".to_string())),
3347 Some(&"std::vector".to_string()),
3348 "Using declaration should map 'vector' to 'std::vector' in type_map"
3349 );
3350 }
3351
3352 #[test]
3353 fn test_extract_field_types_pointer() {
3354 let source = r"
3355 class Service {
3356 public:
3357 Repository* repo;
3358 };
3359 ";
3360 let tree = parse_cpp(source);
3361 let namespace_map = extract_namespace_map_for_test(&tree, source);
3362 let (field_types, _type_map) =
3363 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3364
3365 assert_eq!(field_types.len(), 1);
3367 assert_eq!(
3368 field_types.get(&("Service".to_string(), "repo".to_string())),
3369 Some(&"Repository".to_string())
3370 );
3371 }
3372
3373 #[test]
3374 fn test_extract_field_types_multiple_declarators() {
3375 let source = r"
3376 class Service {
3377 public:
3378 Repository repo_a, repo_b, repo_c;
3379 };
3380 ";
3381 let tree = parse_cpp(source);
3382 let namespace_map = extract_namespace_map_for_test(&tree, source);
3383 let (field_types, _type_map) =
3384 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3385
3386 assert_eq!(field_types.len(), 3);
3388 assert_eq!(
3389 field_types.get(&("Service".to_string(), "repo_a".to_string())),
3390 Some(&"Repository".to_string())
3391 );
3392 assert_eq!(
3393 field_types.get(&("Service".to_string(), "repo_b".to_string())),
3394 Some(&"Repository".to_string())
3395 );
3396 assert_eq!(
3397 field_types.get(&("Service".to_string(), "repo_c".to_string())),
3398 Some(&"Repository".to_string())
3399 );
3400 }
3401
3402 #[test]
3403 fn test_extract_field_types_nested_struct_with_parent_field() {
3404 let source = r"
3407 namespace demo {
3408 struct Outer {
3409 int outer_field;
3410 struct Inner {
3411 int inner_field;
3412 };
3413 Inner nested_instance;
3414 };
3415 }
3416 ";
3417 let tree = parse_cpp(source);
3418 let namespace_map = extract_namespace_map_for_test(&tree, source);
3419 let (field_types, _type_map) =
3420 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3421
3422 assert!(
3425 field_types.len() >= 2,
3426 "Expected at least outer_field and nested_instance"
3427 );
3428
3429 assert_eq!(
3431 field_types.get(&("demo::Outer".to_string(), "outer_field".to_string())),
3432 Some(&"int".to_string())
3433 );
3434
3435 assert_eq!(
3437 field_types.get(&("demo::Outer".to_string(), "nested_instance".to_string())),
3438 Some(&"Inner".to_string())
3439 );
3440
3441 if field_types.contains_key(&("demo::Outer::Inner".to_string(), "inner_field".to_string()))
3443 {
3444 assert_eq!(
3446 field_types.get(&("demo::Outer::Inner".to_string(), "inner_field".to_string())),
3447 Some(&"int".to_string()),
3448 "Inner class fields must use parent-qualified FQN 'demo::Outer::Inner'"
3449 );
3450 }
3451 }
3452
3453 use sqry_core::graph::unified::build::staging::StagingOp;
3470 use sqry_core::graph::unified::edge::kind::{EdgeKind, TypeOfContext};
3471
3472 fn cpp_find_added_node<'a>(
3474 staging: &'a StagingGraph,
3475 canonical_name: &str,
3476 ) -> Option<&'a sqry_core::graph::unified::storage::arena::NodeEntry> {
3477 staging.operations().iter().find_map(|op| {
3478 if let StagingOp::AddNode { entry, .. } = op
3479 && staging.resolve_node_canonical_name(entry) == Some(canonical_name)
3480 {
3481 Some(entry)
3482 } else {
3483 None
3484 }
3485 })
3486 }
3487
3488 fn cpp_find_added_node_id(
3490 staging: &StagingGraph,
3491 canonical_name: &str,
3492 kind: NodeKind,
3493 ) -> Option<sqry_core::graph::unified::NodeId> {
3494 staging.operations().iter().find_map(|op| match op {
3495 StagingOp::AddNode {
3496 entry,
3497 expected_id: Some(id),
3498 } if entry.kind == kind
3499 && staging.resolve_node_canonical_name(entry) == Some(canonical_name) =>
3500 {
3501 Some(*id)
3502 }
3503 _ => None,
3504 })
3505 }
3506
3507 fn build_cpp(source: &str) -> StagingGraph {
3509 let tree = parse_cpp(source);
3510 let mut staging = StagingGraph::new();
3511 let builder = CppGraphBuilder::new();
3512 builder
3513 .build_graph(
3514 &tree,
3515 source.as_bytes(),
3516 Path::new("test.cpp"),
3517 &mut staging,
3518 )
3519 .expect("build_graph must succeed for the test fixture");
3520 staging
3521 }
3522
3523 #[test]
3529 fn test_struct_field_emits_property_with_field_context() {
3530 let source = "struct Point { int x; int y; };";
3531 let staging = build_cpp(source);
3532
3533 assert_has_node_with_kind_exact(&staging, "Point.x", NodeKind::Property);
3535 assert_has_node_with_kind_exact(&staging, "Point.y", NodeKind::Property);
3536
3537 let entry =
3538 cpp_find_added_node(&staging, "Point.x").expect("Point.x should be staged as a node");
3539 assert_eq!(entry.kind, NodeKind::Property, "x must be Property");
3540 assert!(!entry.is_static, "instance field is_static must be false");
3541 let vis = staging.resolve_local_string(entry.visibility.expect("visibility id"));
3542 assert_eq!(
3543 vis,
3544 Some("public"),
3545 "struct default visibility must be 'public'"
3546 );
3547 assert!(entry.end_line > 0, "field end_line must be set (got 0)");
3553 assert!(
3554 entry.end_line > entry.start_line
3555 || (entry.end_line == entry.start_line && entry.end_column > entry.start_column),
3556 "field span must be non-empty: [{}:{}..{}:{}]",
3557 entry.start_line,
3558 entry.start_column,
3559 entry.end_line,
3560 entry.end_column,
3561 );
3562
3563 let x_id = cpp_find_added_node_id(&staging, "Point.x", NodeKind::Property)
3565 .expect("Point.x Property NodeId");
3566 let edge = staging.operations().iter().find_map(|op| {
3567 if let StagingOp::AddEdge {
3568 source: src,
3569 kind: EdgeKind::TypeOf { context, name, .. },
3570 ..
3571 } = op
3572 && *src == x_id
3573 {
3574 Some((*context, *name))
3575 } else {
3576 None
3577 }
3578 });
3579 let (ctx, name) = edge.expect("TypeOf edge from Point.x should be staged");
3580 assert_eq!(
3581 ctx,
3582 Some(TypeOfContext::Field),
3583 "TypeOf edge context must be Field"
3584 );
3585 let resolved_name = name.and_then(|sid| staging.resolve_local_string(sid));
3586 assert_eq!(
3587 resolved_name,
3588 Some("x"),
3589 "TypeOf edge name must be the bare field name 'x'"
3590 );
3591
3592 let stale_variable = staging.nodes().any(|n| {
3594 n.entry.kind == NodeKind::Variable
3595 && matches!(
3596 staging.resolve_node_name(n.entry),
3597 Some("Point.x" | "Point.y" | "Point::x" | "Point::y")
3598 )
3599 });
3600 assert!(
3601 !stale_variable,
3602 "Point fields must not be emitted as NodeKind::Variable"
3603 );
3604 }
3605
3606 #[test]
3608 fn test_class_field_default_visibility_is_private() {
3609 let source = "class Foo { int hidden; };";
3610 let staging = build_cpp(source);
3611
3612 let entry = cpp_find_added_node(&staging, "Foo.hidden")
3613 .expect("Foo.hidden should be staged as a node");
3614 assert_eq!(entry.kind, NodeKind::Property);
3615 let vis = staging.resolve_local_string(entry.visibility.expect("visibility id"));
3616 assert_eq!(
3617 vis,
3618 Some("private"),
3619 "class default visibility must be 'private'"
3620 );
3621 }
3622
3623 #[test]
3625 fn test_class_field_respects_explicit_access_specifier() {
3626 let source = "class Foo { public: int public_field; protected: int prot_field; };";
3627 let staging = build_cpp(source);
3628
3629 let pub_entry = cpp_find_added_node(&staging, "Foo.public_field")
3630 .expect("Foo.public_field should be staged");
3631 assert_eq!(
3632 staging.resolve_local_string(pub_entry.visibility.expect("vis")),
3633 Some("public")
3634 );
3635
3636 let prot_entry = cpp_find_added_node(&staging, "Foo.prot_field")
3637 .expect("Foo.prot_field should be staged");
3638 assert_eq!(
3639 staging.resolve_local_string(prot_entry.visibility.expect("vis")),
3640 Some("protected")
3641 );
3642 }
3643
3644 #[test]
3647 fn test_const_field_emits_constant() {
3648 let source = "class Foo { const int kMax = 0; };";
3649 let staging = build_cpp(source);
3650
3651 assert_has_node_with_kind_exact(&staging, "Foo.kMax", NodeKind::Constant);
3652 let entry = cpp_find_added_node(&staging, "Foo.kMax").expect("Foo.kMax");
3653 assert_eq!(entry.kind, NodeKind::Constant);
3654 assert!(
3655 !entry.is_static,
3656 "const (non-static) field is_static must be false; only `static` keyword sets is_static"
3657 );
3658 }
3659
3660 #[test]
3664 fn test_constexpr_field_emits_constant() {
3665 let source = "class Foo { constexpr static int kAnswer = 42; };";
3666 let staging = build_cpp(source);
3667
3668 assert_has_node_with_kind_exact(&staging, "Foo.kAnswer", NodeKind::Constant);
3669 let entry = cpp_find_added_node(&staging, "Foo.kAnswer").expect("Foo.kAnswer");
3670 assert_eq!(entry.kind, NodeKind::Constant);
3671 assert!(
3672 entry.is_static,
3673 "static constexpr member must have is_static = true"
3674 );
3675 }
3676
3677 #[test]
3680 fn test_static_field_sets_is_static_true() {
3681 let source = "class Foo { static int counter; };";
3682 let staging = build_cpp(source);
3683
3684 let entry = cpp_find_added_node(&staging, "Foo.counter").expect("Foo.counter");
3685 assert_eq!(entry.kind, NodeKind::Property);
3686 assert!(entry.is_static, "static keyword must set is_static = true");
3687 }
3688
3689 #[test]
3692 fn test_bitfield_emits_property() {
3693 let source = "struct Flags { unsigned int low : 4; unsigned int high : 4; };";
3694 let staging = build_cpp(source);
3695
3696 assert_has_node_with_kind_exact(&staging, "Flags.low", NodeKind::Property);
3697 assert_has_node_with_kind_exact(&staging, "Flags.high", NodeKind::Property);
3698 }
3699
3700 #[test]
3706 fn test_anonymous_union_member_fields_emit_property() {
3707 let source = r"
3708class Variant {
3709public:
3710 int tag;
3711 union {
3712 int as_int;
3713 float as_float;
3714 };
3715};
3716";
3717 let staging = build_cpp(source);
3718
3719 assert_has_node_with_kind_exact(&staging, "Variant.tag", NodeKind::Property);
3721
3722 assert_has_node_with_kind_exact(&staging, "Variant.as_int", NodeKind::Property);
3725 assert_has_node_with_kind_exact(&staging, "Variant.as_float", NodeKind::Property);
3726
3727 let as_int = cpp_find_added_node(&staging, "Variant.as_int")
3730 .expect("Variant.as_int should be staged");
3731 let vis = staging.resolve_local_string(as_int.visibility.expect("visibility id"));
3732 assert_eq!(
3733 vis,
3734 Some("public"),
3735 "anonymous-union members must inherit OUTER access (`public:` here)"
3736 );
3737
3738 let bogus = staging.nodes().any(|n| {
3741 staging
3742 .resolve_node_name(n.entry)
3743 .is_some_and(|name| name.contains("::.") || name.starts_with("Variant::."))
3744 });
3745 assert!(
3746 !bogus,
3747 "anonymous union must not produce a synthetic qualifier"
3748 );
3749
3750 let stale_variable = staging.nodes().any(|n| {
3752 n.entry.kind == NodeKind::Variable
3753 && matches!(
3754 staging.resolve_node_name(n.entry),
3755 Some("Variant.tag" | "Variant.as_int" | "Variant.as_float")
3756 )
3757 });
3758 assert!(
3759 !stale_variable,
3760 "anonymous-union members + outer fields must not stay as Variable"
3761 );
3762 }
3763
3764 #[test]
3768 fn test_templated_class_field_emits_property() {
3769 let source = r"
3770template<class T>
3771struct Box {
3772 T value;
3773};
3774";
3775 let staging = build_cpp(source);
3776
3777 assert_has_node_with_kind_exact(&staging, "Box.value", NodeKind::Property);
3778 let entry = cpp_find_added_node(&staging, "Box.value").expect("Box.value");
3779 assert_eq!(entry.kind, NodeKind::Property);
3780 assert!(!entry.is_static);
3781 }
3782
3783 #[test]
3789 fn test_outer_class_field_with_nested_class_present() {
3790 let source = r"
3791class Outer {
3792public:
3793 int outer_value;
3794 class Inner {
3795 public:
3796 int x;
3797 };
3798};
3799";
3800 let staging = build_cpp(source);
3801
3802 assert_has_node_with_kind_exact(&staging, "Outer.outer_value", NodeKind::Property);
3804
3805 assert_has_node_with_kind_exact(&staging, "Outer::Inner.x", NodeKind::Property);
3809
3810 let legacy_hits: Vec<_> = staging
3813 .nodes()
3814 .filter(|n| staging.resolve_node_name(n.entry) == Some("Outer::outer_value"))
3815 .collect();
3816 assert!(
3817 legacy_hits.is_empty(),
3818 "legacy `Outer::outer_value` lookup must return 0 hits"
3819 );
3820
3821 for legacy in ["Inner.x", "Outer::Inner::x", "Outer.Inner.x"] {
3825 let hits: Vec<_> = staging
3826 .nodes()
3827 .filter(|n| staging.resolve_node_name(n.entry) == Some(legacy))
3828 .collect();
3829 assert!(
3830 hits.is_empty(),
3831 "nested-class field `{legacy}` must not appear; expected only `Outer::Inner.x`"
3832 );
3833 }
3834 }
3835
3836 #[test]
3840 fn test_outer_class_with_nested_struct_emits_inner_field() {
3841 let source = r"
3842class Outer {
3843private:
3844 struct Inner {
3845 int y;
3846 };
3847};
3848";
3849 let staging = build_cpp(source);
3850
3851 assert_has_node_with_kind_exact(&staging, "Outer::Inner.y", NodeKind::Property);
3852
3853 let entry = cpp_find_added_node(&staging, "Outer::Inner.y")
3854 .expect("Outer::Inner.y should be staged");
3855 let vis = staging.resolve_local_string(entry.visibility.expect("visibility id"));
3856 assert_eq!(
3857 vis,
3858 Some("public"),
3859 "nested struct field default visibility must be 'public' \
3860 regardless of OUTER access state"
3861 );
3862 }
3863
3864 #[test]
3872 fn test_legacy_double_colon_field_lookup_returns_zero() {
3873 let source = r"
3874class Foo {
3875public:
3876 int bar;
3877 static int baz;
3878 const int qux = 0;
3879};
3880struct Quux {
3881 int corge;
3882};
3883";
3884 let staging = build_cpp(source);
3885
3886 assert_has_node_with_kind_exact(&staging, "Foo.bar", NodeKind::Property);
3888 assert_has_node_with_kind_exact(&staging, "Foo.baz", NodeKind::Property);
3889 assert_has_node_with_kind_exact(&staging, "Foo.qux", NodeKind::Constant);
3890 assert_has_node_with_kind_exact(&staging, "Quux.corge", NodeKind::Property);
3891
3892 for legacy in ["Foo::bar", "Foo::baz", "Foo::qux", "Quux::corge"] {
3895 let hits: Vec<_> = staging
3896 .nodes()
3897 .filter(|n| staging.resolve_node_name(n.entry) == Some(legacy))
3898 .collect();
3899 assert!(
3900 hits.is_empty(),
3901 "legacy lookup for {legacy:?} must return 0 hits, got {} node(s) ({:?})",
3902 hits.len(),
3903 hits.iter()
3904 .map(|n| (n.entry.kind, staging.resolve_node_name(n.entry)))
3905 .collect::<Vec<_>>()
3906 );
3907 }
3908 }
3909
3910 #[test]
3913 fn test_namespaced_class_field_qualified_name() {
3914 let source = r"
3915namespace demo {
3916 class Service {
3917 public:
3918 int counter;
3919 };
3920}
3921";
3922 let staging = build_cpp(source);
3923
3924 assert_has_node_with_kind_exact(&staging, "demo::Service.counter", NodeKind::Property);
3925 }
3926}