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" | "struct_specifier" | "union_specifier"
1626 ) {
1627 continue;
1628 }
1629
1630 let is_struct_or_union = matches!(kind, "struct_specifier" | "union_specifier");
1631
1632 if let Some(name_node) = inner.child_by_field_name("name") {
1633 if let Ok(inner_name) = name_node.utf8_text(content) {
1635 let inner_name = inner_name.trim();
1636 let nested_qualified = format!("{class_qualified_name}::{inner_name}");
1637
1638 if let Some(body) = inner.child_by_field_name("body") {
1639 walk_class_body(
1640 body,
1641 content,
1642 &nested_qualified,
1643 is_struct_or_union,
1644 ast_graph,
1645 helper,
1646 seen_includes,
1647 namespace_stack,
1648 class_stack,
1649 ffi_registry,
1650 pure_virtual_registry,
1651 budget,
1652 )?;
1653 handled_nested = true;
1654 }
1655 }
1656 } else if let Some(body) = inner.child_by_field_name("body") {
1657 let mut anon_cursor = body.walk();
1662 for anon_child in body.children(&mut anon_cursor) {
1663 if anon_child.kind() == "field_declaration" {
1664 process_field_declaration(
1665 anon_child,
1666 content,
1667 class_qualified_name,
1668 current_visibility,
1669 helper,
1670 )?;
1671 }
1672 }
1673 handled_nested = true;
1674 }
1675 }
1676
1677 let _ = handled_nested;
1690 process_field_declaration(
1691 child,
1692 content,
1693 class_qualified_name,
1694 current_visibility,
1695 helper,
1696 )?;
1697 }
1698 "function_definition" => {
1699 if let Some(context) = ast_graph.context_for_start(child.start_byte()) {
1702 let span = span_from_node(child);
1703 helper.add_method_with_signature(
1704 &context.qualified_name,
1705 Some(span),
1706 false, context.is_static,
1708 Some(current_visibility),
1709 context.return_type.as_deref(),
1710 );
1711 }
1712 walk_tree_for_graph(
1714 child,
1715 content,
1716 ast_graph,
1717 helper,
1718 seen_includes,
1719 namespace_stack,
1720 class_stack,
1721 ffi_registry,
1722 pure_virtual_registry,
1723 budget,
1724 )?;
1725 }
1726 _ => {
1727 walk_tree_for_graph(
1729 child,
1730 content,
1731 ast_graph,
1732 helper,
1733 seen_includes,
1734 namespace_stack,
1735 class_stack,
1736 ffi_registry,
1737 pure_virtual_registry,
1738 budget,
1739 )?;
1740 }
1741 }
1742 }
1743
1744 Ok(())
1745}
1746
1747#[allow(clippy::too_many_arguments)]
1749#[allow(clippy::too_many_lines)] fn walk_tree_for_graph(
1751 node: Node,
1752 content: &[u8],
1753 ast_graph: &ASTGraph,
1754 helper: &mut GraphBuildHelper,
1755 seen_includes: &mut HashSet<String>,
1756 namespace_stack: &mut Vec<String>,
1757 class_stack: &mut Vec<String>,
1758 ffi_registry: &FfiRegistry,
1759 pure_virtual_registry: &PureVirtualRegistry,
1760 budget: &mut BuildBudget,
1761) -> GraphResult<()> {
1762 budget.checkpoint("cpp:walk_tree_for_graph")?;
1763 match node.kind() {
1764 "preproc_include" => {
1765 build_import_edge(node, content, helper, seen_includes)?;
1767 }
1768 "linkage_specification" => {
1769 build_ffi_block_for_staging(node, content, helper, namespace_stack);
1771 }
1772 "namespace_definition" => {
1773 if let Some(name_node) = node.child_by_field_name("name")
1775 && let Ok(ns_name) = name_node.utf8_text(content)
1776 {
1777 namespace_stack.push(ns_name.trim().to_string());
1778
1779 let mut cursor = node.walk();
1781 for child in node.children(&mut cursor) {
1782 walk_tree_for_graph(
1783 child,
1784 content,
1785 ast_graph,
1786 helper,
1787 seen_includes,
1788 namespace_stack,
1789 class_stack,
1790 ffi_registry,
1791 pure_virtual_registry,
1792 budget,
1793 )?;
1794 }
1795
1796 namespace_stack.pop();
1797 return Ok(());
1798 }
1799 }
1800 "class_specifier" | "struct_specifier" => {
1801 if let Some(name_node) = node.child_by_field_name("name")
1803 && let Ok(class_name) = name_node.utf8_text(content)
1804 {
1805 let class_name = class_name.trim();
1806 let span = span_from_node(node);
1807 let is_struct = node.kind() == "struct_specifier";
1808
1809 let qualified_class =
1811 build_qualified_name(namespace_stack, class_stack, class_name);
1812
1813 let visibility = "public";
1815 let class_id = if is_struct {
1816 helper.add_struct_with_visibility(
1817 &qualified_class,
1818 Some(span),
1819 Some(visibility),
1820 )
1821 } else {
1822 helper.add_class_with_visibility(&qualified_class, Some(span), Some(visibility))
1823 };
1824
1825 build_inheritance_and_implements_edges(
1828 node,
1829 content,
1830 &qualified_class,
1831 class_id,
1832 helper,
1833 namespace_stack,
1834 pure_virtual_registry,
1835 )?;
1836
1837 if class_stack.is_empty() {
1840 let module_id = helper.add_module(FILE_MODULE_NAME, None);
1841 helper.add_export_edge(module_id, class_id);
1842 }
1843
1844 class_stack.push(class_name.to_string());
1846
1847 if let Some(body) = node.child_by_field_name("body") {
1850 walk_class_body(
1851 body,
1852 content,
1853 &qualified_class,
1854 is_struct,
1855 ast_graph,
1856 helper,
1857 seen_includes,
1858 namespace_stack,
1859 class_stack,
1860 ffi_registry,
1861 pure_virtual_registry,
1862 budget,
1863 )?;
1864 }
1865
1866 class_stack.pop();
1867 return Ok(());
1868 }
1869 }
1870 "enum_specifier" => {
1871 if let Some(name_node) = node.child_by_field_name("name")
1872 && let Ok(enum_name) = name_node.utf8_text(content)
1873 {
1874 let enum_name = enum_name.trim();
1875 let span = span_from_node(node);
1876 let qualified_enum = build_qualified_name(namespace_stack, class_stack, enum_name);
1877 let enum_id = helper.add_enum(&qualified_enum, Some(span));
1878
1879 if class_stack.is_empty() {
1880 let module_id = helper.add_module(FILE_MODULE_NAME, None);
1881 helper.add_export_edge(module_id, enum_id);
1882 }
1883 }
1884 }
1885 "function_definition" => {
1886 if !class_stack.is_empty() {
1890 let mut cursor = node.walk();
1893 for child in node.children(&mut cursor) {
1894 walk_tree_for_graph(
1895 child,
1896 content,
1897 ast_graph,
1898 helper,
1899 seen_includes,
1900 namespace_stack,
1901 class_stack,
1902 ffi_registry,
1903 pure_virtual_registry,
1904 budget,
1905 )?;
1906 }
1907 return Ok(());
1908 }
1909
1910 if let Some(context) = ast_graph.context_for_start(node.start_byte()) {
1912 let span = span_from_node(node);
1913
1914 if context.class_stack.is_empty() {
1916 let visibility = if context.is_static {
1919 "private"
1920 } else {
1921 "public"
1922 };
1923 let fn_id = helper.add_function_with_signature(
1924 &context.qualified_name,
1925 Some(span),
1926 false, false, Some(visibility),
1929 context.return_type.as_deref(),
1930 );
1931
1932 if !context.is_static {
1934 let module_id = helper.add_module(FILE_MODULE_NAME, None);
1935 helper.add_export_edge(module_id, fn_id);
1936 }
1937 } else {
1938 helper.add_method_with_signature(
1943 &context.qualified_name,
1944 Some(span),
1945 false, context.is_static,
1947 Some("public"), context.return_type.as_deref(),
1949 );
1950 }
1951 }
1952 }
1953 "call_expression" => {
1954 if let Ok(Some((caller_qname, callee_qname, argument_count, span))) =
1956 build_call_for_staging(ast_graph, node, content)
1957 {
1958 let caller_function_id =
1960 helper.ensure_callee(&caller_qname, span, CalleeKindHint::Function);
1961 let argument_count = u8::try_from(argument_count).unwrap_or(u8::MAX);
1962
1963 let is_unqualified = !callee_qname.contains("::");
1966 if is_unqualified {
1967 if let Some((ffi_qualified, ffi_convention)) = ffi_registry.get(&callee_qname) {
1968 let ffi_target_id =
1970 helper.ensure_callee(ffi_qualified, span, CalleeKindHint::Function);
1971 helper.add_ffi_edge(caller_function_id, ffi_target_id, *ffi_convention);
1972 } else {
1973 let target_function_id =
1975 helper.ensure_callee(&callee_qname, span, CalleeKindHint::Function);
1976 helper.add_call_edge_full_with_span(
1977 caller_function_id,
1978 target_function_id,
1979 argument_count,
1980 false,
1981 vec![span],
1982 );
1983 }
1984 } else {
1985 let target_function_id =
1987 helper.ensure_callee(&callee_qname, span, CalleeKindHint::Function);
1988 helper.add_call_edge_full_with_span(
1989 caller_function_id,
1990 target_function_id,
1991 argument_count,
1992 false,
1993 vec![span],
1994 );
1995 }
1996 }
1997 }
1998 "declaration" => {
1999 if class_stack.is_empty() {
2002 process_global_variable_declaration(node, content, namespace_stack, helper)?;
2003 }
2004 }
2005 _ => {}
2006 }
2007
2008 let mut cursor = node.walk();
2010 for child in node.children(&mut cursor) {
2011 walk_tree_for_graph(
2012 child,
2013 content,
2014 ast_graph,
2015 helper,
2016 seen_includes,
2017 namespace_stack,
2018 class_stack,
2019 ffi_registry,
2020 pure_virtual_registry,
2021 budget,
2022 )?;
2023 }
2024
2025 Ok(())
2026}
2027
2028fn build_call_for_staging(
2030 ast_graph: &ASTGraph,
2031 call_node: Node<'_>,
2032 content: &[u8],
2033) -> GraphResult<Option<(String, String, usize, Span)>> {
2034 let call_context = ast_graph.find_enclosing(call_node.start_byte());
2036 let caller_qualified_name = if let Some(ctx) = call_context {
2037 ctx.qualified_name.clone()
2038 } else {
2039 return Ok(None);
2041 };
2042
2043 let Some(function_node) = call_node.child_by_field_name("function") else {
2044 return Ok(None);
2045 };
2046
2047 let callee_text = function_node
2048 .utf8_text(content)
2049 .map_err(|_| GraphBuilderError::ParseError {
2050 span: span_from_node(call_node),
2051 reason: "failed to read call expression".to_string(),
2052 })?
2053 .trim();
2054
2055 if callee_text.is_empty() {
2056 return Ok(None);
2057 }
2058
2059 let target_qualified_name = if let Some(ctx) = call_context {
2061 resolve_callee_name(callee_text, ctx, ast_graph)
2062 } else {
2063 callee_text.to_string()
2064 };
2065
2066 let span = span_from_node(call_node);
2067 let argument_count = count_arguments(call_node);
2068
2069 Ok(Some((
2070 caller_qualified_name,
2071 target_qualified_name,
2072 argument_count,
2073 span,
2074 )))
2075}
2076
2077fn build_import_edge(
2084 include_node: Node<'_>,
2085 content: &[u8],
2086 helper: &mut GraphBuildHelper,
2087 seen_includes: &mut HashSet<String>,
2088) -> GraphResult<()> {
2089 let path_node = include_node.child_by_field_name("path").or_else(|| {
2091 let mut cursor = include_node.walk();
2093 include_node.children(&mut cursor).find(|child| {
2094 matches!(
2095 child.kind(),
2096 "system_lib_string" | "string_literal" | "string_content"
2097 )
2098 })
2099 });
2100
2101 let Some(path_node) = path_node else {
2102 return Ok(());
2103 };
2104
2105 let include_path = path_node
2106 .utf8_text(content)
2107 .map_err(|_| GraphBuilderError::ParseError {
2108 span: span_from_node(include_node),
2109 reason: "failed to read include path".to_string(),
2110 })?
2111 .trim();
2112
2113 if include_path.is_empty() {
2114 return Ok(());
2115 }
2116
2117 let is_system_include = include_path.starts_with('<') && include_path.ends_with('>');
2119 let cleaned_path = if is_system_include {
2120 include_path.trim_start_matches('<').trim_end_matches('>')
2122 } else {
2123 include_path.trim_start_matches('"').trim_end_matches('"')
2125 };
2126
2127 if cleaned_path.is_empty() {
2128 return Ok(());
2129 }
2130
2131 if !seen_includes.insert(cleaned_path.to_string()) {
2133 return Ok(()); }
2135
2136 let file_module_id = helper.add_module("<file>", None);
2138
2139 let span = span_from_node(include_node);
2141 let import_id = helper.add_import(cleaned_path, Some(span));
2142
2143 helper.add_import_edge(file_module_id, import_id);
2146
2147 Ok(())
2148}
2149
2150fn collect_ffi_declarations(
2161 node: Node<'_>,
2162 content: &[u8],
2163 ffi_registry: &mut FfiRegistry,
2164 budget: &mut BuildBudget,
2165) -> GraphResult<()> {
2166 budget.checkpoint("cpp:collect_ffi_declarations")?;
2167 if node.kind() == "linkage_specification" {
2168 let abi = extract_ffi_abi(node, content);
2170 let convention = abi_to_convention(&abi);
2171
2172 if let Some(body_node) = node.child_by_field_name("body") {
2174 collect_ffi_from_body(body_node, content, &abi, convention, ffi_registry);
2175 }
2176 }
2177
2178 let mut cursor = node.walk();
2180 for child in node.children(&mut cursor) {
2181 collect_ffi_declarations(child, content, ffi_registry, budget)?;
2182 }
2183
2184 Ok(())
2185}
2186
2187fn collect_ffi_from_body(
2189 body_node: Node<'_>,
2190 content: &[u8],
2191 abi: &str,
2192 convention: FfiConvention,
2193 ffi_registry: &mut FfiRegistry,
2194) {
2195 match body_node.kind() {
2196 "declaration_list" => {
2197 let mut cursor = body_node.walk();
2199 for decl in body_node.children(&mut cursor) {
2200 if decl.kind() == "declaration"
2201 && let Some(fn_name) = extract_ffi_function_name(decl, content)
2202 {
2203 let qualified = format!("extern::{abi}::{fn_name}");
2204 ffi_registry.insert(fn_name, (qualified, convention));
2205 }
2206 }
2207 }
2208 "declaration" => {
2209 if let Some(fn_name) = extract_ffi_function_name(body_node, content) {
2211 let qualified = format!("extern::{abi}::{fn_name}");
2212 ffi_registry.insert(fn_name, (qualified, convention));
2213 }
2214 }
2215 _ => {}
2216 }
2217}
2218
2219fn extract_ffi_function_name(decl_node: Node<'_>, content: &[u8]) -> Option<String> {
2221 if let Some(declarator_node) = decl_node.child_by_field_name("declarator") {
2223 return extract_function_name_from_declarator(declarator_node, content);
2224 }
2225 None
2226}
2227
2228fn extract_function_name_from_declarator(node: Node<'_>, content: &[u8]) -> Option<String> {
2230 match node.kind() {
2231 "function_declarator" => {
2232 if let Some(inner) = node.child_by_field_name("declarator") {
2234 return extract_function_name_from_declarator(inner, content);
2235 }
2236 }
2237 "identifier" => {
2238 if let Ok(name) = node.utf8_text(content) {
2240 let name = name.trim();
2241 if !name.is_empty() {
2242 return Some(name.to_string());
2243 }
2244 }
2245 }
2246 "pointer_declarator" | "reference_declarator" => {
2247 if let Some(inner) = node.child_by_field_name("declarator") {
2249 return extract_function_name_from_declarator(inner, content);
2250 }
2251 }
2252 "parenthesized_declarator" => {
2253 let mut cursor = node.walk();
2255 for child in node.children(&mut cursor) {
2256 if let Some(name) = extract_function_name_from_declarator(child, content) {
2257 return Some(name);
2258 }
2259 }
2260 }
2261 _ => {}
2262 }
2263 None
2264}
2265
2266fn extract_ffi_abi(node: Node<'_>, content: &[u8]) -> String {
2270 if let Some(value_node) = node.child_by_field_name("value")
2272 && value_node.kind() == "string_literal"
2273 {
2274 let mut cursor = value_node.walk();
2276 for child in value_node.children(&mut cursor) {
2277 if child.kind() == "string_content"
2278 && let Ok(text) = child.utf8_text(content)
2279 {
2280 let trimmed = text.trim();
2281 if !trimmed.is_empty() {
2282 return trimmed.to_string();
2283 }
2284 }
2285 }
2286 }
2287 "C".to_string()
2289}
2290
2291fn abi_to_convention(abi: &str) -> FfiConvention {
2293 match abi.to_lowercase().as_str() {
2294 "system" => FfiConvention::System,
2295 "stdcall" => FfiConvention::Stdcall,
2296 "fastcall" => FfiConvention::Fastcall,
2297 "cdecl" => FfiConvention::Cdecl,
2298 _ => FfiConvention::C, }
2300}
2301
2302fn build_ffi_block_for_staging(
2306 node: Node<'_>,
2307 content: &[u8],
2308 helper: &mut GraphBuildHelper,
2309 namespace_stack: &[String],
2310) {
2311 let abi = extract_ffi_abi(node, content);
2313
2314 if let Some(body_node) = node.child_by_field_name("body") {
2316 build_ffi_from_body(body_node, content, &abi, helper, namespace_stack);
2317 }
2318}
2319
2320fn build_ffi_from_body(
2322 body_node: Node<'_>,
2323 content: &[u8],
2324 abi: &str,
2325 helper: &mut GraphBuildHelper,
2326 namespace_stack: &[String],
2327) {
2328 match body_node.kind() {
2329 "declaration_list" => {
2330 let mut cursor = body_node.walk();
2332 for decl in body_node.children(&mut cursor) {
2333 if decl.kind() == "declaration"
2334 && let Some(fn_name) = extract_ffi_function_name(decl, content)
2335 {
2336 let span = span_from_node(decl);
2337 let qualified = if namespace_stack.is_empty() {
2339 format!("extern::{abi}::{fn_name}")
2340 } else {
2341 format!("{}::extern::{abi}::{fn_name}", namespace_stack.join("::"))
2342 };
2343 helper.add_function(
2345 &qualified,
2346 Some(span),
2347 false, true, );
2350 }
2351 }
2352 }
2353 "declaration" => {
2354 if let Some(fn_name) = extract_ffi_function_name(body_node, content) {
2356 let span = span_from_node(body_node);
2357 let qualified = if namespace_stack.is_empty() {
2358 format!("extern::{abi}::{fn_name}")
2359 } else {
2360 format!("{}::extern::{abi}::{fn_name}", namespace_stack.join("::"))
2361 };
2362 helper.add_function(&qualified, Some(span), false, true);
2363 }
2364 }
2365 _ => {}
2366 }
2367}
2368
2369fn collect_pure_virtual_interfaces(
2379 node: Node<'_>,
2380 content: &[u8],
2381 registry: &mut PureVirtualRegistry,
2382 budget: &mut BuildBudget,
2383) -> GraphResult<()> {
2384 budget.checkpoint("cpp:collect_pure_virtual_interfaces")?;
2385 if matches!(node.kind(), "class_specifier" | "struct_specifier")
2386 && let Some(name_node) = node.child_by_field_name("name")
2387 && let Ok(class_name) = name_node.utf8_text(content)
2388 {
2389 let class_name = class_name.trim();
2390 if !class_name.is_empty() && has_pure_virtual_methods(node, content) {
2391 registry.insert(class_name.to_string());
2392 }
2393 }
2394
2395 let mut cursor = node.walk();
2397 for child in node.children(&mut cursor) {
2398 collect_pure_virtual_interfaces(child, content, registry, budget)?;
2399 }
2400
2401 Ok(())
2402}
2403
2404fn has_pure_virtual_methods(class_node: Node<'_>, content: &[u8]) -> bool {
2408 if let Some(body) = class_node.child_by_field_name("body") {
2409 let mut cursor = body.walk();
2410 for child in body.children(&mut cursor) {
2411 if child.kind() == "field_declaration" && is_pure_virtual_declaration(child, content) {
2413 return true;
2414 }
2415 }
2416 }
2417 false
2418}
2419
2420fn is_pure_virtual_declaration(decl_node: Node<'_>, content: &[u8]) -> bool {
2422 let mut has_virtual = false;
2423 let mut has_pure_specifier = false;
2424
2425 let mut cursor = decl_node.walk();
2427 for child in decl_node.children(&mut cursor) {
2428 match child.kind() {
2429 "virtual" => {
2430 has_virtual = true;
2431 }
2432 "number_literal" => {
2433 if let Ok(text) = child.utf8_text(content)
2436 && text.trim() == "0"
2437 {
2438 has_pure_specifier = true;
2439 }
2440 }
2441 _ => {}
2442 }
2443 }
2444
2445 has_virtual && has_pure_specifier
2446}
2447
2448fn build_inheritance_and_implements_edges(
2454 class_node: Node<'_>,
2455 content: &[u8],
2456 _qualified_class_name: &str,
2457 child_id: sqry_core::graph::unified::node::NodeId,
2458 helper: &mut GraphBuildHelper,
2459 namespace_stack: &[String],
2460 pure_virtual_registry: &PureVirtualRegistry,
2461) -> GraphResult<()> {
2462 let mut cursor = class_node.walk();
2464 let base_clause = class_node
2465 .children(&mut cursor)
2466 .find(|child| child.kind() == "base_class_clause");
2467
2468 let Some(base_clause) = base_clause else {
2469 return Ok(()); };
2471
2472 let mut clause_cursor = base_clause.walk();
2474 for child in base_clause.children(&mut clause_cursor) {
2475 match child.kind() {
2476 "type_identifier" => {
2477 let base_name = child
2478 .utf8_text(content)
2479 .map_err(|_| GraphBuilderError::ParseError {
2480 span: span_from_node(child),
2481 reason: "failed to read base class name".to_string(),
2482 })?
2483 .trim();
2484
2485 if !base_name.is_empty() {
2486 let qualified_base = if namespace_stack.is_empty() {
2488 base_name.to_string()
2489 } else {
2490 format!("{}::{}", namespace_stack.join("::"), base_name)
2491 };
2492
2493 if pure_virtual_registry.contains(base_name) {
2495 let interface_id = helper.add_interface(&qualified_base, None);
2497 helper.add_implements_edge(child_id, interface_id);
2498 } else {
2499 let parent_id = helper.add_class(&qualified_base, None);
2501 helper.add_inherits_edge(child_id, parent_id);
2502 }
2503 }
2504 }
2505 "qualified_identifier" => {
2506 let base_name = child
2508 .utf8_text(content)
2509 .map_err(|_| GraphBuilderError::ParseError {
2510 span: span_from_node(child),
2511 reason: "failed to read base class name".to_string(),
2512 })?
2513 .trim();
2514
2515 if !base_name.is_empty() {
2516 let simple_name = base_name.rsplit("::").next().unwrap_or(base_name);
2518
2519 if pure_virtual_registry.contains(simple_name) {
2520 let interface_id = helper.add_interface(base_name, None);
2521 helper.add_implements_edge(child_id, interface_id);
2522 } else {
2523 let parent_id = helper.add_class(base_name, None);
2524 helper.add_inherits_edge(child_id, parent_id);
2525 }
2526 }
2527 }
2528 "template_type" => {
2529 if let Some(template_name_node) = child.child_by_field_name("name")
2531 && let Ok(base_name) = template_name_node.utf8_text(content)
2532 {
2533 let base_name = base_name.trim();
2534 if !base_name.is_empty() {
2535 let qualified_base =
2536 if base_name.contains("::") || namespace_stack.is_empty() {
2537 base_name.to_string()
2538 } else {
2539 format!("{}::{}", namespace_stack.join("::"), base_name)
2540 };
2541
2542 if pure_virtual_registry.contains(base_name) {
2545 let interface_id = helper.add_interface(&qualified_base, None);
2546 helper.add_implements_edge(child_id, interface_id);
2547 } else {
2548 let parent_id = helper.add_class(&qualified_base, None);
2549 helper.add_inherits_edge(child_id, parent_id);
2550 }
2551 }
2552 }
2553 }
2554 _ => {
2555 }
2557 }
2558 }
2559
2560 Ok(())
2561}
2562
2563fn span_from_node(node: Node<'_>) -> Span {
2564 let start = node.start_position();
2565 let end = node.end_position();
2566 Span::new(
2567 sqry_core::graph::node::Position::new(start.row, start.column),
2568 sqry_core::graph::node::Position::new(end.row, end.column),
2569 )
2570}
2571
2572fn count_arguments(node: Node<'_>) -> usize {
2573 node.child_by_field_name("arguments").map_or(0, |args| {
2574 let mut count = 0;
2575 let mut cursor = args.walk();
2576 for child in args.children(&mut cursor) {
2577 if !matches!(child.kind(), "(" | ")" | ",") {
2578 count += 1;
2579 }
2580 }
2581 count
2582 })
2583}
2584
2585#[cfg(test)]
2586mod tests {
2587 use super::*;
2588 use sqry_core::graph::unified::build::test_helpers::{
2589 assert_has_node, assert_has_node_with_kind, assert_has_node_with_kind_exact,
2590 collect_call_edges,
2591 };
2592 use sqry_core::graph::unified::node::NodeKind;
2593 use tree_sitter::Parser;
2594
2595 fn parse_cpp(source: &str) -> Tree {
2596 let mut parser = Parser::new();
2597 parser
2598 .set_language(&tree_sitter_cpp::LANGUAGE.into())
2599 .expect("Failed to set Cpp language");
2600 parser
2601 .parse(source.as_bytes(), None)
2602 .expect("Failed to parse Cpp source")
2603 }
2604
2605 fn test_budget() -> BuildBudget {
2606 BuildBudget::new(Path::new("test.cpp"))
2607 }
2608
2609 fn extract_namespace_map_for_test(
2610 tree: &Tree,
2611 source: &str,
2612 ) -> HashMap<std::ops::Range<usize>, String> {
2613 let mut budget = test_budget();
2614 extract_namespace_map(tree.root_node(), source.as_bytes(), &mut budget)
2615 .expect("namespace extraction should succeed in tests")
2616 }
2617
2618 fn extract_cpp_contexts_for_test(
2619 tree: &Tree,
2620 source: &str,
2621 namespace_map: &HashMap<std::ops::Range<usize>, String>,
2622 ) -> Vec<FunctionContext> {
2623 let mut budget = test_budget();
2624 extract_cpp_contexts(
2625 tree.root_node(),
2626 source.as_bytes(),
2627 namespace_map,
2628 &mut budget,
2629 )
2630 .expect("context extraction should succeed in tests")
2631 }
2632
2633 fn extract_field_and_type_info_for_test(
2634 tree: &Tree,
2635 source: &str,
2636 namespace_map: &HashMap<std::ops::Range<usize>, String>,
2637 ) -> (QualifiedNameMap, QualifiedNameMap) {
2638 let mut budget = test_budget();
2639 extract_field_and_type_info(
2640 tree.root_node(),
2641 source.as_bytes(),
2642 namespace_map,
2643 &mut budget,
2644 )
2645 .expect("field/type extraction should succeed in tests")
2646 }
2647
2648 #[test]
2649 fn test_build_graph_times_out_with_expired_budget() {
2650 let source = r"
2651 namespace demo {
2652 class Service {
2653 public:
2654 void process() {}
2655 };
2656 }
2657 ";
2658 let tree = parse_cpp(source);
2659 let builder = CppGraphBuilder::new();
2660 let mut staging = StagingGraph::new();
2661 let mut budget = BuildBudget::already_expired(Path::new("timeout.cpp"));
2662
2663 let err = builder
2664 .build_graph_with_budget(
2665 &tree,
2666 source.as_bytes(),
2667 Path::new("timeout.cpp"),
2668 &mut staging,
2669 &mut budget,
2670 )
2671 .expect_err("expired budget should force timeout");
2672
2673 match err {
2674 GraphBuilderError::BuildTimedOut {
2675 file,
2676 phase,
2677 timeout_ms,
2678 } => {
2679 assert_eq!(file, PathBuf::from("timeout.cpp"));
2680 assert_eq!(phase, "cpp:extract_namespace_map");
2681 assert_eq!(timeout_ms, 1_000);
2682 }
2683 other => panic!("expected BuildTimedOut, got {other:?}"),
2684 }
2685 }
2686
2687 #[test]
2688 fn test_extract_class() {
2689 let source = "class User { }";
2690 let tree = parse_cpp(source);
2691 let mut staging = StagingGraph::new();
2692 let builder = CppGraphBuilder::new();
2693
2694 let result = builder.build_graph(
2695 &tree,
2696 source.as_bytes(),
2697 Path::new("test.cpp"),
2698 &mut staging,
2699 );
2700
2701 assert!(result.is_ok());
2702 assert_has_node_with_kind(&staging, "User", NodeKind::Class);
2703 }
2704
2705 #[test]
2706 fn test_extract_template_class() {
2707 let source = r"
2708 template <typename T>
2709 class Person {
2710 public:
2711 T name;
2712 T age;
2713 };
2714 ";
2715 let tree = parse_cpp(source);
2716 let mut staging = StagingGraph::new();
2717 let builder = CppGraphBuilder::new();
2718
2719 let result = builder.build_graph(
2720 &tree,
2721 source.as_bytes(),
2722 Path::new("test.cpp"),
2723 &mut staging,
2724 );
2725
2726 assert!(result.is_ok());
2727 assert_has_node_with_kind(&staging, "Person", NodeKind::Class);
2728 }
2729
2730 #[test]
2731 fn test_extract_function() {
2732 let source = r#"
2733 #include <cstdio>
2734 void hello() {
2735 std::printf("Hello");
2736 }
2737 "#;
2738 let tree = parse_cpp(source);
2739 let mut staging = StagingGraph::new();
2740 let builder = CppGraphBuilder::new();
2741
2742 let result = builder.build_graph(
2743 &tree,
2744 source.as_bytes(),
2745 Path::new("test.cpp"),
2746 &mut staging,
2747 );
2748
2749 assert!(result.is_ok());
2750 assert_has_node_with_kind(&staging, "hello", NodeKind::Function);
2751 }
2752
2753 #[test]
2754 fn test_extract_virtual_function() {
2755 let source = r"
2756 class Service {
2757 public:
2758 virtual void fetchData() {}
2759 };
2760 ";
2761 let tree = parse_cpp(source);
2762 let mut staging = StagingGraph::new();
2763 let builder = CppGraphBuilder::new();
2764
2765 let result = builder.build_graph(
2766 &tree,
2767 source.as_bytes(),
2768 Path::new("test.cpp"),
2769 &mut staging,
2770 );
2771
2772 assert!(result.is_ok());
2773 assert_has_node(&staging, "fetchData");
2774 }
2775
2776 #[test]
2777 fn test_extract_call_edge() {
2778 let source = r"
2779 void greet() {}
2780
2781 int main() {
2782 greet();
2783 return 0;
2784 }
2785 ";
2786 let tree = parse_cpp(source);
2787 let mut staging = StagingGraph::new();
2788 let builder = CppGraphBuilder::new();
2789
2790 let result = builder.build_graph(
2791 &tree,
2792 source.as_bytes(),
2793 Path::new("test.cpp"),
2794 &mut staging,
2795 );
2796
2797 assert!(result.is_ok());
2798 assert_has_node(&staging, "main");
2799 assert_has_node(&staging, "greet");
2800 let calls = collect_call_edges(&staging);
2801 assert!(!calls.is_empty());
2802 }
2803
2804 #[test]
2805 fn test_extract_member_call_edge() {
2806 let source = r"
2807 class Service {
2808 public:
2809 void helper() {}
2810 };
2811
2812 int main() {
2813 Service svc;
2814 svc.helper();
2815 return 0;
2816 }
2817 ";
2818 let tree = parse_cpp(source);
2819 let mut staging = StagingGraph::new();
2820 let builder = CppGraphBuilder::new();
2821
2822 let result = builder.build_graph(
2823 &tree,
2824 source.as_bytes(),
2825 Path::new("member.cpp"),
2826 &mut staging,
2827 );
2828
2829 assert!(result.is_ok());
2830 assert_has_node(&staging, "main");
2831 assert_has_node(&staging, "helper");
2832 let calls = collect_call_edges(&staging);
2833 assert!(!calls.is_empty());
2834 }
2835
2836 #[test]
2837 fn test_extract_namespace_map_simple() {
2838 let source = r"
2839 namespace demo {
2840 void func() {}
2841 }
2842 ";
2843 let tree = parse_cpp(source);
2844 let namespace_map = extract_namespace_map_for_test(&tree, source);
2845
2846 assert_eq!(namespace_map.len(), 1);
2848
2849 let (_, ns_prefix) = namespace_map.iter().next().unwrap();
2851 assert_eq!(ns_prefix, "demo::");
2852 }
2853
2854 #[test]
2855 fn test_extract_namespace_map_nested() {
2856 let source = r"
2857 namespace outer {
2858 namespace inner {
2859 void func() {}
2860 }
2861 }
2862 ";
2863 let tree = parse_cpp(source);
2864 let namespace_map = extract_namespace_map_for_test(&tree, source);
2865
2866 assert!(namespace_map.len() >= 2);
2868
2869 let ns_values: Vec<&String> = namespace_map.values().collect();
2871 assert!(ns_values.iter().any(|v| v.as_str() == "outer::"));
2872 assert!(ns_values.iter().any(|v| v.as_str() == "outer::inner::"));
2873 }
2874
2875 #[test]
2876 fn test_extract_namespace_map_multiple() {
2877 let source = r"
2878 namespace first {
2879 void func1() {}
2880 }
2881 namespace second {
2882 void func2() {}
2883 }
2884 ";
2885 let tree = parse_cpp(source);
2886 let namespace_map = extract_namespace_map_for_test(&tree, source);
2887
2888 assert_eq!(namespace_map.len(), 2);
2890
2891 let ns_values: Vec<&String> = namespace_map.values().collect();
2892 assert!(ns_values.iter().any(|v| v.as_str() == "first::"));
2893 assert!(ns_values.iter().any(|v| v.as_str() == "second::"));
2894 }
2895
2896 #[test]
2897 fn test_find_namespace_for_offset() {
2898 let source = r"
2899 namespace demo {
2900 void func() {}
2901 }
2902 ";
2903 let tree = parse_cpp(source);
2904 let namespace_map = extract_namespace_map_for_test(&tree, source);
2905
2906 let func_offset = source.find("func").unwrap();
2908 let ns = find_namespace_for_offset(func_offset, &namespace_map);
2909 assert_eq!(ns, "demo::");
2910
2911 let ns = find_namespace_for_offset(0, &namespace_map);
2913 assert_eq!(ns, "");
2914 }
2915
2916 #[test]
2917 fn test_extract_cpp_contexts_free_function() {
2918 let source = r"
2919 void helper() {}
2920 ";
2921 let tree = parse_cpp(source);
2922 let namespace_map = extract_namespace_map_for_test(&tree, source);
2923 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
2924
2925 assert_eq!(contexts.len(), 1);
2926 assert_eq!(contexts[0].qualified_name, "helper");
2927 assert!(!contexts[0].is_static);
2928 assert!(!contexts[0].is_virtual);
2929 }
2930
2931 #[test]
2932 fn test_extract_cpp_contexts_namespace_function() {
2933 let source = r"
2934 namespace demo {
2935 void helper() {}
2936 }
2937 ";
2938 let tree = parse_cpp(source);
2939 let namespace_map = extract_namespace_map_for_test(&tree, source);
2940 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
2941
2942 assert_eq!(contexts.len(), 1);
2943 assert_eq!(contexts[0].qualified_name, "demo::helper");
2944 assert_eq!(contexts[0].namespace_stack, vec!["demo"]);
2945 }
2946
2947 #[test]
2948 fn test_extract_cpp_contexts_class_method() {
2949 let source = r"
2950 class Service {
2951 public:
2952 void process() {}
2953 };
2954 ";
2955 let tree = parse_cpp(source);
2956 let namespace_map = extract_namespace_map_for_test(&tree, source);
2957 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
2958
2959 assert_eq!(contexts.len(), 1);
2960 assert_eq!(contexts[0].qualified_name, "Service::process");
2961 assert_eq!(contexts[0].class_stack, vec!["Service"]);
2962 }
2963
2964 #[test]
2965 fn test_extract_cpp_contexts_namespace_and_class() {
2966 let source = r"
2967 namespace demo {
2968 class Service {
2969 public:
2970 void process() {}
2971 };
2972 }
2973 ";
2974 let tree = parse_cpp(source);
2975 let namespace_map = extract_namespace_map_for_test(&tree, source);
2976 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
2977
2978 assert_eq!(contexts.len(), 1);
2979 assert_eq!(contexts[0].qualified_name, "demo::Service::process");
2980 assert_eq!(contexts[0].namespace_stack, vec!["demo"]);
2981 assert_eq!(contexts[0].class_stack, vec!["Service"]);
2982 }
2983
2984 #[test]
2985 fn test_extract_cpp_contexts_static_method() {
2986 let source = r"
2987 class Repository {
2988 public:
2989 static void save() {}
2990 };
2991 ";
2992 let tree = parse_cpp(source);
2993 let namespace_map = extract_namespace_map_for_test(&tree, source);
2994 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
2995
2996 assert_eq!(contexts.len(), 1);
2997 assert_eq!(contexts[0].qualified_name, "Repository::save");
2998 assert!(contexts[0].is_static);
2999 }
3000
3001 #[test]
3002 fn test_extract_cpp_contexts_virtual_method() {
3003 let source = r"
3004 class Base {
3005 public:
3006 virtual void render() {}
3007 };
3008 ";
3009 let tree = parse_cpp(source);
3010 let namespace_map = extract_namespace_map_for_test(&tree, source);
3011 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3012
3013 assert_eq!(contexts.len(), 1);
3014 assert_eq!(contexts[0].qualified_name, "Base::render");
3015 assert!(contexts[0].is_virtual);
3016 }
3017
3018 #[test]
3019 fn test_extract_cpp_contexts_inline_function() {
3020 let source = r"
3021 inline void helper() {}
3022 ";
3023 let tree = parse_cpp(source);
3024 let namespace_map = extract_namespace_map_for_test(&tree, source);
3025 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3026
3027 assert_eq!(contexts.len(), 1);
3028 assert_eq!(contexts[0].qualified_name, "helper");
3029 assert!(contexts[0].is_inline);
3030 }
3031
3032 #[test]
3033 fn test_extract_cpp_contexts_out_of_line_definition() {
3034 let source = r"
3035 namespace demo {
3036 class Service {
3037 public:
3038 int process(int v);
3039 };
3040
3041 inline int Service::process(int v) {
3042 return v;
3043 }
3044 }
3045 ";
3046 let tree = parse_cpp(source);
3047 let namespace_map = extract_namespace_map_for_test(&tree, source);
3048 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3049
3050 assert_eq!(contexts.len(), 1);
3052 assert_eq!(contexts[0].qualified_name, "demo::Service::process");
3053 assert!(contexts[0].is_inline);
3054 }
3055
3056 #[test]
3057 fn test_extract_field_types_simple() {
3058 let source = r"
3059 class Service {
3060 public:
3061 Repository repo;
3062 };
3063 ";
3064 let tree = parse_cpp(source);
3065 let namespace_map = extract_namespace_map_for_test(&tree, source);
3066 let (field_types, _type_map) =
3067 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3068
3069 assert_eq!(field_types.len(), 1);
3071 assert_eq!(
3072 field_types.get(&("Service".to_string(), "repo".to_string())),
3073 Some(&"Repository".to_string())
3074 );
3075 }
3076
3077 #[test]
3078 fn test_extract_field_types_namespace() {
3079 let source = r"
3080 namespace demo {
3081 class Service {
3082 public:
3083 Repository repo;
3084 };
3085 }
3086 ";
3087 let tree = parse_cpp(source);
3088 let namespace_map = extract_namespace_map_for_test(&tree, source);
3089 let (field_types, _type_map) =
3090 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3091
3092 assert_eq!(field_types.len(), 1);
3094 assert_eq!(
3095 field_types.get(&("demo::Service".to_string(), "repo".to_string())),
3096 Some(&"Repository".to_string())
3097 );
3098 }
3099
3100 #[test]
3101 fn test_extract_field_types_no_collision() {
3102 let source = r"
3103 class ServiceA {
3104 public:
3105 Repository repo;
3106 };
3107
3108 class ServiceB {
3109 public:
3110 Repository repo;
3111 };
3112 ";
3113 let tree = parse_cpp(source);
3114 let namespace_map = extract_namespace_map_for_test(&tree, source);
3115 let (field_types, _type_map) =
3116 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3117
3118 assert_eq!(field_types.len(), 2);
3120 assert_eq!(
3121 field_types.get(&("ServiceA".to_string(), "repo".to_string())),
3122 Some(&"Repository".to_string())
3123 );
3124 assert_eq!(
3125 field_types.get(&("ServiceB".to_string(), "repo".to_string())),
3126 Some(&"Repository".to_string())
3127 );
3128 }
3129
3130 #[test]
3131 fn test_extract_using_declaration() {
3132 let source = r"
3133 using std::vector;
3134
3135 class Service {
3136 public:
3137 vector data;
3138 };
3139 ";
3140 let tree = parse_cpp(source);
3141 let namespace_map = extract_namespace_map_for_test(&tree, source);
3142 let (field_types, type_map) =
3143 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3144
3145 assert_eq!(field_types.len(), 1);
3147 assert_eq!(
3148 field_types.get(&("Service".to_string(), "data".to_string())),
3149 Some(&"std::vector".to_string()),
3150 "Field type should resolve 'vector' to 'std::vector' via using declaration"
3151 );
3152
3153 assert_eq!(
3155 type_map.get(&(String::new(), "vector".to_string())),
3156 Some(&"std::vector".to_string()),
3157 "Using declaration should map 'vector' to 'std::vector' in type_map"
3158 );
3159 }
3160
3161 #[test]
3162 fn test_extract_field_types_pointer() {
3163 let source = r"
3164 class Service {
3165 public:
3166 Repository* repo;
3167 };
3168 ";
3169 let tree = parse_cpp(source);
3170 let namespace_map = extract_namespace_map_for_test(&tree, source);
3171 let (field_types, _type_map) =
3172 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3173
3174 assert_eq!(field_types.len(), 1);
3176 assert_eq!(
3177 field_types.get(&("Service".to_string(), "repo".to_string())),
3178 Some(&"Repository".to_string())
3179 );
3180 }
3181
3182 #[test]
3183 fn test_extract_field_types_multiple_declarators() {
3184 let source = r"
3185 class Service {
3186 public:
3187 Repository repo_a, repo_b, repo_c;
3188 };
3189 ";
3190 let tree = parse_cpp(source);
3191 let namespace_map = extract_namespace_map_for_test(&tree, source);
3192 let (field_types, _type_map) =
3193 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3194
3195 assert_eq!(field_types.len(), 3);
3197 assert_eq!(
3198 field_types.get(&("Service".to_string(), "repo_a".to_string())),
3199 Some(&"Repository".to_string())
3200 );
3201 assert_eq!(
3202 field_types.get(&("Service".to_string(), "repo_b".to_string())),
3203 Some(&"Repository".to_string())
3204 );
3205 assert_eq!(
3206 field_types.get(&("Service".to_string(), "repo_c".to_string())),
3207 Some(&"Repository".to_string())
3208 );
3209 }
3210
3211 #[test]
3212 fn test_extract_field_types_nested_struct_with_parent_field() {
3213 let source = r"
3216 namespace demo {
3217 struct Outer {
3218 int outer_field;
3219 struct Inner {
3220 int inner_field;
3221 };
3222 Inner nested_instance;
3223 };
3224 }
3225 ";
3226 let tree = parse_cpp(source);
3227 let namespace_map = extract_namespace_map_for_test(&tree, source);
3228 let (field_types, _type_map) =
3229 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3230
3231 assert!(
3234 field_types.len() >= 2,
3235 "Expected at least outer_field and nested_instance"
3236 );
3237
3238 assert_eq!(
3240 field_types.get(&("demo::Outer".to_string(), "outer_field".to_string())),
3241 Some(&"int".to_string())
3242 );
3243
3244 assert_eq!(
3246 field_types.get(&("demo::Outer".to_string(), "nested_instance".to_string())),
3247 Some(&"Inner".to_string())
3248 );
3249
3250 if field_types.contains_key(&("demo::Outer::Inner".to_string(), "inner_field".to_string()))
3252 {
3253 assert_eq!(
3255 field_types.get(&("demo::Outer::Inner".to_string(), "inner_field".to_string())),
3256 Some(&"int".to_string()),
3257 "Inner class fields must use parent-qualified FQN 'demo::Outer::Inner'"
3258 );
3259 }
3260 }
3261
3262 use sqry_core::graph::unified::build::staging::StagingOp;
3279 use sqry_core::graph::unified::edge::kind::{EdgeKind, TypeOfContext};
3280
3281 fn cpp_find_added_node<'a>(
3283 staging: &'a StagingGraph,
3284 canonical_name: &str,
3285 ) -> Option<&'a sqry_core::graph::unified::storage::arena::NodeEntry> {
3286 staging.operations().iter().find_map(|op| {
3287 if let StagingOp::AddNode { entry, .. } = op
3288 && staging.resolve_node_canonical_name(entry) == Some(canonical_name)
3289 {
3290 Some(entry)
3291 } else {
3292 None
3293 }
3294 })
3295 }
3296
3297 fn cpp_find_added_node_id(
3299 staging: &StagingGraph,
3300 canonical_name: &str,
3301 kind: NodeKind,
3302 ) -> Option<sqry_core::graph::unified::NodeId> {
3303 staging.operations().iter().find_map(|op| match op {
3304 StagingOp::AddNode {
3305 entry,
3306 expected_id: Some(id),
3307 } if entry.kind == kind
3308 && staging.resolve_node_canonical_name(entry) == Some(canonical_name) =>
3309 {
3310 Some(*id)
3311 }
3312 _ => None,
3313 })
3314 }
3315
3316 fn build_cpp(source: &str) -> StagingGraph {
3318 let tree = parse_cpp(source);
3319 let mut staging = StagingGraph::new();
3320 let builder = CppGraphBuilder::new();
3321 builder
3322 .build_graph(
3323 &tree,
3324 source.as_bytes(),
3325 Path::new("test.cpp"),
3326 &mut staging,
3327 )
3328 .expect("build_graph must succeed for the test fixture");
3329 staging
3330 }
3331
3332 #[test]
3338 fn test_struct_field_emits_property_with_field_context() {
3339 let source = "struct Point { int x; int y; };";
3340 let staging = build_cpp(source);
3341
3342 assert_has_node_with_kind_exact(&staging, "Point.x", NodeKind::Property);
3344 assert_has_node_with_kind_exact(&staging, "Point.y", NodeKind::Property);
3345
3346 let entry =
3347 cpp_find_added_node(&staging, "Point.x").expect("Point.x should be staged as a node");
3348 assert_eq!(entry.kind, NodeKind::Property, "x must be Property");
3349 assert!(!entry.is_static, "instance field is_static must be false");
3350 let vis = staging.resolve_local_string(entry.visibility.expect("visibility id"));
3351 assert_eq!(
3352 vis,
3353 Some("public"),
3354 "struct default visibility must be 'public'"
3355 );
3356 assert!(entry.end_line > 0, "field end_line must be set (got 0)");
3362 assert!(
3363 entry.end_line > entry.start_line
3364 || (entry.end_line == entry.start_line && entry.end_column > entry.start_column),
3365 "field span must be non-empty: [{}:{}..{}:{}]",
3366 entry.start_line,
3367 entry.start_column,
3368 entry.end_line,
3369 entry.end_column,
3370 );
3371
3372 let x_id = cpp_find_added_node_id(&staging, "Point.x", NodeKind::Property)
3374 .expect("Point.x Property NodeId");
3375 let edge = staging.operations().iter().find_map(|op| {
3376 if let StagingOp::AddEdge {
3377 source: src,
3378 kind: EdgeKind::TypeOf { context, name, .. },
3379 ..
3380 } = op
3381 && *src == x_id
3382 {
3383 Some((*context, *name))
3384 } else {
3385 None
3386 }
3387 });
3388 let (ctx, name) = edge.expect("TypeOf edge from Point.x should be staged");
3389 assert_eq!(
3390 ctx,
3391 Some(TypeOfContext::Field),
3392 "TypeOf edge context must be Field"
3393 );
3394 let resolved_name = name.and_then(|sid| staging.resolve_local_string(sid));
3395 assert_eq!(
3396 resolved_name,
3397 Some("x"),
3398 "TypeOf edge name must be the bare field name 'x'"
3399 );
3400
3401 let stale_variable = staging.nodes().any(|n| {
3403 n.entry.kind == NodeKind::Variable
3404 && matches!(
3405 staging.resolve_node_name(n.entry),
3406 Some("Point.x" | "Point.y" | "Point::x" | "Point::y")
3407 )
3408 });
3409 assert!(
3410 !stale_variable,
3411 "Point fields must not be emitted as NodeKind::Variable"
3412 );
3413 }
3414
3415 #[test]
3417 fn test_class_field_default_visibility_is_private() {
3418 let source = "class Foo { int hidden; };";
3419 let staging = build_cpp(source);
3420
3421 let entry = cpp_find_added_node(&staging, "Foo.hidden")
3422 .expect("Foo.hidden should be staged as a node");
3423 assert_eq!(entry.kind, NodeKind::Property);
3424 let vis = staging.resolve_local_string(entry.visibility.expect("visibility id"));
3425 assert_eq!(
3426 vis,
3427 Some("private"),
3428 "class default visibility must be 'private'"
3429 );
3430 }
3431
3432 #[test]
3434 fn test_class_field_respects_explicit_access_specifier() {
3435 let source = "class Foo { public: int public_field; protected: int prot_field; };";
3436 let staging = build_cpp(source);
3437
3438 let pub_entry = cpp_find_added_node(&staging, "Foo.public_field")
3439 .expect("Foo.public_field should be staged");
3440 assert_eq!(
3441 staging.resolve_local_string(pub_entry.visibility.expect("vis")),
3442 Some("public")
3443 );
3444
3445 let prot_entry = cpp_find_added_node(&staging, "Foo.prot_field")
3446 .expect("Foo.prot_field should be staged");
3447 assert_eq!(
3448 staging.resolve_local_string(prot_entry.visibility.expect("vis")),
3449 Some("protected")
3450 );
3451 }
3452
3453 #[test]
3456 fn test_const_field_emits_constant() {
3457 let source = "class Foo { const int kMax = 0; };";
3458 let staging = build_cpp(source);
3459
3460 assert_has_node_with_kind_exact(&staging, "Foo.kMax", NodeKind::Constant);
3461 let entry = cpp_find_added_node(&staging, "Foo.kMax").expect("Foo.kMax");
3462 assert_eq!(entry.kind, NodeKind::Constant);
3463 assert!(
3464 !entry.is_static,
3465 "const (non-static) field is_static must be false; only `static` keyword sets is_static"
3466 );
3467 }
3468
3469 #[test]
3473 fn test_constexpr_field_emits_constant() {
3474 let source = "class Foo { constexpr static int kAnswer = 42; };";
3475 let staging = build_cpp(source);
3476
3477 assert_has_node_with_kind_exact(&staging, "Foo.kAnswer", NodeKind::Constant);
3478 let entry = cpp_find_added_node(&staging, "Foo.kAnswer").expect("Foo.kAnswer");
3479 assert_eq!(entry.kind, NodeKind::Constant);
3480 assert!(
3481 entry.is_static,
3482 "static constexpr member must have is_static = true"
3483 );
3484 }
3485
3486 #[test]
3489 fn test_static_field_sets_is_static_true() {
3490 let source = "class Foo { static int counter; };";
3491 let staging = build_cpp(source);
3492
3493 let entry = cpp_find_added_node(&staging, "Foo.counter").expect("Foo.counter");
3494 assert_eq!(entry.kind, NodeKind::Property);
3495 assert!(entry.is_static, "static keyword must set is_static = true");
3496 }
3497
3498 #[test]
3501 fn test_bitfield_emits_property() {
3502 let source = "struct Flags { unsigned int low : 4; unsigned int high : 4; };";
3503 let staging = build_cpp(source);
3504
3505 assert_has_node_with_kind_exact(&staging, "Flags.low", NodeKind::Property);
3506 assert_has_node_with_kind_exact(&staging, "Flags.high", NodeKind::Property);
3507 }
3508
3509 #[test]
3515 fn test_anonymous_union_member_fields_emit_property() {
3516 let source = r"
3517class Variant {
3518public:
3519 int tag;
3520 union {
3521 int as_int;
3522 float as_float;
3523 };
3524};
3525";
3526 let staging = build_cpp(source);
3527
3528 assert_has_node_with_kind_exact(&staging, "Variant.tag", NodeKind::Property);
3530
3531 assert_has_node_with_kind_exact(&staging, "Variant.as_int", NodeKind::Property);
3534 assert_has_node_with_kind_exact(&staging, "Variant.as_float", NodeKind::Property);
3535
3536 let as_int = cpp_find_added_node(&staging, "Variant.as_int")
3539 .expect("Variant.as_int should be staged");
3540 let vis = staging.resolve_local_string(as_int.visibility.expect("visibility id"));
3541 assert_eq!(
3542 vis,
3543 Some("public"),
3544 "anonymous-union members must inherit OUTER access (`public:` here)"
3545 );
3546
3547 let bogus = staging.nodes().any(|n| {
3550 staging
3551 .resolve_node_name(n.entry)
3552 .is_some_and(|name| name.contains("::.") || name.starts_with("Variant::."))
3553 });
3554 assert!(
3555 !bogus,
3556 "anonymous union must not produce a synthetic qualifier"
3557 );
3558
3559 let stale_variable = staging.nodes().any(|n| {
3561 n.entry.kind == NodeKind::Variable
3562 && matches!(
3563 staging.resolve_node_name(n.entry),
3564 Some("Variant.tag" | "Variant.as_int" | "Variant.as_float")
3565 )
3566 });
3567 assert!(
3568 !stale_variable,
3569 "anonymous-union members + outer fields must not stay as Variable"
3570 );
3571 }
3572
3573 #[test]
3577 fn test_templated_class_field_emits_property() {
3578 let source = r"
3579template<class T>
3580struct Box {
3581 T value;
3582};
3583";
3584 let staging = build_cpp(source);
3585
3586 assert_has_node_with_kind_exact(&staging, "Box.value", NodeKind::Property);
3587 let entry = cpp_find_added_node(&staging, "Box.value").expect("Box.value");
3588 assert_eq!(entry.kind, NodeKind::Property);
3589 assert!(!entry.is_static);
3590 }
3591
3592 #[test]
3598 fn test_outer_class_field_with_nested_class_present() {
3599 let source = r"
3600class Outer {
3601public:
3602 int outer_value;
3603 class Inner {
3604 public:
3605 int x;
3606 };
3607};
3608";
3609 let staging = build_cpp(source);
3610
3611 assert_has_node_with_kind_exact(&staging, "Outer.outer_value", NodeKind::Property);
3613
3614 assert_has_node_with_kind_exact(&staging, "Outer::Inner.x", NodeKind::Property);
3618
3619 let legacy_hits: Vec<_> = staging
3622 .nodes()
3623 .filter(|n| staging.resolve_node_name(n.entry) == Some("Outer::outer_value"))
3624 .collect();
3625 assert!(
3626 legacy_hits.is_empty(),
3627 "legacy `Outer::outer_value` lookup must return 0 hits"
3628 );
3629
3630 for legacy in ["Inner.x", "Outer::Inner::x", "Outer.Inner.x"] {
3634 let hits: Vec<_> = staging
3635 .nodes()
3636 .filter(|n| staging.resolve_node_name(n.entry) == Some(legacy))
3637 .collect();
3638 assert!(
3639 hits.is_empty(),
3640 "nested-class field `{legacy}` must not appear; expected only `Outer::Inner.x`"
3641 );
3642 }
3643 }
3644
3645 #[test]
3649 fn test_outer_class_with_nested_struct_emits_inner_field() {
3650 let source = r"
3651class Outer {
3652private:
3653 struct Inner {
3654 int y;
3655 };
3656};
3657";
3658 let staging = build_cpp(source);
3659
3660 assert_has_node_with_kind_exact(&staging, "Outer::Inner.y", NodeKind::Property);
3661
3662 let entry = cpp_find_added_node(&staging, "Outer::Inner.y")
3663 .expect("Outer::Inner.y should be staged");
3664 let vis = staging.resolve_local_string(entry.visibility.expect("visibility id"));
3665 assert_eq!(
3666 vis,
3667 Some("public"),
3668 "nested struct field default visibility must be 'public' \
3669 regardless of OUTER access state"
3670 );
3671 }
3672
3673 #[test]
3681 fn test_legacy_double_colon_field_lookup_returns_zero() {
3682 let source = r"
3683class Foo {
3684public:
3685 int bar;
3686 static int baz;
3687 const int qux = 0;
3688};
3689struct Quux {
3690 int corge;
3691};
3692";
3693 let staging = build_cpp(source);
3694
3695 assert_has_node_with_kind_exact(&staging, "Foo.bar", NodeKind::Property);
3697 assert_has_node_with_kind_exact(&staging, "Foo.baz", NodeKind::Property);
3698 assert_has_node_with_kind_exact(&staging, "Foo.qux", NodeKind::Constant);
3699 assert_has_node_with_kind_exact(&staging, "Quux.corge", NodeKind::Property);
3700
3701 for legacy in ["Foo::bar", "Foo::baz", "Foo::qux", "Quux::corge"] {
3704 let hits: Vec<_> = staging
3705 .nodes()
3706 .filter(|n| staging.resolve_node_name(n.entry) == Some(legacy))
3707 .collect();
3708 assert!(
3709 hits.is_empty(),
3710 "legacy lookup for {legacy:?} must return 0 hits, got {} node(s) ({:?})",
3711 hits.len(),
3712 hits.iter()
3713 .map(|n| (n.entry.kind, staging.resolve_node_name(n.entry)))
3714 .collect::<Vec<_>>()
3715 );
3716 }
3717 }
3718
3719 #[test]
3722 fn test_namespaced_class_field_qualified_name() {
3723 let source = r"
3724namespace demo {
3725 class Service {
3726 public:
3727 int counter;
3728 };
3729}
3730";
3731 let staging = build_cpp(source);
3732
3733 assert_has_node_with_kind_exact(&staging, "demo::Service.counter", NodeKind::Property);
3734 }
3735}