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 if kind == "enum_specifier" {
1656 helper.add_enum_with_visibility(
1660 &nested_qualified,
1661 Some(nested_span),
1662 Some(current_visibility),
1663 );
1664 } else {
1665 let nested_id = if is_struct_or_union {
1666 helper.add_struct_with_visibility(
1667 &nested_qualified,
1668 Some(nested_span),
1669 Some(current_visibility),
1670 )
1671 } else {
1672 helper.add_class_with_visibility(
1673 &nested_qualified,
1674 Some(nested_span),
1675 Some(current_visibility),
1676 )
1677 };
1678 build_inheritance_and_implements_edges(
1679 inner,
1680 content,
1681 &nested_qualified,
1682 nested_id,
1683 helper,
1684 namespace_stack,
1685 pure_virtual_registry,
1686 )?;
1687 }
1688
1689 if matches!(
1694 kind,
1695 "class_specifier" | "struct_specifier" | "union_specifier"
1696 ) && let Some(body) = inner.child_by_field_name("body")
1697 {
1698 walk_class_body(
1699 body,
1700 content,
1701 &nested_qualified,
1702 is_struct_or_union,
1703 ast_graph,
1704 helper,
1705 seen_includes,
1706 namespace_stack,
1707 class_stack,
1708 ffi_registry,
1709 pure_virtual_registry,
1710 budget,
1711 )?;
1712 }
1713 handled_nested = true;
1714 }
1715 } else if let Some(body) = inner.child_by_field_name("body") {
1716 let mut anon_cursor = body.walk();
1721 for anon_child in body.children(&mut anon_cursor) {
1722 if anon_child.kind() == "field_declaration" {
1723 process_field_declaration(
1724 anon_child,
1725 content,
1726 class_qualified_name,
1727 current_visibility,
1728 helper,
1729 )?;
1730 }
1731 }
1732 handled_nested = true;
1733 }
1734 }
1735
1736 let _ = handled_nested;
1749 process_field_declaration(
1750 child,
1751 content,
1752 class_qualified_name,
1753 current_visibility,
1754 helper,
1755 )?;
1756 }
1757 "function_definition" => {
1758 if let Some(context) = ast_graph.context_for_start(child.start_byte()) {
1761 let span = span_from_node(child);
1762 helper.add_method_with_signature(
1763 &context.qualified_name,
1764 Some(span),
1765 false, context.is_static,
1767 Some(current_visibility),
1768 context.return_type.as_deref(),
1769 );
1770 }
1771 walk_tree_for_graph(
1773 child,
1774 content,
1775 ast_graph,
1776 helper,
1777 seen_includes,
1778 namespace_stack,
1779 class_stack,
1780 ffi_registry,
1781 pure_virtual_registry,
1782 budget,
1783 )?;
1784 }
1785 _ => {
1786 walk_tree_for_graph(
1788 child,
1789 content,
1790 ast_graph,
1791 helper,
1792 seen_includes,
1793 namespace_stack,
1794 class_stack,
1795 ffi_registry,
1796 pure_virtual_registry,
1797 budget,
1798 )?;
1799 }
1800 }
1801 }
1802
1803 Ok(())
1804}
1805
1806#[allow(clippy::too_many_arguments)]
1808#[allow(clippy::too_many_lines)] fn walk_tree_for_graph(
1810 node: Node,
1811 content: &[u8],
1812 ast_graph: &ASTGraph,
1813 helper: &mut GraphBuildHelper,
1814 seen_includes: &mut HashSet<String>,
1815 namespace_stack: &mut Vec<String>,
1816 class_stack: &mut Vec<String>,
1817 ffi_registry: &FfiRegistry,
1818 pure_virtual_registry: &PureVirtualRegistry,
1819 budget: &mut BuildBudget,
1820) -> GraphResult<()> {
1821 budget.checkpoint("cpp:walk_tree_for_graph")?;
1822 match node.kind() {
1823 "preproc_include" => {
1824 build_import_edge(node, content, helper, seen_includes)?;
1826 }
1827 "linkage_specification" => {
1828 build_ffi_block_for_staging(node, content, helper, namespace_stack);
1830 }
1831 "namespace_definition" => {
1832 if let Some(name_node) = node.child_by_field_name("name")
1834 && let Ok(ns_name) = name_node.utf8_text(content)
1835 {
1836 namespace_stack.push(ns_name.trim().to_string());
1837
1838 let mut cursor = node.walk();
1840 for child in node.children(&mut cursor) {
1841 walk_tree_for_graph(
1842 child,
1843 content,
1844 ast_graph,
1845 helper,
1846 seen_includes,
1847 namespace_stack,
1848 class_stack,
1849 ffi_registry,
1850 pure_virtual_registry,
1851 budget,
1852 )?;
1853 }
1854
1855 namespace_stack.pop();
1856 return Ok(());
1857 }
1858 }
1859 "class_specifier" | "struct_specifier" | "union_specifier" => {
1860 if let Some(name_node) = node.child_by_field_name("name")
1862 && let Ok(class_name) = name_node.utf8_text(content)
1863 {
1864 let class_name = class_name.trim();
1865 let span = span_from_node(node);
1866 let is_struct = matches!(node.kind(), "struct_specifier" | "union_specifier");
1869
1870 let qualified_class =
1872 build_qualified_name(namespace_stack, class_stack, class_name);
1873
1874 let visibility = "public";
1876 let class_id = if is_struct {
1877 helper.add_struct_with_visibility(
1878 &qualified_class,
1879 Some(span),
1880 Some(visibility),
1881 )
1882 } else {
1883 helper.add_class_with_visibility(&qualified_class, Some(span), Some(visibility))
1884 };
1885
1886 build_inheritance_and_implements_edges(
1889 node,
1890 content,
1891 &qualified_class,
1892 class_id,
1893 helper,
1894 namespace_stack,
1895 pure_virtual_registry,
1896 )?;
1897
1898 if class_stack.is_empty() {
1901 let module_id = helper.add_module(FILE_MODULE_NAME, None);
1902 helper.add_export_edge(module_id, class_id);
1903 }
1904
1905 class_stack.push(class_name.to_string());
1907
1908 if let Some(body) = node.child_by_field_name("body") {
1911 walk_class_body(
1912 body,
1913 content,
1914 &qualified_class,
1915 is_struct,
1916 ast_graph,
1917 helper,
1918 seen_includes,
1919 namespace_stack,
1920 class_stack,
1921 ffi_registry,
1922 pure_virtual_registry,
1923 budget,
1924 )?;
1925 }
1926
1927 class_stack.pop();
1928 return Ok(());
1929 }
1930 }
1931 "enum_specifier" => {
1932 if let Some(name_node) = node.child_by_field_name("name")
1933 && let Ok(enum_name) = name_node.utf8_text(content)
1934 {
1935 let enum_name = enum_name.trim();
1936 let span = span_from_node(node);
1937 let qualified_enum = build_qualified_name(namespace_stack, class_stack, enum_name);
1938 let enum_id = helper.add_enum(&qualified_enum, Some(span));
1939
1940 if class_stack.is_empty() {
1941 let module_id = helper.add_module(FILE_MODULE_NAME, None);
1942 helper.add_export_edge(module_id, enum_id);
1943 }
1944 }
1945 }
1946 "function_definition" => {
1947 if !class_stack.is_empty() {
1951 let mut cursor = node.walk();
1954 for child in node.children(&mut cursor) {
1955 walk_tree_for_graph(
1956 child,
1957 content,
1958 ast_graph,
1959 helper,
1960 seen_includes,
1961 namespace_stack,
1962 class_stack,
1963 ffi_registry,
1964 pure_virtual_registry,
1965 budget,
1966 )?;
1967 }
1968 return Ok(());
1969 }
1970
1971 if let Some(context) = ast_graph.context_for_start(node.start_byte()) {
1973 let span = span_from_node(node);
1974
1975 if context.class_stack.is_empty() {
1977 let visibility = if context.is_static {
1980 "private"
1981 } else {
1982 "public"
1983 };
1984 let fn_id = helper.add_function_with_signature(
1985 &context.qualified_name,
1986 Some(span),
1987 false, false, Some(visibility),
1990 context.return_type.as_deref(),
1991 );
1992
1993 if !context.is_static {
1995 let module_id = helper.add_module(FILE_MODULE_NAME, None);
1996 helper.add_export_edge(module_id, fn_id);
1997 }
1998 } else {
1999 helper.add_method_with_signature(
2004 &context.qualified_name,
2005 Some(span),
2006 false, context.is_static,
2008 Some("public"), context.return_type.as_deref(),
2010 );
2011 }
2012 }
2013 }
2014 "call_expression" => {
2015 if let Ok(Some((caller_qname, callee_qname, argument_count, span))) =
2017 build_call_for_staging(ast_graph, node, content)
2018 {
2019 let caller_function_id =
2021 helper.ensure_callee(&caller_qname, span, CalleeKindHint::Function);
2022 let argument_count = u8::try_from(argument_count).unwrap_or(u8::MAX);
2023
2024 let is_unqualified = !callee_qname.contains("::");
2027 if is_unqualified {
2028 if let Some((ffi_qualified, ffi_convention)) = ffi_registry.get(&callee_qname) {
2029 let ffi_target_id =
2031 helper.ensure_callee(ffi_qualified, span, CalleeKindHint::Function);
2032 helper.add_ffi_edge(caller_function_id, ffi_target_id, *ffi_convention);
2033 } else {
2034 let target_function_id =
2036 helper.ensure_callee(&callee_qname, span, CalleeKindHint::Function);
2037 helper.add_call_edge_full_with_span(
2038 caller_function_id,
2039 target_function_id,
2040 argument_count,
2041 false,
2042 vec![span],
2043 );
2044 }
2045 } else {
2046 let target_function_id =
2048 helper.ensure_callee(&callee_qname, span, CalleeKindHint::Function);
2049 helper.add_call_edge_full_with_span(
2050 caller_function_id,
2051 target_function_id,
2052 argument_count,
2053 false,
2054 vec![span],
2055 );
2056 }
2057 }
2058 }
2059 "declaration" => {
2060 if class_stack.is_empty() {
2063 process_global_variable_declaration(node, content, namespace_stack, helper)?;
2064 }
2065 }
2066 _ => {}
2067 }
2068
2069 let mut cursor = node.walk();
2071 for child in node.children(&mut cursor) {
2072 walk_tree_for_graph(
2073 child,
2074 content,
2075 ast_graph,
2076 helper,
2077 seen_includes,
2078 namespace_stack,
2079 class_stack,
2080 ffi_registry,
2081 pure_virtual_registry,
2082 budget,
2083 )?;
2084 }
2085
2086 Ok(())
2087}
2088
2089fn build_call_for_staging(
2091 ast_graph: &ASTGraph,
2092 call_node: Node<'_>,
2093 content: &[u8],
2094) -> GraphResult<Option<(String, String, usize, Span)>> {
2095 let call_context = ast_graph.find_enclosing(call_node.start_byte());
2097 let caller_qualified_name = if let Some(ctx) = call_context {
2098 ctx.qualified_name.clone()
2099 } else {
2100 return Ok(None);
2102 };
2103
2104 let Some(function_node) = call_node.child_by_field_name("function") else {
2105 return Ok(None);
2106 };
2107
2108 let callee_text = function_node
2109 .utf8_text(content)
2110 .map_err(|_| GraphBuilderError::ParseError {
2111 span: span_from_node(call_node),
2112 reason: "failed to read call expression".to_string(),
2113 })?
2114 .trim();
2115
2116 if callee_text.is_empty() {
2117 return Ok(None);
2118 }
2119
2120 let target_qualified_name = if let Some(ctx) = call_context {
2122 resolve_callee_name(callee_text, ctx, ast_graph)
2123 } else {
2124 callee_text.to_string()
2125 };
2126
2127 let span = span_from_node(call_node);
2128 let argument_count = count_arguments(call_node);
2129
2130 Ok(Some((
2131 caller_qualified_name,
2132 target_qualified_name,
2133 argument_count,
2134 span,
2135 )))
2136}
2137
2138fn build_import_edge(
2145 include_node: Node<'_>,
2146 content: &[u8],
2147 helper: &mut GraphBuildHelper,
2148 seen_includes: &mut HashSet<String>,
2149) -> GraphResult<()> {
2150 let path_node = include_node.child_by_field_name("path").or_else(|| {
2152 let mut cursor = include_node.walk();
2154 include_node.children(&mut cursor).find(|child| {
2155 matches!(
2156 child.kind(),
2157 "system_lib_string" | "string_literal" | "string_content"
2158 )
2159 })
2160 });
2161
2162 let Some(path_node) = path_node else {
2163 return Ok(());
2164 };
2165
2166 let include_path = path_node
2167 .utf8_text(content)
2168 .map_err(|_| GraphBuilderError::ParseError {
2169 span: span_from_node(include_node),
2170 reason: "failed to read include path".to_string(),
2171 })?
2172 .trim();
2173
2174 if include_path.is_empty() {
2175 return Ok(());
2176 }
2177
2178 let is_system_include = include_path.starts_with('<') && include_path.ends_with('>');
2180 let cleaned_path = if is_system_include {
2181 include_path.trim_start_matches('<').trim_end_matches('>')
2183 } else {
2184 include_path.trim_start_matches('"').trim_end_matches('"')
2186 };
2187
2188 if cleaned_path.is_empty() {
2189 return Ok(());
2190 }
2191
2192 if !seen_includes.insert(cleaned_path.to_string()) {
2194 return Ok(()); }
2196
2197 let file_module_id = helper.add_module("<file>", None);
2199
2200 let span = span_from_node(include_node);
2202 let import_id = helper.add_import(cleaned_path, Some(span));
2203
2204 helper.add_import_edge(file_module_id, import_id);
2207
2208 Ok(())
2209}
2210
2211fn collect_ffi_declarations(
2222 node: Node<'_>,
2223 content: &[u8],
2224 ffi_registry: &mut FfiRegistry,
2225 budget: &mut BuildBudget,
2226) -> GraphResult<()> {
2227 budget.checkpoint("cpp:collect_ffi_declarations")?;
2228 if node.kind() == "linkage_specification" {
2229 let abi = extract_ffi_abi(node, content);
2231 let convention = abi_to_convention(&abi);
2232
2233 if let Some(body_node) = node.child_by_field_name("body") {
2235 collect_ffi_from_body(body_node, content, &abi, convention, ffi_registry);
2236 }
2237 }
2238
2239 let mut cursor = node.walk();
2241 for child in node.children(&mut cursor) {
2242 collect_ffi_declarations(child, content, ffi_registry, budget)?;
2243 }
2244
2245 Ok(())
2246}
2247
2248fn collect_ffi_from_body(
2250 body_node: Node<'_>,
2251 content: &[u8],
2252 abi: &str,
2253 convention: FfiConvention,
2254 ffi_registry: &mut FfiRegistry,
2255) {
2256 match body_node.kind() {
2257 "declaration_list" => {
2258 let mut cursor = body_node.walk();
2260 for decl in body_node.children(&mut cursor) {
2261 if decl.kind() == "declaration"
2262 && let Some(fn_name) = extract_ffi_function_name(decl, content)
2263 {
2264 let qualified = format!("extern::{abi}::{fn_name}");
2265 ffi_registry.insert(fn_name, (qualified, convention));
2266 }
2267 }
2268 }
2269 "declaration" => {
2270 if let Some(fn_name) = extract_ffi_function_name(body_node, content) {
2272 let qualified = format!("extern::{abi}::{fn_name}");
2273 ffi_registry.insert(fn_name, (qualified, convention));
2274 }
2275 }
2276 _ => {}
2277 }
2278}
2279
2280fn extract_ffi_function_name(decl_node: Node<'_>, content: &[u8]) -> Option<String> {
2282 if let Some(declarator_node) = decl_node.child_by_field_name("declarator") {
2284 return extract_function_name_from_declarator(declarator_node, content);
2285 }
2286 None
2287}
2288
2289fn extract_function_name_from_declarator(node: Node<'_>, content: &[u8]) -> Option<String> {
2291 match node.kind() {
2292 "function_declarator" => {
2293 if let Some(inner) = node.child_by_field_name("declarator") {
2295 return extract_function_name_from_declarator(inner, content);
2296 }
2297 }
2298 "identifier" => {
2299 if let Ok(name) = node.utf8_text(content) {
2301 let name = name.trim();
2302 if !name.is_empty() {
2303 return Some(name.to_string());
2304 }
2305 }
2306 }
2307 "pointer_declarator" | "reference_declarator" => {
2308 if let Some(inner) = node.child_by_field_name("declarator") {
2310 return extract_function_name_from_declarator(inner, content);
2311 }
2312 }
2313 "parenthesized_declarator" => {
2314 let mut cursor = node.walk();
2316 for child in node.children(&mut cursor) {
2317 if let Some(name) = extract_function_name_from_declarator(child, content) {
2318 return Some(name);
2319 }
2320 }
2321 }
2322 _ => {}
2323 }
2324 None
2325}
2326
2327fn extract_ffi_abi(node: Node<'_>, content: &[u8]) -> String {
2331 if let Some(value_node) = node.child_by_field_name("value")
2333 && value_node.kind() == "string_literal"
2334 {
2335 let mut cursor = value_node.walk();
2337 for child in value_node.children(&mut cursor) {
2338 if child.kind() == "string_content"
2339 && let Ok(text) = child.utf8_text(content)
2340 {
2341 let trimmed = text.trim();
2342 if !trimmed.is_empty() {
2343 return trimmed.to_string();
2344 }
2345 }
2346 }
2347 }
2348 "C".to_string()
2350}
2351
2352fn abi_to_convention(abi: &str) -> FfiConvention {
2354 match abi.to_lowercase().as_str() {
2355 "system" => FfiConvention::System,
2356 "stdcall" => FfiConvention::Stdcall,
2357 "fastcall" => FfiConvention::Fastcall,
2358 "cdecl" => FfiConvention::Cdecl,
2359 _ => FfiConvention::C, }
2361}
2362
2363fn build_ffi_block_for_staging(
2367 node: Node<'_>,
2368 content: &[u8],
2369 helper: &mut GraphBuildHelper,
2370 namespace_stack: &[String],
2371) {
2372 let abi = extract_ffi_abi(node, content);
2374
2375 if let Some(body_node) = node.child_by_field_name("body") {
2377 build_ffi_from_body(body_node, content, &abi, helper, namespace_stack);
2378 }
2379}
2380
2381fn build_ffi_from_body(
2383 body_node: Node<'_>,
2384 content: &[u8],
2385 abi: &str,
2386 helper: &mut GraphBuildHelper,
2387 namespace_stack: &[String],
2388) {
2389 match body_node.kind() {
2390 "declaration_list" => {
2391 let mut cursor = body_node.walk();
2393 for decl in body_node.children(&mut cursor) {
2394 if decl.kind() == "declaration"
2395 && let Some(fn_name) = extract_ffi_function_name(decl, content)
2396 {
2397 let span = span_from_node(decl);
2398 let qualified = if namespace_stack.is_empty() {
2400 format!("extern::{abi}::{fn_name}")
2401 } else {
2402 format!("{}::extern::{abi}::{fn_name}", namespace_stack.join("::"))
2403 };
2404 helper.add_function(
2406 &qualified,
2407 Some(span),
2408 false, true, );
2411 }
2412 }
2413 }
2414 "declaration" => {
2415 if let Some(fn_name) = extract_ffi_function_name(body_node, content) {
2417 let span = span_from_node(body_node);
2418 let qualified = if namespace_stack.is_empty() {
2419 format!("extern::{abi}::{fn_name}")
2420 } else {
2421 format!("{}::extern::{abi}::{fn_name}", namespace_stack.join("::"))
2422 };
2423 helper.add_function(&qualified, Some(span), false, true);
2424 }
2425 }
2426 _ => {}
2427 }
2428}
2429
2430fn collect_pure_virtual_interfaces(
2440 node: Node<'_>,
2441 content: &[u8],
2442 registry: &mut PureVirtualRegistry,
2443 budget: &mut BuildBudget,
2444) -> GraphResult<()> {
2445 budget.checkpoint("cpp:collect_pure_virtual_interfaces")?;
2446 if matches!(node.kind(), "class_specifier" | "struct_specifier")
2447 && let Some(name_node) = node.child_by_field_name("name")
2448 && let Ok(class_name) = name_node.utf8_text(content)
2449 {
2450 let class_name = class_name.trim();
2451 if !class_name.is_empty() && has_pure_virtual_methods(node, content) {
2452 registry.insert(class_name.to_string());
2453 }
2454 }
2455
2456 let mut cursor = node.walk();
2458 for child in node.children(&mut cursor) {
2459 collect_pure_virtual_interfaces(child, content, registry, budget)?;
2460 }
2461
2462 Ok(())
2463}
2464
2465fn has_pure_virtual_methods(class_node: Node<'_>, content: &[u8]) -> bool {
2469 if let Some(body) = class_node.child_by_field_name("body") {
2470 let mut cursor = body.walk();
2471 for child in body.children(&mut cursor) {
2472 if child.kind() == "field_declaration" && is_pure_virtual_declaration(child, content) {
2474 return true;
2475 }
2476 }
2477 }
2478 false
2479}
2480
2481fn is_pure_virtual_declaration(decl_node: Node<'_>, content: &[u8]) -> bool {
2483 let mut has_virtual = false;
2484 let mut has_pure_specifier = false;
2485
2486 let mut cursor = decl_node.walk();
2488 for child in decl_node.children(&mut cursor) {
2489 match child.kind() {
2490 "virtual" => {
2491 has_virtual = true;
2492 }
2493 "number_literal" => {
2494 if let Ok(text) = child.utf8_text(content)
2497 && text.trim() == "0"
2498 {
2499 has_pure_specifier = true;
2500 }
2501 }
2502 _ => {}
2503 }
2504 }
2505
2506 has_virtual && has_pure_specifier
2507}
2508
2509fn build_inheritance_and_implements_edges(
2515 class_node: Node<'_>,
2516 content: &[u8],
2517 _qualified_class_name: &str,
2518 child_id: sqry_core::graph::unified::node::NodeId,
2519 helper: &mut GraphBuildHelper,
2520 namespace_stack: &[String],
2521 pure_virtual_registry: &PureVirtualRegistry,
2522) -> GraphResult<()> {
2523 let mut cursor = class_node.walk();
2525 let base_clause = class_node
2526 .children(&mut cursor)
2527 .find(|child| child.kind() == "base_class_clause");
2528
2529 let Some(base_clause) = base_clause else {
2530 return Ok(()); };
2532
2533 let mut clause_cursor = base_clause.walk();
2535 for child in base_clause.children(&mut clause_cursor) {
2536 match child.kind() {
2537 "type_identifier" => {
2538 let base_name = child
2539 .utf8_text(content)
2540 .map_err(|_| GraphBuilderError::ParseError {
2541 span: span_from_node(child),
2542 reason: "failed to read base class name".to_string(),
2543 })?
2544 .trim();
2545
2546 if !base_name.is_empty() {
2547 let qualified_base = if namespace_stack.is_empty() {
2549 base_name.to_string()
2550 } else {
2551 format!("{}::{}", namespace_stack.join("::"), base_name)
2552 };
2553
2554 if pure_virtual_registry.contains(base_name) {
2556 let interface_id = helper.add_interface(&qualified_base, None);
2558 helper.add_implements_edge(child_id, interface_id);
2559 } else {
2560 let parent_id = helper.add_class(&qualified_base, None);
2562 helper.add_inherits_edge(child_id, parent_id);
2563 }
2564 }
2565 }
2566 "qualified_identifier" => {
2567 let base_name = child
2569 .utf8_text(content)
2570 .map_err(|_| GraphBuilderError::ParseError {
2571 span: span_from_node(child),
2572 reason: "failed to read base class name".to_string(),
2573 })?
2574 .trim();
2575
2576 if !base_name.is_empty() {
2577 let simple_name = base_name.rsplit("::").next().unwrap_or(base_name);
2579
2580 if pure_virtual_registry.contains(simple_name) {
2581 let interface_id = helper.add_interface(base_name, None);
2582 helper.add_implements_edge(child_id, interface_id);
2583 } else {
2584 let parent_id = helper.add_class(base_name, None);
2585 helper.add_inherits_edge(child_id, parent_id);
2586 }
2587 }
2588 }
2589 "template_type" => {
2590 if let Some(template_name_node) = child.child_by_field_name("name")
2592 && let Ok(base_name) = template_name_node.utf8_text(content)
2593 {
2594 let base_name = base_name.trim();
2595 if !base_name.is_empty() {
2596 let qualified_base =
2597 if base_name.contains("::") || namespace_stack.is_empty() {
2598 base_name.to_string()
2599 } else {
2600 format!("{}::{}", namespace_stack.join("::"), base_name)
2601 };
2602
2603 if pure_virtual_registry.contains(base_name) {
2606 let interface_id = helper.add_interface(&qualified_base, None);
2607 helper.add_implements_edge(child_id, interface_id);
2608 } else {
2609 let parent_id = helper.add_class(&qualified_base, None);
2610 helper.add_inherits_edge(child_id, parent_id);
2611 }
2612 }
2613 }
2614 }
2615 _ => {
2616 }
2618 }
2619 }
2620
2621 Ok(())
2622}
2623
2624fn span_from_node(node: Node<'_>) -> Span {
2625 let start = node.start_position();
2626 let end = node.end_position();
2627 Span::new(
2628 sqry_core::graph::node::Position::new(start.row, start.column),
2629 sqry_core::graph::node::Position::new(end.row, end.column),
2630 )
2631}
2632
2633fn count_arguments(node: Node<'_>) -> usize {
2634 node.child_by_field_name("arguments").map_or(0, |args| {
2635 let mut count = 0;
2636 let mut cursor = args.walk();
2637 for child in args.children(&mut cursor) {
2638 if !matches!(child.kind(), "(" | ")" | ",") {
2639 count += 1;
2640 }
2641 }
2642 count
2643 })
2644}
2645
2646#[cfg(test)]
2647mod tests {
2648 use super::*;
2649 use sqry_core::graph::unified::build::test_helpers::{
2650 assert_has_node, assert_has_node_with_kind, assert_has_node_with_kind_exact,
2651 collect_call_edges,
2652 };
2653 use sqry_core::graph::unified::node::NodeKind;
2654 use tree_sitter::Parser;
2655
2656 fn parse_cpp(source: &str) -> Tree {
2657 let mut parser = Parser::new();
2658 parser
2659 .set_language(&tree_sitter_cpp::LANGUAGE.into())
2660 .expect("Failed to set Cpp language");
2661 parser
2662 .parse(source.as_bytes(), None)
2663 .expect("Failed to parse Cpp source")
2664 }
2665
2666 fn test_budget() -> BuildBudget {
2667 BuildBudget::new(Path::new("test.cpp"))
2668 }
2669
2670 fn extract_namespace_map_for_test(
2671 tree: &Tree,
2672 source: &str,
2673 ) -> HashMap<std::ops::Range<usize>, String> {
2674 let mut budget = test_budget();
2675 extract_namespace_map(tree.root_node(), source.as_bytes(), &mut budget)
2676 .expect("namespace extraction should succeed in tests")
2677 }
2678
2679 fn extract_cpp_contexts_for_test(
2680 tree: &Tree,
2681 source: &str,
2682 namespace_map: &HashMap<std::ops::Range<usize>, String>,
2683 ) -> Vec<FunctionContext> {
2684 let mut budget = test_budget();
2685 extract_cpp_contexts(
2686 tree.root_node(),
2687 source.as_bytes(),
2688 namespace_map,
2689 &mut budget,
2690 )
2691 .expect("context extraction should succeed in tests")
2692 }
2693
2694 fn extract_field_and_type_info_for_test(
2695 tree: &Tree,
2696 source: &str,
2697 namespace_map: &HashMap<std::ops::Range<usize>, String>,
2698 ) -> (QualifiedNameMap, QualifiedNameMap) {
2699 let mut budget = test_budget();
2700 extract_field_and_type_info(
2701 tree.root_node(),
2702 source.as_bytes(),
2703 namespace_map,
2704 &mut budget,
2705 )
2706 .expect("field/type extraction should succeed in tests")
2707 }
2708
2709 #[test]
2710 fn test_build_graph_times_out_with_expired_budget() {
2711 let source = r"
2712 namespace demo {
2713 class Service {
2714 public:
2715 void process() {}
2716 };
2717 }
2718 ";
2719 let tree = parse_cpp(source);
2720 let builder = CppGraphBuilder::new();
2721 let mut staging = StagingGraph::new();
2722 let mut budget = BuildBudget::already_expired(Path::new("timeout.cpp"));
2723
2724 let err = builder
2725 .build_graph_with_budget(
2726 &tree,
2727 source.as_bytes(),
2728 Path::new("timeout.cpp"),
2729 &mut staging,
2730 &mut budget,
2731 )
2732 .expect_err("expired budget should force timeout");
2733
2734 match err {
2735 GraphBuilderError::BuildTimedOut {
2736 file,
2737 phase,
2738 timeout_ms,
2739 } => {
2740 assert_eq!(file, PathBuf::from("timeout.cpp"));
2741 assert_eq!(phase, "cpp:extract_namespace_map");
2742 assert_eq!(timeout_ms, 1_000);
2743 }
2744 other => panic!("expected BuildTimedOut, got {other:?}"),
2745 }
2746 }
2747
2748 #[test]
2749 fn test_extract_class() {
2750 let source = "class User { }";
2751 let tree = parse_cpp(source);
2752 let mut staging = StagingGraph::new();
2753 let builder = CppGraphBuilder::new();
2754
2755 let result = builder.build_graph(
2756 &tree,
2757 source.as_bytes(),
2758 Path::new("test.cpp"),
2759 &mut staging,
2760 );
2761
2762 assert!(result.is_ok());
2763 assert_has_node_with_kind(&staging, "User", NodeKind::Class);
2764 }
2765
2766 #[test]
2767 fn test_extract_template_class() {
2768 let source = r"
2769 template <typename T>
2770 class Person {
2771 public:
2772 T name;
2773 T age;
2774 };
2775 ";
2776 let tree = parse_cpp(source);
2777 let mut staging = StagingGraph::new();
2778 let builder = CppGraphBuilder::new();
2779
2780 let result = builder.build_graph(
2781 &tree,
2782 source.as_bytes(),
2783 Path::new("test.cpp"),
2784 &mut staging,
2785 );
2786
2787 assert!(result.is_ok());
2788 assert_has_node_with_kind(&staging, "Person", NodeKind::Class);
2789 }
2790
2791 #[test]
2792 fn test_nested_named_types_emit_nodes() {
2793 let source = r"
2798 class Outer {
2799 public:
2800 class Inner { int z; };
2801 struct InnerS { int w; };
2802 union InnerU { int i; float f; };
2803 enum class InnerE { A, B };
2804 class L1 { public: class L2 { int q; }; };
2805 };
2806 namespace ns {
2807 class NsOuter { public: class NsInner { int n; }; };
2808 }
2809 ";
2810 let staging = build_cpp(source);
2811
2812 assert_has_node_with_kind_exact(&staging, "Outer::Inner", NodeKind::Class);
2814 assert_has_node_with_kind_exact(&staging, "Outer::InnerS", NodeKind::Struct);
2815 assert_has_node_with_kind_exact(&staging, "Outer::InnerU", NodeKind::Struct);
2817 assert_has_node_with_kind_exact(&staging, "Outer::InnerE", NodeKind::Enum);
2818 assert_has_node_with_kind_exact(&staging, "Outer::L1", NodeKind::Class);
2820 assert_has_node_with_kind_exact(&staging, "Outer::L1::L2", NodeKind::Class);
2821 assert_has_node_with_kind_exact(&staging, "ns::NsOuter", NodeKind::Class);
2823 assert_has_node_with_kind_exact(&staging, "ns::NsOuter::NsInner", NodeKind::Class);
2824
2825 assert_has_node_with_kind_exact(&staging, "Outer::Inner.z", NodeKind::Property);
2828 assert_has_node_with_kind_exact(&staging, "Outer::L1::L2.q", NodeKind::Property);
2829 assert_has_node_with_kind_exact(&staging, "ns::NsOuter::NsInner.n", NodeKind::Property);
2830 }
2831
2832 #[test]
2833 fn test_nested_enum_carries_enclosing_visibility() {
2834 let source = r"
2838 class Outer {
2839 private:
2840 enum class Secret { A, B };
2841 public:
2842 enum class Pub { X, Y };
2843 };
2844 ";
2845 let staging = build_cpp(source);
2846
2847 let secret = cpp_find_added_node(&staging, "Outer::Secret")
2848 .expect("nested enum Outer::Secret must be staged");
2849 assert_eq!(secret.kind, NodeKind::Enum, "Secret must be an Enum node");
2850 let secret_vis = staging.resolve_local_string(
2851 secret
2852 .visibility
2853 .expect("nested enum must carry a visibility id"),
2854 );
2855 assert_eq!(
2856 secret_vis,
2857 Some("private"),
2858 "nested enum under `private:` must be private"
2859 );
2860
2861 let pub_enum = cpp_find_added_node(&staging, "Outer::Pub")
2862 .expect("nested enum Outer::Pub must be staged");
2863 let pub_vis = staging.resolve_local_string(
2864 pub_enum
2865 .visibility
2866 .expect("nested enum must carry a visibility id"),
2867 );
2868 assert_eq!(
2869 pub_vis,
2870 Some("public"),
2871 "nested enum under `public:` must be public"
2872 );
2873 }
2874
2875 #[test]
2876 fn test_nested_class_emits_inheritance_edge() {
2877 let source = r"
2881 struct Base { virtual ~Base(); };
2882 class Outer {
2883 public:
2884 class Derived : public Base {};
2885 };
2886 ";
2887 let staging = build_cpp(source);
2888
2889 let derived_id = cpp_find_added_node_id(&staging, "Outer::Derived", NodeKind::Class)
2890 .expect("nested Derived class node must be staged");
2891
2892 let has_inherits = staging.operations().iter().any(|op| {
2893 matches!(
2894 op,
2895 StagingOp::AddEdge {
2896 source: src,
2897 kind: EdgeKind::Inherits,
2898 ..
2899 } if *src == derived_id
2900 )
2901 });
2902 assert!(
2903 has_inherits,
2904 "nested Derived must emit an Inherits edge to its base"
2905 );
2906 }
2907
2908 #[test]
2909 fn test_top_level_union_emits_struct_node() {
2910 let source = "union Value { int i; float f; };";
2914 let staging = build_cpp(source);
2915 assert_has_node_with_kind_exact(&staging, "Value", NodeKind::Struct);
2916 }
2917
2918 #[test]
2919 fn test_extract_function() {
2920 let source = r#"
2921 #include <cstdio>
2922 void hello() {
2923 std::printf("Hello");
2924 }
2925 "#;
2926 let tree = parse_cpp(source);
2927 let mut staging = StagingGraph::new();
2928 let builder = CppGraphBuilder::new();
2929
2930 let result = builder.build_graph(
2931 &tree,
2932 source.as_bytes(),
2933 Path::new("test.cpp"),
2934 &mut staging,
2935 );
2936
2937 assert!(result.is_ok());
2938 assert_has_node_with_kind(&staging, "hello", NodeKind::Function);
2939 }
2940
2941 #[test]
2942 fn test_extract_virtual_function() {
2943 let source = r"
2944 class Service {
2945 public:
2946 virtual void fetchData() {}
2947 };
2948 ";
2949 let tree = parse_cpp(source);
2950 let mut staging = StagingGraph::new();
2951 let builder = CppGraphBuilder::new();
2952
2953 let result = builder.build_graph(
2954 &tree,
2955 source.as_bytes(),
2956 Path::new("test.cpp"),
2957 &mut staging,
2958 );
2959
2960 assert!(result.is_ok());
2961 assert_has_node(&staging, "fetchData");
2962 }
2963
2964 #[test]
2965 fn test_extract_call_edge() {
2966 let source = r"
2967 void greet() {}
2968
2969 int main() {
2970 greet();
2971 return 0;
2972 }
2973 ";
2974 let tree = parse_cpp(source);
2975 let mut staging = StagingGraph::new();
2976 let builder = CppGraphBuilder::new();
2977
2978 let result = builder.build_graph(
2979 &tree,
2980 source.as_bytes(),
2981 Path::new("test.cpp"),
2982 &mut staging,
2983 );
2984
2985 assert!(result.is_ok());
2986 assert_has_node(&staging, "main");
2987 assert_has_node(&staging, "greet");
2988 let calls = collect_call_edges(&staging);
2989 assert!(!calls.is_empty());
2990 }
2991
2992 #[test]
2993 fn test_extract_member_call_edge() {
2994 let source = r"
2995 class Service {
2996 public:
2997 void helper() {}
2998 };
2999
3000 int main() {
3001 Service svc;
3002 svc.helper();
3003 return 0;
3004 }
3005 ";
3006 let tree = parse_cpp(source);
3007 let mut staging = StagingGraph::new();
3008 let builder = CppGraphBuilder::new();
3009
3010 let result = builder.build_graph(
3011 &tree,
3012 source.as_bytes(),
3013 Path::new("member.cpp"),
3014 &mut staging,
3015 );
3016
3017 assert!(result.is_ok());
3018 assert_has_node(&staging, "main");
3019 assert_has_node(&staging, "helper");
3020 let calls = collect_call_edges(&staging);
3021 assert!(!calls.is_empty());
3022 }
3023
3024 #[test]
3025 fn test_extract_namespace_map_simple() {
3026 let source = r"
3027 namespace demo {
3028 void func() {}
3029 }
3030 ";
3031 let tree = parse_cpp(source);
3032 let namespace_map = extract_namespace_map_for_test(&tree, source);
3033
3034 assert_eq!(namespace_map.len(), 1);
3036
3037 let (_, ns_prefix) = namespace_map.iter().next().unwrap();
3039 assert_eq!(ns_prefix, "demo::");
3040 }
3041
3042 #[test]
3043 fn test_extract_namespace_map_nested() {
3044 let source = r"
3045 namespace outer {
3046 namespace inner {
3047 void func() {}
3048 }
3049 }
3050 ";
3051 let tree = parse_cpp(source);
3052 let namespace_map = extract_namespace_map_for_test(&tree, source);
3053
3054 assert!(namespace_map.len() >= 2);
3056
3057 let ns_values: Vec<&String> = namespace_map.values().collect();
3059 assert!(ns_values.iter().any(|v| v.as_str() == "outer::"));
3060 assert!(ns_values.iter().any(|v| v.as_str() == "outer::inner::"));
3061 }
3062
3063 #[test]
3064 fn test_extract_namespace_map_multiple() {
3065 let source = r"
3066 namespace first {
3067 void func1() {}
3068 }
3069 namespace second {
3070 void func2() {}
3071 }
3072 ";
3073 let tree = parse_cpp(source);
3074 let namespace_map = extract_namespace_map_for_test(&tree, source);
3075
3076 assert_eq!(namespace_map.len(), 2);
3078
3079 let ns_values: Vec<&String> = namespace_map.values().collect();
3080 assert!(ns_values.iter().any(|v| v.as_str() == "first::"));
3081 assert!(ns_values.iter().any(|v| v.as_str() == "second::"));
3082 }
3083
3084 #[test]
3085 fn test_find_namespace_for_offset() {
3086 let source = r"
3087 namespace demo {
3088 void func() {}
3089 }
3090 ";
3091 let tree = parse_cpp(source);
3092 let namespace_map = extract_namespace_map_for_test(&tree, source);
3093
3094 let func_offset = source.find("func").unwrap();
3096 let ns = find_namespace_for_offset(func_offset, &namespace_map);
3097 assert_eq!(ns, "demo::");
3098
3099 let ns = find_namespace_for_offset(0, &namespace_map);
3101 assert_eq!(ns, "");
3102 }
3103
3104 #[test]
3105 fn test_extract_cpp_contexts_free_function() {
3106 let source = r"
3107 void helper() {}
3108 ";
3109 let tree = parse_cpp(source);
3110 let namespace_map = extract_namespace_map_for_test(&tree, source);
3111 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3112
3113 assert_eq!(contexts.len(), 1);
3114 assert_eq!(contexts[0].qualified_name, "helper");
3115 assert!(!contexts[0].is_static);
3116 assert!(!contexts[0].is_virtual);
3117 }
3118
3119 #[test]
3120 fn test_extract_cpp_contexts_namespace_function() {
3121 let source = r"
3122 namespace demo {
3123 void helper() {}
3124 }
3125 ";
3126 let tree = parse_cpp(source);
3127 let namespace_map = extract_namespace_map_for_test(&tree, source);
3128 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3129
3130 assert_eq!(contexts.len(), 1);
3131 assert_eq!(contexts[0].qualified_name, "demo::helper");
3132 assert_eq!(contexts[0].namespace_stack, vec!["demo"]);
3133 }
3134
3135 #[test]
3136 fn test_extract_cpp_contexts_class_method() {
3137 let source = r"
3138 class Service {
3139 public:
3140 void process() {}
3141 };
3142 ";
3143 let tree = parse_cpp(source);
3144 let namespace_map = extract_namespace_map_for_test(&tree, source);
3145 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3146
3147 assert_eq!(contexts.len(), 1);
3148 assert_eq!(contexts[0].qualified_name, "Service::process");
3149 assert_eq!(contexts[0].class_stack, vec!["Service"]);
3150 }
3151
3152 #[test]
3153 fn test_extract_cpp_contexts_namespace_and_class() {
3154 let source = r"
3155 namespace demo {
3156 class Service {
3157 public:
3158 void process() {}
3159 };
3160 }
3161 ";
3162 let tree = parse_cpp(source);
3163 let namespace_map = extract_namespace_map_for_test(&tree, source);
3164 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3165
3166 assert_eq!(contexts.len(), 1);
3167 assert_eq!(contexts[0].qualified_name, "demo::Service::process");
3168 assert_eq!(contexts[0].namespace_stack, vec!["demo"]);
3169 assert_eq!(contexts[0].class_stack, vec!["Service"]);
3170 }
3171
3172 #[test]
3173 fn test_extract_cpp_contexts_static_method() {
3174 let source = r"
3175 class Repository {
3176 public:
3177 static void save() {}
3178 };
3179 ";
3180 let tree = parse_cpp(source);
3181 let namespace_map = extract_namespace_map_for_test(&tree, source);
3182 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3183
3184 assert_eq!(contexts.len(), 1);
3185 assert_eq!(contexts[0].qualified_name, "Repository::save");
3186 assert!(contexts[0].is_static);
3187 }
3188
3189 #[test]
3190 fn test_extract_cpp_contexts_virtual_method() {
3191 let source = r"
3192 class Base {
3193 public:
3194 virtual void render() {}
3195 };
3196 ";
3197 let tree = parse_cpp(source);
3198 let namespace_map = extract_namespace_map_for_test(&tree, source);
3199 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3200
3201 assert_eq!(contexts.len(), 1);
3202 assert_eq!(contexts[0].qualified_name, "Base::render");
3203 assert!(contexts[0].is_virtual);
3204 }
3205
3206 #[test]
3207 fn test_extract_cpp_contexts_inline_function() {
3208 let source = r"
3209 inline void helper() {}
3210 ";
3211 let tree = parse_cpp(source);
3212 let namespace_map = extract_namespace_map_for_test(&tree, source);
3213 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3214
3215 assert_eq!(contexts.len(), 1);
3216 assert_eq!(contexts[0].qualified_name, "helper");
3217 assert!(contexts[0].is_inline);
3218 }
3219
3220 #[test]
3221 fn test_extract_cpp_contexts_out_of_line_definition() {
3222 let source = r"
3223 namespace demo {
3224 class Service {
3225 public:
3226 int process(int v);
3227 };
3228
3229 inline int Service::process(int v) {
3230 return v;
3231 }
3232 }
3233 ";
3234 let tree = parse_cpp(source);
3235 let namespace_map = extract_namespace_map_for_test(&tree, source);
3236 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3237
3238 assert_eq!(contexts.len(), 1);
3240 assert_eq!(contexts[0].qualified_name, "demo::Service::process");
3241 assert!(contexts[0].is_inline);
3242 }
3243
3244 #[test]
3245 fn test_extract_field_types_simple() {
3246 let source = r"
3247 class Service {
3248 public:
3249 Repository repo;
3250 };
3251 ";
3252 let tree = parse_cpp(source);
3253 let namespace_map = extract_namespace_map_for_test(&tree, source);
3254 let (field_types, _type_map) =
3255 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3256
3257 assert_eq!(field_types.len(), 1);
3259 assert_eq!(
3260 field_types.get(&("Service".to_string(), "repo".to_string())),
3261 Some(&"Repository".to_string())
3262 );
3263 }
3264
3265 #[test]
3266 fn test_extract_field_types_namespace() {
3267 let source = r"
3268 namespace demo {
3269 class Service {
3270 public:
3271 Repository repo;
3272 };
3273 }
3274 ";
3275 let tree = parse_cpp(source);
3276 let namespace_map = extract_namespace_map_for_test(&tree, source);
3277 let (field_types, _type_map) =
3278 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3279
3280 assert_eq!(field_types.len(), 1);
3282 assert_eq!(
3283 field_types.get(&("demo::Service".to_string(), "repo".to_string())),
3284 Some(&"Repository".to_string())
3285 );
3286 }
3287
3288 #[test]
3289 fn test_extract_field_types_no_collision() {
3290 let source = r"
3291 class ServiceA {
3292 public:
3293 Repository repo;
3294 };
3295
3296 class ServiceB {
3297 public:
3298 Repository repo;
3299 };
3300 ";
3301 let tree = parse_cpp(source);
3302 let namespace_map = extract_namespace_map_for_test(&tree, source);
3303 let (field_types, _type_map) =
3304 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3305
3306 assert_eq!(field_types.len(), 2);
3308 assert_eq!(
3309 field_types.get(&("ServiceA".to_string(), "repo".to_string())),
3310 Some(&"Repository".to_string())
3311 );
3312 assert_eq!(
3313 field_types.get(&("ServiceB".to_string(), "repo".to_string())),
3314 Some(&"Repository".to_string())
3315 );
3316 }
3317
3318 #[test]
3319 fn test_extract_using_declaration() {
3320 let source = r"
3321 using std::vector;
3322
3323 class Service {
3324 public:
3325 vector data;
3326 };
3327 ";
3328 let tree = parse_cpp(source);
3329 let namespace_map = extract_namespace_map_for_test(&tree, source);
3330 let (field_types, type_map) =
3331 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3332
3333 assert_eq!(field_types.len(), 1);
3335 assert_eq!(
3336 field_types.get(&("Service".to_string(), "data".to_string())),
3337 Some(&"std::vector".to_string()),
3338 "Field type should resolve 'vector' to 'std::vector' via using declaration"
3339 );
3340
3341 assert_eq!(
3343 type_map.get(&(String::new(), "vector".to_string())),
3344 Some(&"std::vector".to_string()),
3345 "Using declaration should map 'vector' to 'std::vector' in type_map"
3346 );
3347 }
3348
3349 #[test]
3350 fn test_extract_field_types_pointer() {
3351 let source = r"
3352 class Service {
3353 public:
3354 Repository* repo;
3355 };
3356 ";
3357 let tree = parse_cpp(source);
3358 let namespace_map = extract_namespace_map_for_test(&tree, source);
3359 let (field_types, _type_map) =
3360 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3361
3362 assert_eq!(field_types.len(), 1);
3364 assert_eq!(
3365 field_types.get(&("Service".to_string(), "repo".to_string())),
3366 Some(&"Repository".to_string())
3367 );
3368 }
3369
3370 #[test]
3371 fn test_extract_field_types_multiple_declarators() {
3372 let source = r"
3373 class Service {
3374 public:
3375 Repository repo_a, repo_b, repo_c;
3376 };
3377 ";
3378 let tree = parse_cpp(source);
3379 let namespace_map = extract_namespace_map_for_test(&tree, source);
3380 let (field_types, _type_map) =
3381 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3382
3383 assert_eq!(field_types.len(), 3);
3385 assert_eq!(
3386 field_types.get(&("Service".to_string(), "repo_a".to_string())),
3387 Some(&"Repository".to_string())
3388 );
3389 assert_eq!(
3390 field_types.get(&("Service".to_string(), "repo_b".to_string())),
3391 Some(&"Repository".to_string())
3392 );
3393 assert_eq!(
3394 field_types.get(&("Service".to_string(), "repo_c".to_string())),
3395 Some(&"Repository".to_string())
3396 );
3397 }
3398
3399 #[test]
3400 fn test_extract_field_types_nested_struct_with_parent_field() {
3401 let source = r"
3404 namespace demo {
3405 struct Outer {
3406 int outer_field;
3407 struct Inner {
3408 int inner_field;
3409 };
3410 Inner nested_instance;
3411 };
3412 }
3413 ";
3414 let tree = parse_cpp(source);
3415 let namespace_map = extract_namespace_map_for_test(&tree, source);
3416 let (field_types, _type_map) =
3417 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3418
3419 assert!(
3422 field_types.len() >= 2,
3423 "Expected at least outer_field and nested_instance"
3424 );
3425
3426 assert_eq!(
3428 field_types.get(&("demo::Outer".to_string(), "outer_field".to_string())),
3429 Some(&"int".to_string())
3430 );
3431
3432 assert_eq!(
3434 field_types.get(&("demo::Outer".to_string(), "nested_instance".to_string())),
3435 Some(&"Inner".to_string())
3436 );
3437
3438 if field_types.contains_key(&("demo::Outer::Inner".to_string(), "inner_field".to_string()))
3440 {
3441 assert_eq!(
3443 field_types.get(&("demo::Outer::Inner".to_string(), "inner_field".to_string())),
3444 Some(&"int".to_string()),
3445 "Inner class fields must use parent-qualified FQN 'demo::Outer::Inner'"
3446 );
3447 }
3448 }
3449
3450 use sqry_core::graph::unified::build::staging::StagingOp;
3467 use sqry_core::graph::unified::edge::kind::{EdgeKind, TypeOfContext};
3468
3469 fn cpp_find_added_node<'a>(
3471 staging: &'a StagingGraph,
3472 canonical_name: &str,
3473 ) -> Option<&'a sqry_core::graph::unified::storage::arena::NodeEntry> {
3474 staging.operations().iter().find_map(|op| {
3475 if let StagingOp::AddNode { entry, .. } = op
3476 && staging.resolve_node_canonical_name(entry) == Some(canonical_name)
3477 {
3478 Some(entry)
3479 } else {
3480 None
3481 }
3482 })
3483 }
3484
3485 fn cpp_find_added_node_id(
3487 staging: &StagingGraph,
3488 canonical_name: &str,
3489 kind: NodeKind,
3490 ) -> Option<sqry_core::graph::unified::NodeId> {
3491 staging.operations().iter().find_map(|op| match op {
3492 StagingOp::AddNode {
3493 entry,
3494 expected_id: Some(id),
3495 } if entry.kind == kind
3496 && staging.resolve_node_canonical_name(entry) == Some(canonical_name) =>
3497 {
3498 Some(*id)
3499 }
3500 _ => None,
3501 })
3502 }
3503
3504 fn build_cpp(source: &str) -> StagingGraph {
3506 let tree = parse_cpp(source);
3507 let mut staging = StagingGraph::new();
3508 let builder = CppGraphBuilder::new();
3509 builder
3510 .build_graph(
3511 &tree,
3512 source.as_bytes(),
3513 Path::new("test.cpp"),
3514 &mut staging,
3515 )
3516 .expect("build_graph must succeed for the test fixture");
3517 staging
3518 }
3519
3520 #[test]
3526 fn test_struct_field_emits_property_with_field_context() {
3527 let source = "struct Point { int x; int y; };";
3528 let staging = build_cpp(source);
3529
3530 assert_has_node_with_kind_exact(&staging, "Point.x", NodeKind::Property);
3532 assert_has_node_with_kind_exact(&staging, "Point.y", NodeKind::Property);
3533
3534 let entry =
3535 cpp_find_added_node(&staging, "Point.x").expect("Point.x should be staged as a node");
3536 assert_eq!(entry.kind, NodeKind::Property, "x must be Property");
3537 assert!(!entry.is_static, "instance field is_static must be false");
3538 let vis = staging.resolve_local_string(entry.visibility.expect("visibility id"));
3539 assert_eq!(
3540 vis,
3541 Some("public"),
3542 "struct default visibility must be 'public'"
3543 );
3544 assert!(entry.end_line > 0, "field end_line must be set (got 0)");
3550 assert!(
3551 entry.end_line > entry.start_line
3552 || (entry.end_line == entry.start_line && entry.end_column > entry.start_column),
3553 "field span must be non-empty: [{}:{}..{}:{}]",
3554 entry.start_line,
3555 entry.start_column,
3556 entry.end_line,
3557 entry.end_column,
3558 );
3559
3560 let x_id = cpp_find_added_node_id(&staging, "Point.x", NodeKind::Property)
3562 .expect("Point.x Property NodeId");
3563 let edge = staging.operations().iter().find_map(|op| {
3564 if let StagingOp::AddEdge {
3565 source: src,
3566 kind: EdgeKind::TypeOf { context, name, .. },
3567 ..
3568 } = op
3569 && *src == x_id
3570 {
3571 Some((*context, *name))
3572 } else {
3573 None
3574 }
3575 });
3576 let (ctx, name) = edge.expect("TypeOf edge from Point.x should be staged");
3577 assert_eq!(
3578 ctx,
3579 Some(TypeOfContext::Field),
3580 "TypeOf edge context must be Field"
3581 );
3582 let resolved_name = name.and_then(|sid| staging.resolve_local_string(sid));
3583 assert_eq!(
3584 resolved_name,
3585 Some("x"),
3586 "TypeOf edge name must be the bare field name 'x'"
3587 );
3588
3589 let stale_variable = staging.nodes().any(|n| {
3591 n.entry.kind == NodeKind::Variable
3592 && matches!(
3593 staging.resolve_node_name(n.entry),
3594 Some("Point.x" | "Point.y" | "Point::x" | "Point::y")
3595 )
3596 });
3597 assert!(
3598 !stale_variable,
3599 "Point fields must not be emitted as NodeKind::Variable"
3600 );
3601 }
3602
3603 #[test]
3605 fn test_class_field_default_visibility_is_private() {
3606 let source = "class Foo { int hidden; };";
3607 let staging = build_cpp(source);
3608
3609 let entry = cpp_find_added_node(&staging, "Foo.hidden")
3610 .expect("Foo.hidden should be staged as a node");
3611 assert_eq!(entry.kind, NodeKind::Property);
3612 let vis = staging.resolve_local_string(entry.visibility.expect("visibility id"));
3613 assert_eq!(
3614 vis,
3615 Some("private"),
3616 "class default visibility must be 'private'"
3617 );
3618 }
3619
3620 #[test]
3622 fn test_class_field_respects_explicit_access_specifier() {
3623 let source = "class Foo { public: int public_field; protected: int prot_field; };";
3624 let staging = build_cpp(source);
3625
3626 let pub_entry = cpp_find_added_node(&staging, "Foo.public_field")
3627 .expect("Foo.public_field should be staged");
3628 assert_eq!(
3629 staging.resolve_local_string(pub_entry.visibility.expect("vis")),
3630 Some("public")
3631 );
3632
3633 let prot_entry = cpp_find_added_node(&staging, "Foo.prot_field")
3634 .expect("Foo.prot_field should be staged");
3635 assert_eq!(
3636 staging.resolve_local_string(prot_entry.visibility.expect("vis")),
3637 Some("protected")
3638 );
3639 }
3640
3641 #[test]
3644 fn test_const_field_emits_constant() {
3645 let source = "class Foo { const int kMax = 0; };";
3646 let staging = build_cpp(source);
3647
3648 assert_has_node_with_kind_exact(&staging, "Foo.kMax", NodeKind::Constant);
3649 let entry = cpp_find_added_node(&staging, "Foo.kMax").expect("Foo.kMax");
3650 assert_eq!(entry.kind, NodeKind::Constant);
3651 assert!(
3652 !entry.is_static,
3653 "const (non-static) field is_static must be false; only `static` keyword sets is_static"
3654 );
3655 }
3656
3657 #[test]
3661 fn test_constexpr_field_emits_constant() {
3662 let source = "class Foo { constexpr static int kAnswer = 42; };";
3663 let staging = build_cpp(source);
3664
3665 assert_has_node_with_kind_exact(&staging, "Foo.kAnswer", NodeKind::Constant);
3666 let entry = cpp_find_added_node(&staging, "Foo.kAnswer").expect("Foo.kAnswer");
3667 assert_eq!(entry.kind, NodeKind::Constant);
3668 assert!(
3669 entry.is_static,
3670 "static constexpr member must have is_static = true"
3671 );
3672 }
3673
3674 #[test]
3677 fn test_static_field_sets_is_static_true() {
3678 let source = "class Foo { static int counter; };";
3679 let staging = build_cpp(source);
3680
3681 let entry = cpp_find_added_node(&staging, "Foo.counter").expect("Foo.counter");
3682 assert_eq!(entry.kind, NodeKind::Property);
3683 assert!(entry.is_static, "static keyword must set is_static = true");
3684 }
3685
3686 #[test]
3689 fn test_bitfield_emits_property() {
3690 let source = "struct Flags { unsigned int low : 4; unsigned int high : 4; };";
3691 let staging = build_cpp(source);
3692
3693 assert_has_node_with_kind_exact(&staging, "Flags.low", NodeKind::Property);
3694 assert_has_node_with_kind_exact(&staging, "Flags.high", NodeKind::Property);
3695 }
3696
3697 #[test]
3703 fn test_anonymous_union_member_fields_emit_property() {
3704 let source = r"
3705class Variant {
3706public:
3707 int tag;
3708 union {
3709 int as_int;
3710 float as_float;
3711 };
3712};
3713";
3714 let staging = build_cpp(source);
3715
3716 assert_has_node_with_kind_exact(&staging, "Variant.tag", NodeKind::Property);
3718
3719 assert_has_node_with_kind_exact(&staging, "Variant.as_int", NodeKind::Property);
3722 assert_has_node_with_kind_exact(&staging, "Variant.as_float", NodeKind::Property);
3723
3724 let as_int = cpp_find_added_node(&staging, "Variant.as_int")
3727 .expect("Variant.as_int should be staged");
3728 let vis = staging.resolve_local_string(as_int.visibility.expect("visibility id"));
3729 assert_eq!(
3730 vis,
3731 Some("public"),
3732 "anonymous-union members must inherit OUTER access (`public:` here)"
3733 );
3734
3735 let bogus = staging.nodes().any(|n| {
3738 staging
3739 .resolve_node_name(n.entry)
3740 .is_some_and(|name| name.contains("::.") || name.starts_with("Variant::."))
3741 });
3742 assert!(
3743 !bogus,
3744 "anonymous union must not produce a synthetic qualifier"
3745 );
3746
3747 let stale_variable = staging.nodes().any(|n| {
3749 n.entry.kind == NodeKind::Variable
3750 && matches!(
3751 staging.resolve_node_name(n.entry),
3752 Some("Variant.tag" | "Variant.as_int" | "Variant.as_float")
3753 )
3754 });
3755 assert!(
3756 !stale_variable,
3757 "anonymous-union members + outer fields must not stay as Variable"
3758 );
3759 }
3760
3761 #[test]
3765 fn test_templated_class_field_emits_property() {
3766 let source = r"
3767template<class T>
3768struct Box {
3769 T value;
3770};
3771";
3772 let staging = build_cpp(source);
3773
3774 assert_has_node_with_kind_exact(&staging, "Box.value", NodeKind::Property);
3775 let entry = cpp_find_added_node(&staging, "Box.value").expect("Box.value");
3776 assert_eq!(entry.kind, NodeKind::Property);
3777 assert!(!entry.is_static);
3778 }
3779
3780 #[test]
3786 fn test_outer_class_field_with_nested_class_present() {
3787 let source = r"
3788class Outer {
3789public:
3790 int outer_value;
3791 class Inner {
3792 public:
3793 int x;
3794 };
3795};
3796";
3797 let staging = build_cpp(source);
3798
3799 assert_has_node_with_kind_exact(&staging, "Outer.outer_value", NodeKind::Property);
3801
3802 assert_has_node_with_kind_exact(&staging, "Outer::Inner.x", NodeKind::Property);
3806
3807 let legacy_hits: Vec<_> = staging
3810 .nodes()
3811 .filter(|n| staging.resolve_node_name(n.entry) == Some("Outer::outer_value"))
3812 .collect();
3813 assert!(
3814 legacy_hits.is_empty(),
3815 "legacy `Outer::outer_value` lookup must return 0 hits"
3816 );
3817
3818 for legacy in ["Inner.x", "Outer::Inner::x", "Outer.Inner.x"] {
3822 let hits: Vec<_> = staging
3823 .nodes()
3824 .filter(|n| staging.resolve_node_name(n.entry) == Some(legacy))
3825 .collect();
3826 assert!(
3827 hits.is_empty(),
3828 "nested-class field `{legacy}` must not appear; expected only `Outer::Inner.x`"
3829 );
3830 }
3831 }
3832
3833 #[test]
3837 fn test_outer_class_with_nested_struct_emits_inner_field() {
3838 let source = r"
3839class Outer {
3840private:
3841 struct Inner {
3842 int y;
3843 };
3844};
3845";
3846 let staging = build_cpp(source);
3847
3848 assert_has_node_with_kind_exact(&staging, "Outer::Inner.y", NodeKind::Property);
3849
3850 let entry = cpp_find_added_node(&staging, "Outer::Inner.y")
3851 .expect("Outer::Inner.y should be staged");
3852 let vis = staging.resolve_local_string(entry.visibility.expect("visibility id"));
3853 assert_eq!(
3854 vis,
3855 Some("public"),
3856 "nested struct field default visibility must be 'public' \
3857 regardless of OUTER access state"
3858 );
3859 }
3860
3861 #[test]
3869 fn test_legacy_double_colon_field_lookup_returns_zero() {
3870 let source = r"
3871class Foo {
3872public:
3873 int bar;
3874 static int baz;
3875 const int qux = 0;
3876};
3877struct Quux {
3878 int corge;
3879};
3880";
3881 let staging = build_cpp(source);
3882
3883 assert_has_node_with_kind_exact(&staging, "Foo.bar", NodeKind::Property);
3885 assert_has_node_with_kind_exact(&staging, "Foo.baz", NodeKind::Property);
3886 assert_has_node_with_kind_exact(&staging, "Foo.qux", NodeKind::Constant);
3887 assert_has_node_with_kind_exact(&staging, "Quux.corge", NodeKind::Property);
3888
3889 for legacy in ["Foo::bar", "Foo::baz", "Foo::qux", "Quux::corge"] {
3892 let hits: Vec<_> = staging
3893 .nodes()
3894 .filter(|n| staging.resolve_node_name(n.entry) == Some(legacy))
3895 .collect();
3896 assert!(
3897 hits.is_empty(),
3898 "legacy lookup for {legacy:?} must return 0 hits, got {} node(s) ({:?})",
3899 hits.len(),
3900 hits.iter()
3901 .map(|n| (n.entry.kind, staging.resolve_node_name(n.entry)))
3902 .collect::<Vec<_>>()
3903 );
3904 }
3905 }
3906
3907 #[test]
3910 fn test_namespaced_class_field_qualified_name() {
3911 let source = r"
3912namespace demo {
3913 class Service {
3914 public:
3915 int counter;
3916 };
3917}
3918";
3919 let staging = build_cpp(source);
3920
3921 assert_has_node_with_kind_exact(&staging, "demo::Service.counter", NodeKind::Property);
3922 }
3923}