1use sqry_core::graph::unified::build::helper::CalleeKindHint;
19use sqry_core::graph::unified::build::shape::{CfBucket, ShapeMapping};
20use sqry_core::graph::unified::storage::shape::SignatureShape;
21use sqry_core::graph::unified::{FfiConvention, GraphBuildHelper, StagingGraph};
22use sqry_core::graph::{GraphBuilder, GraphBuilderError, GraphResult, Language, Position, Span};
23use std::{
24 collections::{HashMap, HashSet},
25 path::{Path, PathBuf},
26 sync::OnceLock,
27 time::{Duration, Instant},
28};
29use tree_sitter::{Node, Tree};
30
31const FILE_MODULE_NAME: &str = "<file_module>";
34
35type QualifiedNameMap = HashMap<(String, String), String>;
38
39type FfiRegistry = HashMap<String, (String, FfiConvention)>;
46
47type PureVirtualRegistry = HashSet<String>;
51
52const DEFAULT_GRAPH_BUILD_TIMEOUT_MS: u64 = 10_000;
53const MIN_GRAPH_BUILD_TIMEOUT_MS: u64 = 1_000;
54const MAX_GRAPH_BUILD_TIMEOUT_MS: u64 = 60_000;
55const BUDGET_CHECK_INTERVAL: u32 = 1024;
56
57fn cpp_graph_build_timeout() -> Duration {
58 let timeout_ms = std::env::var("SQRY_CPP_GRAPH_BUILD_TIMEOUT_MS")
59 .ok()
60 .and_then(|value| value.parse::<u64>().ok())
61 .unwrap_or(DEFAULT_GRAPH_BUILD_TIMEOUT_MS)
62 .clamp(MIN_GRAPH_BUILD_TIMEOUT_MS, MAX_GRAPH_BUILD_TIMEOUT_MS);
63 Duration::from_millis(timeout_ms)
64}
65
66struct BuildBudget {
67 file: PathBuf,
68 phase_timeout: Duration,
69 started_at: Instant,
70 checkpoints: u32,
71}
72
73impl BuildBudget {
74 fn new(file: &Path) -> Self {
75 Self {
76 file: file.to_path_buf(),
77 phase_timeout: cpp_graph_build_timeout(),
78 started_at: Instant::now(),
79 checkpoints: 0,
80 }
81 }
82
83 #[cfg(test)]
84 fn already_expired(file: &Path) -> Self {
85 Self {
86 file: file.to_path_buf(),
87 phase_timeout: Duration::from_secs(1),
88 started_at: Instant::now().checked_sub(Duration::from_secs(60)).unwrap(),
89 checkpoints: BUDGET_CHECK_INTERVAL - 1,
90 }
91 }
92
93 fn checkpoint(&mut self, phase: &'static str) -> GraphResult<()> {
94 self.checkpoints = self.checkpoints.wrapping_add(1);
95 if self.checkpoints.is_multiple_of(BUDGET_CHECK_INTERVAL)
96 && self.started_at.elapsed() > self.phase_timeout
97 {
98 return Err(GraphBuilderError::BuildTimedOut {
99 file: self.file.clone(),
100 phase,
101 #[allow(clippy::cast_possible_truncation)] timeout_ms: self.phase_timeout.as_millis() as u64,
103 });
104 }
105 Ok(())
106 }
107}
108
109#[allow(dead_code)] trait SpanExt {
112 fn from_node(node: &tree_sitter::Node) -> Self;
113}
114
115impl SpanExt for Span {
116 fn from_node(node: &tree_sitter::Node) -> Self {
117 Span::new(
118 Position::new(node.start_position().row, node.start_position().column),
119 Position::new(node.end_position().row, node.end_position().column),
120 )
121 }
122}
123
124#[derive(Debug)]
136struct ASTGraph {
137 contexts: Vec<FunctionContext>,
139 context_start_index: HashMap<usize, usize>,
141
142 field_types: QualifiedNameMap,
148
149 type_map: QualifiedNameMap,
156
157 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)]
227 is_virtual: bool,
228 #[allow(dead_code)]
232 is_inline: bool,
233 namespace_stack: Vec<String>,
235 class_stack: Vec<String>,
239 return_type: Option<String>,
241}
242
243impl FunctionContext {
244 #[allow(dead_code)] fn qualified_name(&self) -> &str {
246 &self.qualified_name
247 }
248}
249
250#[derive(Debug, Default, Clone, Copy)]
274pub struct CppGraphBuilder;
275
276impl CppGraphBuilder {
277 #[must_use]
279 pub fn new() -> Self {
280 Self
281 }
282
283 #[allow(clippy::unused_self)] #[allow(clippy::trivially_copy_pass_by_ref)] fn build_graph_with_budget(
286 #[allow(clippy::trivially_copy_pass_by_ref)] &self,
288 tree: &Tree,
289 content: &[u8],
290 file: &Path,
291 staging: &mut StagingGraph,
292 budget: &mut BuildBudget,
293 ) -> GraphResult<()> {
294 let mut helper = GraphBuildHelper::new(staging, file, Language::Cpp);
296
297 let ast_graph = ASTGraph::from_tree(tree.root_node(), content, budget)?;
299
300 let mut seen_includes: HashSet<String> = HashSet::new();
302
303 let mut namespace_stack: Vec<String> = Vec::new();
305 let mut class_stack: Vec<String> = Vec::new();
306
307 let mut ffi_registry = FfiRegistry::new();
310 collect_ffi_declarations(tree.root_node(), content, &mut ffi_registry, budget)?;
311
312 let mut pure_virtual_registry = PureVirtualRegistry::new();
314 collect_pure_virtual_interfaces(
315 tree.root_node(),
316 content,
317 &mut pure_virtual_registry,
318 budget,
319 )?;
320
321 walk_tree_for_graph(
323 tree.root_node(),
324 content,
325 &ast_graph,
326 &mut helper,
327 &mut seen_includes,
328 &mut namespace_stack,
329 &mut class_stack,
330 &ffi_registry,
331 &pure_virtual_registry,
332 budget,
333 )?;
334
335 Ok(())
336 }
337
338 #[allow(dead_code)] fn extract_class_attributes(node: &tree_sitter::Node, content: &[u8]) -> Vec<String> {
341 let mut attributes = Vec::new();
342 let mut cursor = node.walk();
343 for child in node.children(&mut cursor) {
344 if child.kind() == "modifiers" {
345 let mut mod_cursor = child.walk();
346 for modifier in child.children(&mut mod_cursor) {
347 if let Ok(mod_text) = modifier.utf8_text(content) {
348 match mod_text {
349 "template" => attributes.push("template".to_string()),
350 "sealed" => attributes.push("sealed".to_string()),
351 "abstract" => attributes.push("abstract".to_string()),
352 "open" => attributes.push("open".to_string()),
353 "final" => attributes.push("final".to_string()),
354 "inner" => attributes.push("inner".to_string()),
355 "value" => attributes.push("value".to_string()),
356 _ => {}
357 }
358 }
359 }
360 }
361 }
362 attributes
363 }
364
365 #[allow(dead_code)] fn extract_is_virtual(node: &tree_sitter::Node, content: &[u8]) -> bool {
368 if let Some(spec) = node.child_by_field_name("declaration_specifiers")
369 && let Ok(text) = spec.utf8_text(content)
370 && text.contains("virtual")
371 {
372 return true;
373 }
374
375 if let Ok(text) = node.utf8_text(content)
376 && text.contains("virtual")
377 {
378 return true;
379 }
380
381 if let Some(parent) = node.parent()
382 && (parent.kind() == "field_declaration" || parent.kind() == "declaration")
383 && let Ok(text) = parent.utf8_text(content)
384 && text.contains("virtual")
385 {
386 return true;
387 }
388
389 false
390 }
391
392 #[allow(dead_code)] fn extract_function_attributes(node: &tree_sitter::Node, content: &[u8]) -> Vec<String> {
395 let mut attributes = Vec::new();
396 for node_ref in [
397 node.child_by_field_name("declaration_specifiers"),
398 node.parent(),
399 ]
400 .into_iter()
401 .flatten()
402 {
403 if let Ok(text) = node_ref.utf8_text(content) {
404 for keyword in [
405 "virtual",
406 "inline",
407 "constexpr",
408 "operator",
409 "override",
410 "static",
411 ] {
412 if text.contains(keyword) && !attributes.contains(&keyword.to_string()) {
413 attributes.push(keyword.to_string());
414 }
415 }
416 }
417 }
418
419 if let Ok(text) = node.utf8_text(content) {
420 for keyword in [
421 "virtual",
422 "inline",
423 "constexpr",
424 "operator",
425 "override",
426 "static",
427 ] {
428 if text.contains(keyword) && !attributes.contains(&keyword.to_string()) {
429 attributes.push(keyword.to_string());
430 }
431 }
432 }
433
434 attributes
435 }
436}
437
438impl GraphBuilder for CppGraphBuilder {
439 fn language(&self) -> Language {
440 Language::Cpp
441 }
442
443 fn shape_mapping(&self) -> Option<&dyn ShapeMapping> {
444 Some(cpp_shape_mapping())
445 }
446
447 fn build_graph(
448 &self,
449 tree: &Tree,
450 content: &[u8],
451 file: &Path,
452 staging: &mut StagingGraph,
453 ) -> GraphResult<()> {
454 let mut budget = BuildBudget::new(file);
455 self.build_graph_with_budget(tree, content, file, staging, &mut budget)
456 }
457}
458
459pub struct CppShapeMapping {
468 cf_by_kind_id: Vec<Option<CfBucket>>,
469}
470
471impl CppShapeMapping {
472 fn build() -> Self {
474 let lang: tree_sitter::Language = tree_sitter_cpp::LANGUAGE.into();
475 let count = lang.node_kind_count();
476 let mut cf_by_kind_id = vec![None; count];
477 for (id, slot) in cf_by_kind_id.iter_mut().enumerate() {
478 let Ok(kind_id) = u16::try_from(id) else {
479 break;
480 };
481 if !lang.node_kind_is_named(kind_id) {
482 continue;
483 }
484 if let Some(name) = lang.node_kind_for_id(kind_id) {
485 *slot = cf_bucket_for_cpp_kind(name);
486 }
487 }
488 Self { cf_by_kind_id }
489 }
490}
491
492impl ShapeMapping for CppShapeMapping {
493 fn cf_bucket(&self, ts_node_kind_id: u16) -> Option<CfBucket> {
494 self.cf_by_kind_id
495 .get(ts_node_kind_id as usize)
496 .copied()
497 .flatten()
498 }
499
500 fn signature_shape(&self, fn_node: Node, _src: &[u8]) -> SignatureShape {
501 let mut shape = SignatureShape::default();
502 if let Some(params) = cpp_parameter_list(fn_node) {
506 let mut cursor = params.walk();
507 for child in params.named_children(&mut cursor) {
508 match child.kind() {
509 "parameter_declaration" => {
510 shape.arity_positional = shape.arity_positional.saturating_add(1);
511 }
512 "optional_parameter_declaration" => {
514 shape.arity_positional = shape.arity_positional.saturating_add(1);
515 shape.has_defaults = true;
516 }
517 "variadic_parameter_declaration" | "variadic_declarator" => {
519 shape.has_varargs = true;
520 }
521 _ => {}
522 }
523 }
524 }
525 shape.has_return_annotation = fn_node.child_by_field_name("type").is_some();
528 shape
529 }
530}
531
532fn cpp_parameter_list(fn_node: Node) -> Option<Node> {
536 let mut declarator = fn_node.child_by_field_name("declarator")?;
537 for _ in 0..8 {
540 if declarator.kind() == "function_declarator" {
541 return declarator.child_by_field_name("parameters");
542 }
543 match declarator.child_by_field_name("declarator") {
544 Some(inner) => declarator = inner,
545 None => break,
546 }
547 }
548 None
549}
550
551fn cf_bucket_for_cpp_kind(name: &str) -> Option<CfBucket> {
554 let bucket = match name {
555 "if_statement" | "conditional_expression" => CfBucket::Branch,
556 "for_statement" | "for_range_loop" | "while_statement" | "do_statement" => CfBucket::Loop,
557 "switch_statement" | "case_statement" => CfBucket::Match,
558 "try_statement" => CfBucket::Try,
559 "catch_clause" => CfBucket::Catch,
560 "throw_statement" | "throw_expression" => CfBucket::Throw,
561 "return_statement" | "co_return_statement" => CfBucket::Return,
562 "co_yield_expression" => CfBucket::Yield,
563 "co_await_expression" => CfBucket::Await,
564 "break_statement" | "continue_statement" | "goto_statement" => CfBucket::BreakContinue,
565 "call_expression" => CfBucket::Call,
566 "assignment_expression" | "init_declarator" | "declaration" => CfBucket::Assign,
567 "lambda_expression" => CfBucket::Closure,
568 _ => return None,
569 };
570 Some(bucket)
571}
572
573#[must_use]
575pub fn cpp_shape_mapping() -> &'static CppShapeMapping {
576 static MAPPING: OnceLock<CppShapeMapping> = OnceLock::new();
577 MAPPING.get_or_init(CppShapeMapping::build)
578}
579
580fn extract_namespace_map(
592 node: Node,
593 content: &[u8],
594 budget: &mut BuildBudget,
595) -> GraphResult<HashMap<std::ops::Range<usize>, String>> {
596 let mut map = HashMap::new();
597
598 let recursion_limits = sqry_core::config::RecursionLimits::load_or_default()
600 .expect("Failed to load recursion limits");
601 let file_ops_depth = recursion_limits
602 .effective_file_ops_depth()
603 .expect("Invalid file_ops_depth configuration");
604 let mut guard = sqry_core::query::security::RecursionGuard::new(file_ops_depth)
605 .expect("Failed to create recursion guard");
606
607 extract_namespaces_recursive(node, content, "", &mut map, &mut guard, budget).map_err(|e| {
608 match e {
609 timeout @ GraphBuilderError::BuildTimedOut { .. } => timeout,
610 other => GraphBuilderError::ParseError {
611 span: span_from_node(node),
612 reason: format!("C++ namespace extraction failed: {other}"),
613 },
614 }
615 })?;
616
617 Ok(map)
618}
619
620fn extract_namespaces_recursive(
626 node: Node,
627 content: &[u8],
628 current_ns: &str,
629 map: &mut HashMap<std::ops::Range<usize>, String>,
630 guard: &mut sqry_core::query::security::RecursionGuard,
631 budget: &mut BuildBudget,
632) -> GraphResult<()> {
633 budget.checkpoint("cpp:extract_namespace_map")?;
634 guard.enter().map_err(|e| GraphBuilderError::ParseError {
635 span: span_from_node(node),
636 reason: format!("C++ namespace extraction hit recursion limit: {e}"),
637 })?;
638
639 if node.kind() == "namespace_definition" {
640 let ns_name = if let Some(name_node) = node.child_by_field_name("name") {
642 extract_identifier(name_node, content)
643 } else {
644 String::from("anonymous")
646 };
647
648 let new_ns = if current_ns.is_empty() {
650 format!("{ns_name}::")
651 } else {
652 format!("{current_ns}{ns_name}::")
653 };
654
655 if let Some(body) = node.child_by_field_name("body") {
657 let range = body.start_byte()..body.end_byte();
658 map.insert(range, new_ns.clone());
659
660 let mut cursor = body.walk();
662 for child in body.children(&mut cursor) {
663 extract_namespaces_recursive(child, content, &new_ns, map, guard, budget)?;
664 }
665 }
666 } else {
667 let mut cursor = node.walk();
669 for child in node.children(&mut cursor) {
670 extract_namespaces_recursive(child, content, current_ns, map, guard, budget)?;
671 }
672 }
673
674 guard.exit();
675 Ok(())
676}
677
678fn extract_identifier(node: Node, content: &[u8]) -> String {
680 node.utf8_text(content).unwrap_or("").to_string()
681}
682
683fn find_namespace_for_offset(
685 byte_offset: usize,
686 namespace_map: &HashMap<std::ops::Range<usize>, String>,
687) -> String {
688 let mut matching_ranges: Vec<_> = namespace_map
690 .iter()
691 .filter(|(range, _)| range.contains(&byte_offset))
692 .collect();
693
694 matching_ranges.sort_by_key(|(range, _)| range.end - range.start);
696
697 matching_ranges
699 .first()
700 .map_or("", |(_, ns)| ns.as_str())
701 .to_string()
702}
703
704fn extract_cpp_contexts(
711 node: Node,
712 content: &[u8],
713 namespace_map: &HashMap<std::ops::Range<usize>, String>,
714 budget: &mut BuildBudget,
715) -> GraphResult<Vec<FunctionContext>> {
716 let mut contexts = Vec::new();
717 let mut class_stack = Vec::new();
718
719 let recursion_limits = sqry_core::config::RecursionLimits::load_or_default()
721 .expect("Failed to load recursion limits");
722 let file_ops_depth = recursion_limits
723 .effective_file_ops_depth()
724 .expect("Invalid file_ops_depth configuration");
725 let mut guard = sqry_core::query::security::RecursionGuard::new(file_ops_depth)
726 .expect("Failed to create recursion guard");
727
728 extract_contexts_recursive(
729 node,
730 content,
731 namespace_map,
732 &mut contexts,
733 &mut class_stack,
734 &mut guard,
735 budget,
736 )
737 .map_err(|e| match e {
738 timeout @ GraphBuilderError::BuildTimedOut { .. } => timeout,
739 other => GraphBuilderError::ParseError {
740 span: span_from_node(node),
741 reason: format!("C++ context extraction failed: {other}"),
742 },
743 })?;
744
745 Ok(contexts)
746}
747
748fn extract_contexts_recursive(
753 node: Node,
754 content: &[u8],
755 namespace_map: &HashMap<std::ops::Range<usize>, String>,
756 contexts: &mut Vec<FunctionContext>,
757 class_stack: &mut Vec<String>,
758 guard: &mut sqry_core::query::security::RecursionGuard,
759 budget: &mut BuildBudget,
760) -> GraphResult<()> {
761 budget.checkpoint("cpp:extract_contexts")?;
762 guard.enter().map_err(|e| GraphBuilderError::ParseError {
763 span: span_from_node(node),
764 reason: format!("C++ context extraction hit recursion limit: {e}"),
765 })?;
766
767 match node.kind() {
768 "class_specifier" | "struct_specifier" => {
769 if let Some(name_node) = node.child_by_field_name("name") {
771 let class_name = extract_identifier(name_node, content);
772 class_stack.push(class_name);
773
774 if let Some(body) = node.child_by_field_name("body") {
776 let mut cursor = body.walk();
777 for child in body.children(&mut cursor) {
778 extract_contexts_recursive(
779 child,
780 content,
781 namespace_map,
782 contexts,
783 class_stack,
784 guard,
785 budget,
786 )?;
787 }
788 }
789
790 class_stack.pop();
791 }
792 }
793
794 "function_definition" => {
795 if let Some(declarator) = node.child_by_field_name("declarator") {
797 let (func_name, class_prefix) =
798 extract_function_name_with_class(declarator, content);
799
800 let namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
802 let namespace_stack: Vec<String> = if namespace.is_empty() {
803 Vec::new()
804 } else {
805 namespace
806 .trim_end_matches("::")
807 .split("::")
808 .map(String::from)
809 .collect()
810 };
811
812 let effective_class_stack: Vec<String> = if !class_stack.is_empty() {
816 class_stack.clone()
817 } else if let Some(ref prefix) = class_prefix {
818 vec![prefix.clone()]
819 } else {
820 Vec::new()
821 };
822
823 let qualified_name =
825 build_qualified_name(&namespace_stack, &effective_class_stack, &func_name);
826
827 let is_static = is_static_function(node, content);
829 let is_virtual = is_virtual_function(node, content);
830 let is_inline = is_inline_function(node, content);
831
832 let return_type = node
834 .child_by_field_name("type")
835 .and_then(|type_node| type_node.utf8_text(content).ok())
836 .map(std::string::ToString::to_string);
837
838 let span = (node.start_byte(), node.end_byte());
840
841 contexts.push(FunctionContext {
842 qualified_name,
843 span,
844 is_static,
845 is_virtual,
846 is_inline,
847 namespace_stack,
848 class_stack: effective_class_stack,
849 return_type,
850 });
851 }
852
853 }
855
856 _ => {
857 let mut cursor = node.walk();
859 for child in node.children(&mut cursor) {
860 extract_contexts_recursive(
861 child,
862 content,
863 namespace_map,
864 contexts,
865 class_stack,
866 guard,
867 budget,
868 )?;
869 }
870 }
871 }
872
873 guard.exit();
874 Ok(())
875}
876
877fn build_qualified_name(namespace_stack: &[String], class_stack: &[String], name: &str) -> String {
882 let mut parts = Vec::new();
883
884 parts.extend(namespace_stack.iter().cloned());
886
887 for class_name in class_stack {
889 parts.push(class_name.clone());
890 }
891
892 parts.push(name.to_string());
894
895 parts.join("::")
896}
897
898fn extract_function_name_with_class(declarator: Node, content: &[u8]) -> (String, Option<String>) {
903 match declarator.kind() {
911 "function_declarator" => {
912 if let Some(declarator_inner) = declarator.child_by_field_name("declarator") {
914 extract_function_name_with_class(declarator_inner, content)
915 } else {
916 (extract_identifier(declarator, content), None)
917 }
918 }
919 "qualified_identifier" => {
920 let name = if let Some(name_node) = declarator.child_by_field_name("name") {
922 extract_identifier(name_node, content)
923 } else {
924 extract_identifier(declarator, content)
925 };
926
927 let class_prefix = declarator
929 .child_by_field_name("scope")
930 .map(|scope_node| extract_identifier(scope_node, content));
931
932 (name, class_prefix)
933 }
934 "field_identifier" | "identifier" | "destructor_name" | "operator_name" => {
935 (extract_identifier(declarator, content), None)
936 }
937 _ => {
938 (extract_identifier(declarator, content), None)
940 }
941 }
942}
943
944#[allow(dead_code)]
946fn extract_function_name(declarator: Node, content: &[u8]) -> String {
947 extract_function_name_with_class(declarator, content).0
948}
949
950fn is_static_function(node: Node, content: &[u8]) -> bool {
952 has_specifier(node, "static", content)
953}
954
955fn is_virtual_function(node: Node, content: &[u8]) -> bool {
957 has_specifier(node, "virtual", content)
958}
959
960fn is_inline_function(node: Node, content: &[u8]) -> bool {
962 has_specifier(node, "inline", content)
963}
964
965fn has_specifier(node: Node, specifier: &str, content: &[u8]) -> bool {
967 let mut cursor = node.walk();
969 for child in node.children(&mut cursor) {
970 if (child.kind() == "storage_class_specifier"
971 || child.kind() == "type_qualifier"
972 || child.kind() == "virtual"
973 || child.kind() == "inline")
974 && let Ok(text) = child.utf8_text(content)
975 && text == specifier
976 {
977 return true;
978 }
979 }
980 false
981}
982
983fn extract_field_and_type_info(
993 node: Node,
994 content: &[u8],
995 namespace_map: &HashMap<std::ops::Range<usize>, String>,
996 budget: &mut BuildBudget,
997) -> GraphResult<(QualifiedNameMap, QualifiedNameMap)> {
998 let mut field_types = HashMap::new();
999 let mut type_map = HashMap::new();
1000 let mut class_stack = Vec::new();
1001
1002 let mut declared_classes: HashSet<String> = HashSet::new();
1010 let mut collect_stack: Vec<String> = Vec::new();
1011 collect_declared_class_fqns(
1012 node,
1013 content,
1014 namespace_map,
1015 &mut declared_classes,
1016 &mut collect_stack,
1017 budget,
1018 )?;
1019
1020 extract_fields_recursive(
1021 node,
1022 content,
1023 namespace_map,
1024 &declared_classes,
1025 &mut field_types,
1026 &mut type_map,
1027 &mut class_stack,
1028 budget,
1029 )?;
1030
1031 Ok((field_types, type_map))
1032}
1033
1034fn build_class_fqn(class_name: &str, namespace: &str, class_stack: &[String]) -> String {
1041 if let Some(parent_fqn) = class_stack.last() {
1042 format!("{parent_fqn}::{class_name}")
1043 } else if namespace.is_empty() {
1044 class_name.to_string()
1045 } else {
1046 format!("{}::{}", namespace.trim_end_matches("::"), class_name)
1047 }
1048}
1049
1050fn collect_declared_class_fqns(
1054 node: Node,
1055 content: &[u8],
1056 namespace_map: &HashMap<std::ops::Range<usize>, String>,
1057 declared: &mut HashSet<String>,
1058 class_stack: &mut Vec<String>,
1059 budget: &mut BuildBudget,
1060) -> GraphResult<()> {
1061 budget.checkpoint("cpp:collect_declared_classes")?;
1062 match node.kind() {
1063 "class_specifier" | "struct_specifier" => {
1064 if let Some(name_node) = node.child_by_field_name("name") {
1065 let class_name = extract_identifier(name_node, content);
1066 let namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
1067 let class_fqn = build_class_fqn(&class_name, &namespace, class_stack);
1068
1069 declared.insert(class_fqn.clone());
1070 class_stack.push(class_fqn);
1071
1072 let mut cursor = node.walk();
1073 for child in node.children(&mut cursor) {
1074 collect_declared_class_fqns(
1075 child,
1076 content,
1077 namespace_map,
1078 declared,
1079 class_stack,
1080 budget,
1081 )?;
1082 }
1083
1084 class_stack.pop();
1085 }
1086 }
1087 _ => {
1088 let mut cursor = node.walk();
1089 for child in node.children(&mut cursor) {
1090 collect_declared_class_fqns(
1091 child,
1092 content,
1093 namespace_map,
1094 declared,
1095 class_stack,
1096 budget,
1097 )?;
1098 }
1099 }
1100 }
1101 Ok(())
1102}
1103
1104fn is_cpp_primitive(name: &str) -> bool {
1108 matches!(
1109 name,
1110 "int"
1111 | "void"
1112 | "bool"
1113 | "char"
1114 | "double"
1115 | "float"
1116 | "long"
1117 | "short"
1118 | "unsigned"
1119 | "signed"
1120 | "wchar_t"
1121 | "auto"
1122 | "char8_t"
1123 | "char16_t"
1124 | "char32_t"
1125 | "int8_t"
1126 | "int16_t"
1127 | "int32_t"
1128 | "int64_t"
1129 | "uint8_t"
1130 | "uint16_t"
1131 | "uint32_t"
1132 | "uint64_t"
1133 | "intptr_t"
1134 | "uintptr_t"
1135 | "size_t"
1136 | "ssize_t"
1137 | "ptrdiff_t"
1138 )
1139}
1140
1141fn qualify_field_type(
1156 resolved_type: &str,
1157 class_fqn: &str,
1158 namespace: &str,
1159 declared_classes: &HashSet<String>,
1160) -> String {
1161 if resolved_type.contains("::") || is_cpp_primitive(resolved_type) {
1163 return resolved_type.to_string();
1164 }
1165
1166 let candidate = format!("{class_fqn}::{resolved_type}");
1168 if declared_classes.contains(&candidate) {
1169 return candidate;
1170 }
1171
1172 let mut prefix = class_fqn;
1174 while let Some(idx) = prefix.rfind("::") {
1175 prefix = &prefix[..idx];
1176 let candidate = format!("{prefix}::{resolved_type}");
1177 if declared_classes.contains(&candidate) {
1178 return candidate;
1179 }
1180 }
1181
1182 let namespace_key = namespace.trim_end_matches("::");
1184 if !namespace_key.is_empty() {
1185 let candidate = format!("{namespace_key}::{resolved_type}");
1186 if declared_classes.contains(&candidate) {
1187 return candidate;
1188 }
1189 }
1190
1191 resolved_type.to_string()
1195}
1196
1197fn extract_fields_recursive(
1199 node: Node,
1200 content: &[u8],
1201 namespace_map: &HashMap<std::ops::Range<usize>, String>,
1202 declared_classes: &HashSet<String>,
1203 field_types: &mut HashMap<(String, String), String>,
1204 type_map: &mut HashMap<(String, String), String>,
1205 class_stack: &mut Vec<String>,
1206 budget: &mut BuildBudget,
1207) -> GraphResult<()> {
1208 budget.checkpoint("cpp:extract_fields")?;
1209 match node.kind() {
1210 "class_specifier" | "struct_specifier" => {
1211 if let Some(name_node) = node.child_by_field_name("name") {
1213 let class_name = extract_identifier(name_node, content);
1214 let namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
1215
1216 let class_fqn = build_class_fqn(&class_name, &namespace, class_stack);
1218
1219 class_stack.push(class_fqn.clone());
1220
1221 let mut cursor = node.walk();
1223 for child in node.children(&mut cursor) {
1224 extract_fields_recursive(
1225 child,
1226 content,
1227 namespace_map,
1228 declared_classes,
1229 field_types,
1230 type_map,
1231 class_stack,
1232 budget,
1233 )?;
1234 }
1235
1236 class_stack.pop();
1237 }
1238 }
1239
1240 "field_declaration" => {
1241 if let Some(class_fqn) = class_stack.last() {
1243 extract_field_declaration(
1244 node,
1245 content,
1246 class_fqn,
1247 namespace_map,
1248 declared_classes,
1249 field_types,
1250 type_map,
1251 );
1252 }
1253
1254 let mut cursor = node.walk();
1259 for child in node.children(&mut cursor) {
1260 extract_fields_recursive(
1261 child,
1262 content,
1263 namespace_map,
1264 declared_classes,
1265 field_types,
1266 type_map,
1267 class_stack,
1268 budget,
1269 )?;
1270 }
1271 }
1272
1273 "using_directive" => {
1274 extract_using_directive(node, content, namespace_map, type_map);
1276 }
1277
1278 "using_declaration" => {
1279 extract_using_declaration(node, content, namespace_map, type_map);
1281 }
1282
1283 _ => {
1284 let mut cursor = node.walk();
1286 for child in node.children(&mut cursor) {
1287 extract_fields_recursive(
1288 child,
1289 content,
1290 namespace_map,
1291 declared_classes,
1292 field_types,
1293 type_map,
1294 class_stack,
1295 budget,
1296 )?;
1297 }
1298 }
1299 }
1300
1301 Ok(())
1302}
1303
1304fn extract_field_declaration(
1306 node: Node,
1307 content: &[u8],
1308 class_fqn: &str,
1309 namespace_map: &HashMap<std::ops::Range<usize>, String>,
1310 declared_classes: &HashSet<String>,
1311 field_types: &mut HashMap<(String, String), String>,
1312 type_map: &HashMap<(String, String), String>,
1313) {
1314 let mut field_type = None;
1319 let mut field_names = Vec::new();
1320
1321 let mut cursor = node.walk();
1322 for child in node.children(&mut cursor) {
1323 match child.kind() {
1324 "type_identifier" | "primitive_type" | "qualified_identifier" | "template_type" => {
1325 field_type = Some(extract_type_name(child, content));
1326 }
1327 "field_identifier" => {
1328 field_names.push(extract_identifier(child, content));
1330 }
1331 "field_declarator"
1332 | "init_declarator"
1333 | "pointer_declarator"
1334 | "reference_declarator"
1335 | "array_declarator" => {
1336 if let Some(name) = extract_field_name(child, content) {
1338 field_names.push(name);
1339 }
1340 }
1341 _ => {}
1342 }
1343 }
1344
1345 if let Some(ftype) = field_type {
1349 let namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
1350 let resolved = resolve_type_to_fqn(&ftype, &namespace, type_map);
1351 let field_type_fqn = qualify_field_type(&resolved, class_fqn, &namespace, declared_classes);
1352
1353 for fname in field_names {
1355 field_types.insert((class_fqn.to_string(), fname), field_type_fqn.clone());
1356 }
1357 }
1358}
1359
1360fn extract_type_name(type_node: Node, content: &[u8]) -> String {
1362 match type_node.kind() {
1363 "type_identifier" | "primitive_type" => extract_identifier(type_node, content),
1364 "qualified_identifier" => {
1365 extract_identifier(type_node, content)
1367 }
1368 "template_type" => {
1369 if let Some(name) = type_node.child_by_field_name("name") {
1371 extract_identifier(name, content)
1372 } else {
1373 extract_identifier(type_node, content)
1374 }
1375 }
1376 _ => {
1377 extract_identifier(type_node, content)
1379 }
1380 }
1381}
1382
1383fn extract_field_name(declarator: Node, content: &[u8]) -> Option<String> {
1385 match declarator.kind() {
1386 "field_declarator" => {
1387 if let Some(declarator_inner) = declarator.child_by_field_name("declarator") {
1389 extract_field_name(declarator_inner, content)
1390 } else {
1391 Some(extract_identifier(declarator, content))
1392 }
1393 }
1394 "field_identifier" | "identifier" => Some(extract_identifier(declarator, content)),
1395 "pointer_declarator" | "reference_declarator" | "array_declarator" => {
1396 if let Some(declarator_inner) = declarator.child_by_field_name("declarator") {
1398 extract_field_name(declarator_inner, content)
1399 } else {
1400 None
1401 }
1402 }
1403 "init_declarator" => {
1404 if let Some(declarator_inner) = declarator.child_by_field_name("declarator") {
1406 extract_field_name(declarator_inner, content)
1407 } else {
1408 None
1409 }
1410 }
1411 _ => None,
1412 }
1413}
1414
1415fn resolve_type_to_fqn(
1417 type_name: &str,
1418 namespace: &str,
1419 type_map: &HashMap<(String, String), String>,
1420) -> String {
1421 if type_name.contains("::") {
1423 return type_name.to_string();
1424 }
1425
1426 let namespace_key = namespace.trim_end_matches("::").to_string();
1428 if let Some(fqn) = type_map.get(&(namespace_key.clone(), type_name.to_string())) {
1429 return fqn.clone();
1430 }
1431
1432 if let Some(fqn) = type_map.get(&(String::new(), type_name.to_string())) {
1434 return fqn.clone();
1435 }
1436
1437 type_name.to_string()
1439}
1440
1441fn extract_using_directive(
1443 node: Node,
1444 content: &[u8],
1445 namespace_map: &HashMap<std::ops::Range<usize>, String>,
1446 _type_map: &mut HashMap<(String, String), String>,
1447) {
1448 let _namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
1452
1453 if let Some(name_node) = node.child_by_field_name("name") {
1455 let _using_ns = extract_identifier(name_node, content);
1456 }
1460}
1461
1462fn extract_using_declaration(
1467 node: Node,
1468 content: &[u8],
1469 namespace_map: &HashMap<std::ops::Range<usize>, String>,
1470 type_map: &mut HashMap<(String, String), String>,
1471) {
1472 let namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
1473 let namespace_key = namespace.trim_end_matches("::").to_string();
1474
1475 let mut cursor = node.walk();
1477 for child in node.children(&mut cursor) {
1478 if child.kind() == "qualified_identifier" || child.kind() == "identifier" {
1479 let fqn = extract_identifier(child, content);
1480
1481 if let Some(simple_name) = fqn.split("::").last() {
1483 type_map.insert((namespace_key, simple_name.to_string()), fqn);
1485 }
1486 break;
1487 }
1488 }
1489}
1490
1491fn resolve_callee_name(
1513 function_node: Node<'_>,
1514 callee_name: &str,
1515 caller_ctx: &FunctionContext,
1516 ast_graph: &ASTGraph,
1517 content: &[u8],
1518) -> String {
1519 if function_node.kind() == "field_expression" {
1521 if let Some(fqn) = resolve_member_call(function_node, caller_ctx, ast_graph, content) {
1522 return fqn;
1523 }
1524 return resolve_callee_name_namespace_prefixed(callee_name, caller_ctx);
1525 }
1526
1527 if !callee_name.starts_with("::")
1531 && callee_name.contains("::")
1532 && let Some(fqn) = resolve_static_call(function_node, callee_name, ast_graph)
1533 {
1534 return fqn;
1535 }
1536
1537 resolve_callee_name_namespace_prefixed(callee_name, caller_ctx)
1538}
1539
1540fn resolve_callee_name_namespace_prefixed(
1545 callee_name: &str,
1546 caller_ctx: &FunctionContext,
1547) -> String {
1548 if callee_name.starts_with("::") {
1550 return callee_name.trim_start_matches("::").to_string();
1551 }
1552
1553 if callee_name.contains("::") {
1555 if !caller_ctx.namespace_stack.is_empty() {
1557 let namespace_prefix = caller_ctx.namespace_stack.join("::");
1558 return format!("{namespace_prefix}::{callee_name}");
1559 }
1560 return callee_name.to_string();
1561 }
1562
1563 let mut parts = Vec::new();
1565
1566 if !caller_ctx.namespace_stack.is_empty() {
1568 parts.extend(caller_ctx.namespace_stack.iter().cloned());
1569 }
1570
1571 parts.push(callee_name.to_string());
1576
1577 parts.join("::")
1578}
1579
1580fn enclosing_class_fqn(caller_ctx: &FunctionContext) -> Option<String> {
1589 if caller_ctx.class_stack.is_empty() {
1590 return None;
1591 }
1592 let mut parts: Vec<&str> = Vec::new();
1593 parts.extend(caller_ctx.namespace_stack.iter().map(String::as_str));
1594 parts.extend(caller_ctx.class_stack.iter().map(String::as_str));
1595 Some(parts.join("::"))
1596}
1597
1598fn resolve_member_call(
1607 function_node: Node<'_>,
1608 caller_ctx: &FunctionContext,
1609 ast_graph: &ASTGraph,
1610 content: &[u8],
1611) -> Option<String> {
1612 let receiver_node = function_node.child_by_field_name("argument")?;
1613 let method_node = function_node.child_by_field_name("field")?;
1614
1615 if !matches!(receiver_node.kind(), "identifier" | "field_identifier") {
1618 return None;
1619 }
1620
1621 let receiver_text = receiver_node.utf8_text(content).ok()?.trim();
1622 let method_text = method_node.utf8_text(content).ok()?.trim();
1623 if receiver_text.is_empty() || method_text.is_empty() {
1624 return None;
1625 }
1626
1627 let class_fqn = enclosing_class_fqn(caller_ctx)?;
1628 let field_type = ast_graph
1629 .field_types
1630 .get(&(class_fqn, receiver_text.to_string()))?;
1631
1632 Some(format!("{field_type}::{method_text}"))
1633}
1634
1635fn resolve_static_call(
1641 function_node: Node<'_>,
1642 callee_name: &str,
1643 ast_graph: &ASTGraph,
1644) -> Option<String> {
1645 let (qualifier, method) = callee_name.rsplit_once("::")?;
1646 if qualifier.is_empty() || qualifier.contains("::") || method.is_empty() {
1648 return None;
1649 }
1650
1651 let namespace = find_namespace_for_offset(function_node.start_byte(), &ast_graph.namespace_map);
1652 let namespace_key = namespace.trim_end_matches("::").to_string();
1653
1654 let resolved_qualifier = ast_graph
1655 .type_map
1656 .get(&(namespace_key, qualifier.to_string()))
1657 .or_else(|| {
1658 ast_graph
1659 .type_map
1660 .get(&(String::new(), qualifier.to_string()))
1661 })?;
1662
1663 Some(format!("{resolved_qualifier}::{method}"))
1664}
1665
1666fn strip_type_qualifiers(type_text: &str) -> String {
1673 let mut result = type_text.trim().to_string();
1674
1675 result = result.replace("const ", "");
1677 result = result.replace("volatile ", "");
1678 result = result.replace("mutable ", "");
1679 result = result.replace("constexpr ", "");
1680
1681 result = result.replace(" const", "");
1683 result = result.replace(" volatile", "");
1684 result = result.replace(" mutable", "");
1685 result = result.replace(" constexpr", "");
1686
1687 result = result.replace(['*', '&'], "");
1689
1690 result = result.trim().to_string();
1692
1693 if let Some(last_part) = result.split("::").last() {
1695 result = last_part.to_string();
1696 }
1697
1698 if let Some(open_bracket) = result.find('<') {
1700 result = result[..open_bracket].to_string();
1701 }
1702
1703 result.trim().to_string()
1704}
1705
1706#[allow(clippy::unnecessary_wraps, clippy::too_many_lines)]
1722fn process_field_declaration(
1723 node: Node,
1724 content: &[u8],
1725 class_qualified_name: &str,
1726 visibility: &str,
1727 helper: &mut GraphBuildHelper,
1728) -> GraphResult<()> {
1729 let mut field_type_text = None;
1731 let mut field_names = Vec::new();
1732 let mut is_static_kw = false;
1735 let mut is_const = false;
1736 let mut is_constexpr = false;
1737
1738 let mut cursor = node.walk();
1739 for child in node.children(&mut cursor) {
1740 match child.kind() {
1741 "type_identifier" | "primitive_type" => {
1742 if let Ok(text) = child.utf8_text(content) {
1743 field_type_text = Some(text.to_string());
1744 }
1745 }
1746 "qualified_identifier" => {
1747 if let Ok(text) = child.utf8_text(content) {
1749 field_type_text = Some(text.to_string());
1750 }
1751 }
1752 "template_type" => {
1753 if let Ok(text) = child.utf8_text(content) {
1755 field_type_text = Some(text.to_string());
1756 }
1757 }
1758 "sized_type_specifier" => {
1759 if let Ok(text) = child.utf8_text(content) {
1761 field_type_text = Some(text.to_string());
1762 }
1763 }
1764 "type_qualifier" => {
1765 if let Ok(text) = child.utf8_text(content) {
1772 let trimmed = text.trim();
1773 if trimmed == "const" {
1774 is_const = true;
1775 } else if trimmed == "constexpr" {
1776 is_constexpr = true;
1777 }
1778 if field_type_text.is_none() {
1779 field_type_text = Some(text.to_string());
1780 }
1781 }
1782 }
1783 "storage_class_specifier" => {
1784 if let Ok(text) = child.utf8_text(content) {
1787 let trimmed = text.trim();
1788 if trimmed == "static" {
1789 is_static_kw = true;
1790 } else if trimmed == "constexpr" {
1791 is_constexpr = true;
1792 }
1793 }
1794 }
1795 "auto" => {
1796 field_type_text = Some("auto".to_string());
1798 }
1799 "decltype" => {
1800 if let Ok(text) = child.utf8_text(content) {
1802 field_type_text = Some(text.to_string());
1803 }
1804 }
1805 "struct_specifier" | "class_specifier" | "enum_specifier" | "union_specifier" => {
1806 if let Ok(text) = child.utf8_text(content) {
1808 field_type_text = Some(text.to_string());
1809 }
1810 }
1811 "field_identifier" => {
1812 if let Ok(name) = child.utf8_text(content) {
1813 field_names.push(name.trim().to_string());
1814 }
1815 }
1816 "field_declarator"
1817 | "pointer_declarator"
1818 | "reference_declarator"
1819 | "init_declarator" => {
1820 if let Some(name) = extract_field_name(child, content) {
1822 field_names.push(name);
1823 }
1824 }
1825 _ => {}
1826 }
1827 }
1828
1829 if let Some(type_text) = field_type_text {
1831 let base_type = strip_type_qualifiers(&type_text);
1832 let is_constant = is_const || is_constexpr;
1833
1834 for field_name in field_names {
1835 let field_qualified = format!("{class_qualified_name}.{field_name}");
1838 let span = span_from_node(node);
1839
1840 let field_id = if is_constant {
1844 helper.add_constant_with_name_static_and_visibility(
1845 &field_name,
1846 &field_qualified,
1847 Some(span),
1848 is_static_kw,
1849 Some(visibility),
1850 )
1851 } else {
1852 helper.add_property_with_name_static_and_visibility(
1853 &field_name,
1854 &field_qualified,
1855 Some(span),
1856 is_static_kw,
1857 Some(visibility),
1858 )
1859 };
1860
1861 let type_id = helper.add_type(&base_type, None);
1863
1864 helper.add_typeof_edge_with_context(
1866 field_id,
1867 type_id,
1868 Some(sqry_core::graph::unified::edge::kind::TypeOfContext::Field),
1869 None,
1870 Some(&field_name),
1871 );
1872
1873 helper.add_reference_edge(field_id, type_id);
1876 }
1877 }
1878
1879 Ok(())
1880}
1881
1882#[allow(clippy::unnecessary_wraps)]
1884fn process_global_variable_declaration(
1885 node: Node,
1886 content: &[u8],
1887 namespace_stack: &[String],
1888 helper: &mut GraphBuildHelper,
1889) -> GraphResult<()> {
1890 if node.kind() != "declaration" {
1892 return Ok(());
1893 }
1894
1895 let mut cursor_check = node.walk();
1898 for child in node.children(&mut cursor_check) {
1899 if child.kind() == "function_declarator" {
1900 return Ok(());
1901 }
1902 }
1903
1904 let mut type_text = None;
1906 let mut var_names = Vec::new();
1907
1908 let mut cursor = node.walk();
1909 for child in node.children(&mut cursor) {
1910 match child.kind() {
1911 "type_identifier" | "primitive_type" | "qualified_identifier" | "template_type" => {
1912 if let Ok(text) = child.utf8_text(content) {
1913 type_text = Some(text.to_string());
1914 }
1915 }
1916 "init_declarator" => {
1917 if let Some(declarator) = child.child_by_field_name("declarator")
1919 && let Some(name) = extract_declarator_name(declarator, content)
1920 {
1921 var_names.push(name);
1922 }
1923 }
1924 "pointer_declarator" | "reference_declarator" => {
1925 if let Some(name) = extract_declarator_name(child, content) {
1926 var_names.push(name);
1927 }
1928 }
1929 "identifier" => {
1930 if let Ok(name) = child.utf8_text(content) {
1932 var_names.push(name.to_string());
1933 }
1934 }
1935 _ => {}
1936 }
1937 }
1938
1939 if let Some(type_text) = type_text {
1940 let base_type = strip_type_qualifiers(&type_text);
1941
1942 for var_name in var_names {
1943 let qualified = if namespace_stack.is_empty() {
1945 var_name.clone()
1946 } else {
1947 format!("{}::{}", namespace_stack.join("::"), var_name)
1948 };
1949
1950 let span = span_from_node(node);
1951
1952 let var_id = helper.add_node_with_visibility(
1954 &qualified,
1955 Some(span),
1956 sqry_core::graph::unified::node::NodeKind::Variable,
1957 Some("public"),
1958 );
1959 helper.mark_definition(var_id);
1961
1962 let type_id = helper.add_type(&base_type, None);
1964
1965 helper.add_typeof_edge(var_id, type_id);
1967 helper.add_reference_edge(var_id, type_id);
1968 }
1969 }
1970
1971 Ok(())
1972}
1973
1974fn extract_declarator_name(node: Node, content: &[u8]) -> Option<String> {
1976 match node.kind() {
1977 "identifier" => {
1978 if let Ok(name) = node.utf8_text(content) {
1979 Some(name.to_string())
1980 } else {
1981 None
1982 }
1983 }
1984 "pointer_declarator" | "reference_declarator" | "array_declarator" => {
1985 if let Some(inner) = node.child_by_field_name("declarator") {
1987 extract_declarator_name(inner, content)
1988 } else {
1989 let mut cursor = node.walk();
1991 for child in node.children(&mut cursor) {
1992 if child.kind() == "identifier"
1993 && let Ok(name) = child.utf8_text(content)
1994 {
1995 return Some(name.to_string());
1996 }
1997 }
1998 None
1999 }
2000 }
2001 "init_declarator" => {
2002 if let Some(inner) = node.child_by_field_name("declarator") {
2004 extract_declarator_name(inner, content)
2005 } else {
2006 None
2007 }
2008 }
2009 "field_declarator" => {
2010 if let Some(inner) = node.child_by_field_name("declarator") {
2012 extract_declarator_name(inner, content)
2013 } else {
2014 if let Ok(name) = node.utf8_text(content) {
2016 Some(name.to_string())
2017 } else {
2018 None
2019 }
2020 }
2021 }
2022 _ => None,
2023 }
2024}
2025
2026#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
2028fn walk_class_body(
2029 body_node: Node,
2030 content: &[u8],
2031 class_qualified_name: &str,
2032 is_struct: bool,
2033 ast_graph: &ASTGraph,
2034 helper: &mut GraphBuildHelper,
2035 seen_includes: &mut HashSet<String>,
2036 namespace_stack: &mut Vec<String>,
2037 class_stack: &mut Vec<String>,
2038 ffi_registry: &FfiRegistry,
2039 pure_virtual_registry: &PureVirtualRegistry,
2040 budget: &mut BuildBudget,
2041) -> GraphResult<()> {
2042 let mut current_visibility = if is_struct { "public" } else { "private" };
2044
2045 let mut cursor = body_node.walk();
2046 for child in body_node.children(&mut cursor) {
2047 budget.checkpoint("cpp:walk_class_body")?;
2048 match child.kind() {
2049 "access_specifier" => {
2050 if let Ok(text) = child.utf8_text(content) {
2052 let spec = text.trim().trim_end_matches(':').trim();
2053 current_visibility = spec;
2054 }
2055 }
2056 "field_declaration" => {
2057 let mut handled_nested = false;
2073 let mut inner_cursor = child.walk();
2074 for inner in child.children(&mut inner_cursor) {
2075 let kind = inner.kind();
2076 if !matches!(
2077 kind,
2078 "class_specifier"
2079 | "struct_specifier"
2080 | "union_specifier"
2081 | "enum_specifier"
2082 ) {
2083 continue;
2084 }
2085
2086 let is_struct_or_union = matches!(kind, "struct_specifier" | "union_specifier");
2087
2088 if let Some(name_node) = inner.child_by_field_name("name") {
2089 if let Ok(inner_name) = name_node.utf8_text(content) {
2101 let inner_name = inner_name.trim();
2102 let nested_qualified = format!("{class_qualified_name}::{inner_name}");
2103 let nested_span = span_from_node(inner);
2104
2105 if kind == "enum_specifier" {
2109 helper.add_enum_with_visibility(
2113 &nested_qualified,
2114 Some(nested_span),
2115 Some(current_visibility),
2116 );
2117 } else {
2118 let nested_id = if is_struct_or_union {
2119 helper.add_struct_with_visibility(
2120 &nested_qualified,
2121 Some(nested_span),
2122 Some(current_visibility),
2123 )
2124 } else {
2125 helper.add_class_with_visibility(
2126 &nested_qualified,
2127 Some(nested_span),
2128 Some(current_visibility),
2129 )
2130 };
2131 build_inheritance_and_implements_edges(
2132 inner,
2133 content,
2134 &nested_qualified,
2135 nested_id,
2136 helper,
2137 namespace_stack,
2138 pure_virtual_registry,
2139 )?;
2140 }
2141
2142 if matches!(
2147 kind,
2148 "class_specifier" | "struct_specifier" | "union_specifier"
2149 ) && let Some(body) = inner.child_by_field_name("body")
2150 {
2151 walk_class_body(
2152 body,
2153 content,
2154 &nested_qualified,
2155 is_struct_or_union,
2156 ast_graph,
2157 helper,
2158 seen_includes,
2159 namespace_stack,
2160 class_stack,
2161 ffi_registry,
2162 pure_virtual_registry,
2163 budget,
2164 )?;
2165 }
2166 handled_nested = true;
2167 }
2168 } else if let Some(body) = inner.child_by_field_name("body") {
2169 let mut anon_cursor = body.walk();
2174 for anon_child in body.children(&mut anon_cursor) {
2175 if anon_child.kind() == "field_declaration" {
2176 process_field_declaration(
2177 anon_child,
2178 content,
2179 class_qualified_name,
2180 current_visibility,
2181 helper,
2182 )?;
2183 }
2184 }
2185 handled_nested = true;
2186 }
2187 }
2188
2189 let _ = handled_nested;
2202 process_field_declaration(
2203 child,
2204 content,
2205 class_qualified_name,
2206 current_visibility,
2207 helper,
2208 )?;
2209 }
2210 "function_definition" => {
2211 if let Some(context) = ast_graph.context_for_start(child.start_byte()) {
2214 let span = span_from_node(child);
2215 helper.add_method_with_signature(
2216 &context.qualified_name,
2217 Some(span),
2218 false, context.is_static,
2220 Some(current_visibility),
2221 context.return_type.as_deref(),
2222 );
2223 }
2224 walk_tree_for_graph(
2226 child,
2227 content,
2228 ast_graph,
2229 helper,
2230 seen_includes,
2231 namespace_stack,
2232 class_stack,
2233 ffi_registry,
2234 pure_virtual_registry,
2235 budget,
2236 )?;
2237 }
2238 _ => {
2239 walk_tree_for_graph(
2241 child,
2242 content,
2243 ast_graph,
2244 helper,
2245 seen_includes,
2246 namespace_stack,
2247 class_stack,
2248 ffi_registry,
2249 pure_virtual_registry,
2250 budget,
2251 )?;
2252 }
2253 }
2254 }
2255
2256 Ok(())
2257}
2258
2259#[allow(clippy::too_many_arguments)]
2261#[allow(clippy::too_many_lines)] fn walk_tree_for_graph(
2263 node: Node,
2264 content: &[u8],
2265 ast_graph: &ASTGraph,
2266 helper: &mut GraphBuildHelper,
2267 seen_includes: &mut HashSet<String>,
2268 namespace_stack: &mut Vec<String>,
2269 class_stack: &mut Vec<String>,
2270 ffi_registry: &FfiRegistry,
2271 pure_virtual_registry: &PureVirtualRegistry,
2272 budget: &mut BuildBudget,
2273) -> GraphResult<()> {
2274 budget.checkpoint("cpp:walk_tree_for_graph")?;
2275 match node.kind() {
2276 "preproc_include" => {
2277 build_import_edge(node, content, helper, seen_includes)?;
2279 }
2280 "linkage_specification" => {
2281 build_ffi_block_for_staging(node, content, helper, namespace_stack);
2283 }
2284 "namespace_definition" => {
2285 if let Some(name_node) = node.child_by_field_name("name")
2287 && let Ok(ns_name) = name_node.utf8_text(content)
2288 {
2289 namespace_stack.push(ns_name.trim().to_string());
2290
2291 let mut cursor = node.walk();
2293 for child in node.children(&mut cursor) {
2294 walk_tree_for_graph(
2295 child,
2296 content,
2297 ast_graph,
2298 helper,
2299 seen_includes,
2300 namespace_stack,
2301 class_stack,
2302 ffi_registry,
2303 pure_virtual_registry,
2304 budget,
2305 )?;
2306 }
2307
2308 namespace_stack.pop();
2309 return Ok(());
2310 }
2311 }
2312 "class_specifier" | "struct_specifier" | "union_specifier" => {
2313 if let Some(name_node) = node.child_by_field_name("name")
2315 && let Ok(class_name) = name_node.utf8_text(content)
2316 {
2317 let class_name = class_name.trim();
2318 let span = span_from_node(node);
2319 let is_struct = matches!(node.kind(), "struct_specifier" | "union_specifier");
2322
2323 let qualified_class =
2325 build_qualified_name(namespace_stack, class_stack, class_name);
2326
2327 let visibility = "public";
2329 let class_id = if is_struct {
2330 helper.add_struct_with_visibility(
2331 &qualified_class,
2332 Some(span),
2333 Some(visibility),
2334 )
2335 } else {
2336 helper.add_class_with_visibility(&qualified_class, Some(span), Some(visibility))
2337 };
2338
2339 build_inheritance_and_implements_edges(
2342 node,
2343 content,
2344 &qualified_class,
2345 class_id,
2346 helper,
2347 namespace_stack,
2348 pure_virtual_registry,
2349 )?;
2350
2351 if class_stack.is_empty() {
2354 let module_id = helper.add_module(FILE_MODULE_NAME, None);
2355 helper.add_export_edge(module_id, class_id);
2356 }
2357
2358 class_stack.push(class_name.to_string());
2360
2361 if let Some(body) = node.child_by_field_name("body") {
2364 walk_class_body(
2365 body,
2366 content,
2367 &qualified_class,
2368 is_struct,
2369 ast_graph,
2370 helper,
2371 seen_includes,
2372 namespace_stack,
2373 class_stack,
2374 ffi_registry,
2375 pure_virtual_registry,
2376 budget,
2377 )?;
2378 }
2379
2380 class_stack.pop();
2381 return Ok(());
2382 }
2383 }
2384 "enum_specifier" => {
2385 if let Some(name_node) = node.child_by_field_name("name")
2386 && let Ok(enum_name) = name_node.utf8_text(content)
2387 {
2388 let enum_name = enum_name.trim();
2389 let span = span_from_node(node);
2390 let qualified_enum = build_qualified_name(namespace_stack, class_stack, enum_name);
2391 let enum_id = helper.add_enum(&qualified_enum, Some(span));
2392
2393 if class_stack.is_empty() {
2394 let module_id = helper.add_module(FILE_MODULE_NAME, None);
2395 helper.add_export_edge(module_id, enum_id);
2396 }
2397 }
2398 }
2399 "function_definition" => {
2400 if !class_stack.is_empty() {
2404 let mut cursor = node.walk();
2407 for child in node.children(&mut cursor) {
2408 walk_tree_for_graph(
2409 child,
2410 content,
2411 ast_graph,
2412 helper,
2413 seen_includes,
2414 namespace_stack,
2415 class_stack,
2416 ffi_registry,
2417 pure_virtual_registry,
2418 budget,
2419 )?;
2420 }
2421 return Ok(());
2422 }
2423
2424 if let Some(context) = ast_graph.context_for_start(node.start_byte()) {
2426 let span = span_from_node(node);
2427
2428 if context.class_stack.is_empty() {
2430 let visibility = if context.is_static {
2433 "private"
2434 } else {
2435 "public"
2436 };
2437 let fn_id = helper.add_function_with_signature(
2438 &context.qualified_name,
2439 Some(span),
2440 false, false, Some(visibility),
2443 context.return_type.as_deref(),
2444 );
2445
2446 if !context.is_static {
2448 let module_id = helper.add_module(FILE_MODULE_NAME, None);
2449 helper.add_export_edge(module_id, fn_id);
2450 }
2451 } else {
2452 helper.add_method_with_signature(
2457 &context.qualified_name,
2458 Some(span),
2459 false, context.is_static,
2461 Some("public"), context.return_type.as_deref(),
2463 );
2464 }
2465 }
2466 }
2467 "call_expression" => {
2468 if let Ok(Some((caller_qname, callee_qname, argument_count, span))) =
2470 build_call_for_staging(ast_graph, node, content)
2471 {
2472 let caller_function_id =
2474 helper.ensure_callee(&caller_qname, span, CalleeKindHint::Function);
2475 let argument_count = u8::try_from(argument_count).unwrap_or(u8::MAX);
2476
2477 let is_unqualified = !callee_qname.contains("::");
2480 if is_unqualified {
2481 if let Some((ffi_qualified, ffi_convention)) = ffi_registry.get(&callee_qname) {
2482 let ffi_target_id =
2484 helper.ensure_callee(ffi_qualified, span, CalleeKindHint::Function);
2485 helper.add_ffi_edge(caller_function_id, ffi_target_id, *ffi_convention);
2486 } else {
2487 let target_function_id =
2489 helper.ensure_callee(&callee_qname, span, CalleeKindHint::Function);
2490 helper.add_call_edge_full_with_span(
2491 caller_function_id,
2492 target_function_id,
2493 argument_count,
2494 false,
2495 vec![span],
2496 );
2497 }
2498 } else {
2499 let target_function_id =
2501 helper.ensure_callee(&callee_qname, span, CalleeKindHint::Function);
2502 helper.add_call_edge_full_with_span(
2503 caller_function_id,
2504 target_function_id,
2505 argument_count,
2506 false,
2507 vec![span],
2508 );
2509 }
2510 }
2511 }
2512 "declaration" => {
2513 if class_stack.is_empty() {
2516 process_global_variable_declaration(node, content, namespace_stack, helper)?;
2517 }
2518 }
2519 _ => {}
2520 }
2521
2522 let mut cursor = node.walk();
2524 for child in node.children(&mut cursor) {
2525 walk_tree_for_graph(
2526 child,
2527 content,
2528 ast_graph,
2529 helper,
2530 seen_includes,
2531 namespace_stack,
2532 class_stack,
2533 ffi_registry,
2534 pure_virtual_registry,
2535 budget,
2536 )?;
2537 }
2538
2539 Ok(())
2540}
2541
2542fn build_call_for_staging(
2544 ast_graph: &ASTGraph,
2545 call_node: Node<'_>,
2546 content: &[u8],
2547) -> GraphResult<Option<(String, String, usize, Span)>> {
2548 let call_context = ast_graph.find_enclosing(call_node.start_byte());
2550 let caller_qualified_name = if let Some(ctx) = call_context {
2551 ctx.qualified_name.clone()
2552 } else {
2553 return Ok(None);
2555 };
2556
2557 let Some(function_node) = call_node.child_by_field_name("function") else {
2558 return Ok(None);
2559 };
2560
2561 let callee_text = function_node
2562 .utf8_text(content)
2563 .map_err(|_| GraphBuilderError::ParseError {
2564 span: span_from_node(call_node),
2565 reason: "failed to read call expression".to_string(),
2566 })?
2567 .trim();
2568
2569 if callee_text.is_empty() {
2570 return Ok(None);
2571 }
2572
2573 let target_qualified_name = if let Some(ctx) = call_context {
2575 resolve_callee_name(function_node, callee_text, ctx, ast_graph, content)
2576 } else {
2577 callee_text.to_string()
2578 };
2579
2580 let span = span_from_node(call_node);
2581 let argument_count = count_arguments(call_node);
2582
2583 Ok(Some((
2584 caller_qualified_name,
2585 target_qualified_name,
2586 argument_count,
2587 span,
2588 )))
2589}
2590
2591fn build_import_edge(
2598 include_node: Node<'_>,
2599 content: &[u8],
2600 helper: &mut GraphBuildHelper,
2601 seen_includes: &mut HashSet<String>,
2602) -> GraphResult<()> {
2603 let path_node = include_node.child_by_field_name("path").or_else(|| {
2605 let mut cursor = include_node.walk();
2607 include_node.children(&mut cursor).find(|child| {
2608 matches!(
2609 child.kind(),
2610 "system_lib_string" | "string_literal" | "string_content"
2611 )
2612 })
2613 });
2614
2615 let Some(path_node) = path_node else {
2616 return Ok(());
2617 };
2618
2619 let include_path = path_node
2620 .utf8_text(content)
2621 .map_err(|_| GraphBuilderError::ParseError {
2622 span: span_from_node(include_node),
2623 reason: "failed to read include path".to_string(),
2624 })?
2625 .trim();
2626
2627 if include_path.is_empty() {
2628 return Ok(());
2629 }
2630
2631 let is_system_include = include_path.starts_with('<') && include_path.ends_with('>');
2633 let cleaned_path = if is_system_include {
2634 include_path.trim_start_matches('<').trim_end_matches('>')
2636 } else {
2637 include_path.trim_start_matches('"').trim_end_matches('"')
2639 };
2640
2641 if cleaned_path.is_empty() {
2642 return Ok(());
2643 }
2644
2645 if !seen_includes.insert(cleaned_path.to_string()) {
2647 return Ok(()); }
2649
2650 let file_module_id = helper.add_module("<file>", None);
2652
2653 let span = span_from_node(include_node);
2655 let import_id = helper.add_import(cleaned_path, Some(span));
2656
2657 helper.add_import_edge(file_module_id, import_id);
2660
2661 Ok(())
2662}
2663
2664fn collect_ffi_declarations(
2675 node: Node<'_>,
2676 content: &[u8],
2677 ffi_registry: &mut FfiRegistry,
2678 budget: &mut BuildBudget,
2679) -> GraphResult<()> {
2680 budget.checkpoint("cpp:collect_ffi_declarations")?;
2681 if node.kind() == "linkage_specification" {
2682 let abi = extract_ffi_abi(node, content);
2684 let convention = abi_to_convention(&abi);
2685
2686 if let Some(body_node) = node.child_by_field_name("body") {
2688 collect_ffi_from_body(body_node, content, &abi, convention, ffi_registry);
2689 }
2690 }
2691
2692 let mut cursor = node.walk();
2694 for child in node.children(&mut cursor) {
2695 collect_ffi_declarations(child, content, ffi_registry, budget)?;
2696 }
2697
2698 Ok(())
2699}
2700
2701fn collect_ffi_from_body(
2703 body_node: Node<'_>,
2704 content: &[u8],
2705 abi: &str,
2706 convention: FfiConvention,
2707 ffi_registry: &mut FfiRegistry,
2708) {
2709 match body_node.kind() {
2710 "declaration_list" => {
2711 let mut cursor = body_node.walk();
2713 for decl in body_node.children(&mut cursor) {
2714 if decl.kind() == "declaration"
2715 && let Some(fn_name) = extract_ffi_function_name(decl, content)
2716 {
2717 let qualified = format!("extern::{abi}::{fn_name}");
2718 ffi_registry.insert(fn_name, (qualified, convention));
2719 }
2720 }
2721 }
2722 "declaration" => {
2723 if let Some(fn_name) = extract_ffi_function_name(body_node, content) {
2725 let qualified = format!("extern::{abi}::{fn_name}");
2726 ffi_registry.insert(fn_name, (qualified, convention));
2727 }
2728 }
2729 _ => {}
2730 }
2731}
2732
2733fn extract_ffi_function_name(decl_node: Node<'_>, content: &[u8]) -> Option<String> {
2735 if let Some(declarator_node) = decl_node.child_by_field_name("declarator") {
2737 return extract_function_name_from_declarator(declarator_node, content);
2738 }
2739 None
2740}
2741
2742fn extract_function_name_from_declarator(node: Node<'_>, content: &[u8]) -> Option<String> {
2744 match node.kind() {
2745 "function_declarator" => {
2746 if let Some(inner) = node.child_by_field_name("declarator") {
2748 return extract_function_name_from_declarator(inner, content);
2749 }
2750 }
2751 "identifier" => {
2752 if let Ok(name) = node.utf8_text(content) {
2754 let name = name.trim();
2755 if !name.is_empty() {
2756 return Some(name.to_string());
2757 }
2758 }
2759 }
2760 "pointer_declarator" | "reference_declarator" => {
2761 if let Some(inner) = node.child_by_field_name("declarator") {
2763 return extract_function_name_from_declarator(inner, content);
2764 }
2765 }
2766 "parenthesized_declarator" => {
2767 let mut cursor = node.walk();
2769 for child in node.children(&mut cursor) {
2770 if let Some(name) = extract_function_name_from_declarator(child, content) {
2771 return Some(name);
2772 }
2773 }
2774 }
2775 _ => {}
2776 }
2777 None
2778}
2779
2780fn extract_ffi_abi(node: Node<'_>, content: &[u8]) -> String {
2784 if let Some(value_node) = node.child_by_field_name("value")
2786 && value_node.kind() == "string_literal"
2787 {
2788 let mut cursor = value_node.walk();
2790 for child in value_node.children(&mut cursor) {
2791 if child.kind() == "string_content"
2792 && let Ok(text) = child.utf8_text(content)
2793 {
2794 let trimmed = text.trim();
2795 if !trimmed.is_empty() {
2796 return trimmed.to_string();
2797 }
2798 }
2799 }
2800 }
2801 "C".to_string()
2803}
2804
2805fn abi_to_convention(abi: &str) -> FfiConvention {
2807 match abi.to_lowercase().as_str() {
2808 "system" => FfiConvention::System,
2809 "stdcall" => FfiConvention::Stdcall,
2810 "fastcall" => FfiConvention::Fastcall,
2811 "cdecl" => FfiConvention::Cdecl,
2812 _ => FfiConvention::C, }
2814}
2815
2816fn build_ffi_block_for_staging(
2820 node: Node<'_>,
2821 content: &[u8],
2822 helper: &mut GraphBuildHelper,
2823 namespace_stack: &[String],
2824) {
2825 let abi = extract_ffi_abi(node, content);
2827
2828 if let Some(body_node) = node.child_by_field_name("body") {
2830 build_ffi_from_body(body_node, content, &abi, helper, namespace_stack);
2831 }
2832}
2833
2834fn build_ffi_from_body(
2836 body_node: Node<'_>,
2837 content: &[u8],
2838 abi: &str,
2839 helper: &mut GraphBuildHelper,
2840 namespace_stack: &[String],
2841) {
2842 match body_node.kind() {
2843 "declaration_list" => {
2844 let mut cursor = body_node.walk();
2846 for decl in body_node.children(&mut cursor) {
2847 if decl.kind() == "declaration"
2848 && let Some(fn_name) = extract_ffi_function_name(decl, content)
2849 {
2850 let span = span_from_node(decl);
2851 let qualified = if namespace_stack.is_empty() {
2853 format!("extern::{abi}::{fn_name}")
2854 } else {
2855 format!("{}::extern::{abi}::{fn_name}", namespace_stack.join("::"))
2856 };
2857 helper.add_function(
2859 &qualified,
2860 Some(span),
2861 false, true, );
2864 }
2865 }
2866 }
2867 "declaration" => {
2868 if let Some(fn_name) = extract_ffi_function_name(body_node, content) {
2870 let span = span_from_node(body_node);
2871 let qualified = if namespace_stack.is_empty() {
2872 format!("extern::{abi}::{fn_name}")
2873 } else {
2874 format!("{}::extern::{abi}::{fn_name}", namespace_stack.join("::"))
2875 };
2876 helper.add_function(&qualified, Some(span), false, true);
2877 }
2878 }
2879 _ => {}
2880 }
2881}
2882
2883fn collect_pure_virtual_interfaces(
2893 node: Node<'_>,
2894 content: &[u8],
2895 registry: &mut PureVirtualRegistry,
2896 budget: &mut BuildBudget,
2897) -> GraphResult<()> {
2898 budget.checkpoint("cpp:collect_pure_virtual_interfaces")?;
2899 if matches!(node.kind(), "class_specifier" | "struct_specifier")
2900 && let Some(name_node) = node.child_by_field_name("name")
2901 && let Ok(class_name) = name_node.utf8_text(content)
2902 {
2903 let class_name = class_name.trim();
2904 if !class_name.is_empty() && has_pure_virtual_methods(node, content) {
2905 registry.insert(class_name.to_string());
2906 }
2907 }
2908
2909 let mut cursor = node.walk();
2911 for child in node.children(&mut cursor) {
2912 collect_pure_virtual_interfaces(child, content, registry, budget)?;
2913 }
2914
2915 Ok(())
2916}
2917
2918fn has_pure_virtual_methods(class_node: Node<'_>, content: &[u8]) -> bool {
2922 if let Some(body) = class_node.child_by_field_name("body") {
2923 let mut cursor = body.walk();
2924 for child in body.children(&mut cursor) {
2925 if child.kind() == "field_declaration" && is_pure_virtual_declaration(child, content) {
2927 return true;
2928 }
2929 }
2930 }
2931 false
2932}
2933
2934fn is_pure_virtual_declaration(decl_node: Node<'_>, content: &[u8]) -> bool {
2936 let mut has_virtual = false;
2937 let mut has_pure_specifier = false;
2938
2939 let mut cursor = decl_node.walk();
2941 for child in decl_node.children(&mut cursor) {
2942 match child.kind() {
2943 "virtual" => {
2944 has_virtual = true;
2945 }
2946 "number_literal" => {
2947 if let Ok(text) = child.utf8_text(content)
2950 && text.trim() == "0"
2951 {
2952 has_pure_specifier = true;
2953 }
2954 }
2955 _ => {}
2956 }
2957 }
2958
2959 has_virtual && has_pure_specifier
2960}
2961
2962fn build_inheritance_and_implements_edges(
2968 class_node: Node<'_>,
2969 content: &[u8],
2970 _qualified_class_name: &str,
2971 child_id: sqry_core::graph::unified::node::NodeId,
2972 helper: &mut GraphBuildHelper,
2973 namespace_stack: &[String],
2974 pure_virtual_registry: &PureVirtualRegistry,
2975) -> GraphResult<()> {
2976 let mut cursor = class_node.walk();
2978 let base_clause = class_node
2979 .children(&mut cursor)
2980 .find(|child| child.kind() == "base_class_clause");
2981
2982 let Some(base_clause) = base_clause else {
2983 return Ok(()); };
2985
2986 let mut clause_cursor = base_clause.walk();
2988 for child in base_clause.children(&mut clause_cursor) {
2989 match child.kind() {
2990 "type_identifier" => {
2991 let base_name = child
2992 .utf8_text(content)
2993 .map_err(|_| GraphBuilderError::ParseError {
2994 span: span_from_node(child),
2995 reason: "failed to read base class name".to_string(),
2996 })?
2997 .trim();
2998
2999 if !base_name.is_empty() {
3000 let qualified_base = if namespace_stack.is_empty() {
3002 base_name.to_string()
3003 } else {
3004 format!("{}::{}", namespace_stack.join("::"), base_name)
3005 };
3006
3007 if pure_virtual_registry.contains(base_name) {
3009 let interface_id = helper.add_interface(&qualified_base, None);
3011 helper.add_implements_edge(child_id, interface_id);
3012 } else {
3013 let parent_id = helper.add_class(&qualified_base, None);
3015 helper.add_inherits_edge(child_id, parent_id);
3016 }
3017 }
3018 }
3019 "qualified_identifier" => {
3020 let base_name = child
3022 .utf8_text(content)
3023 .map_err(|_| GraphBuilderError::ParseError {
3024 span: span_from_node(child),
3025 reason: "failed to read base class name".to_string(),
3026 })?
3027 .trim();
3028
3029 if !base_name.is_empty() {
3030 let simple_name = base_name.rsplit("::").next().unwrap_or(base_name);
3032
3033 if pure_virtual_registry.contains(simple_name) {
3034 let interface_id = helper.add_interface(base_name, None);
3035 helper.add_implements_edge(child_id, interface_id);
3036 } else {
3037 let parent_id = helper.add_class(base_name, None);
3038 helper.add_inherits_edge(child_id, parent_id);
3039 }
3040 }
3041 }
3042 "template_type" => {
3043 if let Some(template_name_node) = child.child_by_field_name("name")
3045 && let Ok(base_name) = template_name_node.utf8_text(content)
3046 {
3047 let base_name = base_name.trim();
3048 if !base_name.is_empty() {
3049 let qualified_base =
3050 if base_name.contains("::") || namespace_stack.is_empty() {
3051 base_name.to_string()
3052 } else {
3053 format!("{}::{}", namespace_stack.join("::"), base_name)
3054 };
3055
3056 if pure_virtual_registry.contains(base_name) {
3059 let interface_id = helper.add_interface(&qualified_base, None);
3060 helper.add_implements_edge(child_id, interface_id);
3061 } else {
3062 let parent_id = helper.add_class(&qualified_base, None);
3063 helper.add_inherits_edge(child_id, parent_id);
3064 }
3065 }
3066 }
3067 }
3068 _ => {
3069 }
3071 }
3072 }
3073
3074 Ok(())
3075}
3076
3077fn span_from_node(node: Node<'_>) -> Span {
3078 let start = node.start_position();
3079 let end = node.end_position();
3080 Span::new(
3081 sqry_core::graph::node::Position::new(start.row, start.column),
3082 sqry_core::graph::node::Position::new(end.row, end.column),
3083 )
3084}
3085
3086fn count_arguments(node: Node<'_>) -> usize {
3087 node.child_by_field_name("arguments").map_or(0, |args| {
3088 let mut count = 0;
3089 let mut cursor = args.walk();
3090 for child in args.children(&mut cursor) {
3091 if !matches!(child.kind(), "(" | ")" | ",") {
3092 count += 1;
3093 }
3094 }
3095 count
3096 })
3097}
3098
3099#[cfg(test)]
3100mod tests {
3101 use super::*;
3102 use sqry_core::graph::unified::build::test_helpers::{
3103 assert_has_ffi_call_edge, assert_has_node, assert_has_node_with_kind,
3104 assert_has_node_with_kind_exact, collect_call_edges,
3105 };
3106 use sqry_core::graph::unified::node::NodeKind;
3107 use tree_sitter::Parser;
3108
3109 fn parse_cpp(source: &str) -> Tree {
3110 let mut parser = Parser::new();
3111 parser
3112 .set_language(&tree_sitter_cpp::LANGUAGE.into())
3113 .expect("Failed to set Cpp language");
3114 parser
3115 .parse(source.as_bytes(), None)
3116 .expect("Failed to parse Cpp source")
3117 }
3118
3119 fn test_budget() -> BuildBudget {
3120 BuildBudget::new(Path::new("test.cpp"))
3121 }
3122
3123 fn extract_namespace_map_for_test(
3124 tree: &Tree,
3125 source: &str,
3126 ) -> HashMap<std::ops::Range<usize>, String> {
3127 let mut budget = test_budget();
3128 extract_namespace_map(tree.root_node(), source.as_bytes(), &mut budget)
3129 .expect("namespace extraction should succeed in tests")
3130 }
3131
3132 fn extract_cpp_contexts_for_test(
3133 tree: &Tree,
3134 source: &str,
3135 namespace_map: &HashMap<std::ops::Range<usize>, String>,
3136 ) -> Vec<FunctionContext> {
3137 let mut budget = test_budget();
3138 extract_cpp_contexts(
3139 tree.root_node(),
3140 source.as_bytes(),
3141 namespace_map,
3142 &mut budget,
3143 )
3144 .expect("context extraction should succeed in tests")
3145 }
3146
3147 fn extract_field_and_type_info_for_test(
3148 tree: &Tree,
3149 source: &str,
3150 namespace_map: &HashMap<std::ops::Range<usize>, String>,
3151 ) -> (QualifiedNameMap, QualifiedNameMap) {
3152 let mut budget = test_budget();
3153 extract_field_and_type_info(
3154 tree.root_node(),
3155 source.as_bytes(),
3156 namespace_map,
3157 &mut budget,
3158 )
3159 .expect("field/type extraction should succeed in tests")
3160 }
3161
3162 #[test]
3163 fn test_build_graph_times_out_with_expired_budget() {
3164 let source = r"
3165 namespace demo {
3166 class Service {
3167 public:
3168 void process() {}
3169 };
3170 }
3171 ";
3172 let tree = parse_cpp(source);
3173 let builder = CppGraphBuilder::new();
3174 let mut staging = StagingGraph::new();
3175 let mut budget = BuildBudget::already_expired(Path::new("timeout.cpp"));
3176
3177 let err = builder
3178 .build_graph_with_budget(
3179 &tree,
3180 source.as_bytes(),
3181 Path::new("timeout.cpp"),
3182 &mut staging,
3183 &mut budget,
3184 )
3185 .expect_err("expired budget should force timeout");
3186
3187 match err {
3188 GraphBuilderError::BuildTimedOut {
3189 file,
3190 phase,
3191 timeout_ms,
3192 } => {
3193 assert_eq!(file, PathBuf::from("timeout.cpp"));
3194 assert_eq!(phase, "cpp:extract_namespace_map");
3195 assert_eq!(timeout_ms, 1_000);
3196 }
3197 other => panic!("expected BuildTimedOut, got {other:?}"),
3198 }
3199 }
3200
3201 #[test]
3202 fn test_extract_class() {
3203 let source = "class User { }";
3204 let tree = parse_cpp(source);
3205 let mut staging = StagingGraph::new();
3206 let builder = CppGraphBuilder::new();
3207
3208 let result = builder.build_graph(
3209 &tree,
3210 source.as_bytes(),
3211 Path::new("test.cpp"),
3212 &mut staging,
3213 );
3214
3215 assert!(result.is_ok());
3216 assert_has_node_with_kind(&staging, "User", NodeKind::Class);
3217 }
3218
3219 #[test]
3220 fn test_extract_template_class() {
3221 let source = r"
3222 template <typename T>
3223 class Person {
3224 public:
3225 T name;
3226 T age;
3227 };
3228 ";
3229 let tree = parse_cpp(source);
3230 let mut staging = StagingGraph::new();
3231 let builder = CppGraphBuilder::new();
3232
3233 let result = builder.build_graph(
3234 &tree,
3235 source.as_bytes(),
3236 Path::new("test.cpp"),
3237 &mut staging,
3238 );
3239
3240 assert!(result.is_ok());
3241 assert_has_node_with_kind(&staging, "Person", NodeKind::Class);
3242 }
3243
3244 #[test]
3245 fn test_nested_named_types_emit_nodes() {
3246 let source = r"
3251 class Outer {
3252 public:
3253 class Inner { int z; };
3254 struct InnerS { int w; };
3255 union InnerU { int i; float f; };
3256 enum class InnerE { A, B };
3257 class L1 { public: class L2 { int q; }; };
3258 };
3259 namespace ns {
3260 class NsOuter { public: class NsInner { int n; }; };
3261 }
3262 ";
3263 let staging = build_cpp(source);
3264
3265 assert_has_node_with_kind_exact(&staging, "Outer::Inner", NodeKind::Class);
3267 assert_has_node_with_kind_exact(&staging, "Outer::InnerS", NodeKind::Struct);
3268 assert_has_node_with_kind_exact(&staging, "Outer::InnerU", NodeKind::Struct);
3270 assert_has_node_with_kind_exact(&staging, "Outer::InnerE", NodeKind::Enum);
3271 assert_has_node_with_kind_exact(&staging, "Outer::L1", NodeKind::Class);
3273 assert_has_node_with_kind_exact(&staging, "Outer::L1::L2", NodeKind::Class);
3274 assert_has_node_with_kind_exact(&staging, "ns::NsOuter", NodeKind::Class);
3276 assert_has_node_with_kind_exact(&staging, "ns::NsOuter::NsInner", NodeKind::Class);
3277
3278 assert_has_node_with_kind_exact(&staging, "Outer::Inner.z", NodeKind::Property);
3281 assert_has_node_with_kind_exact(&staging, "Outer::L1::L2.q", NodeKind::Property);
3282 assert_has_node_with_kind_exact(&staging, "ns::NsOuter::NsInner.n", NodeKind::Property);
3283 }
3284
3285 #[test]
3286 fn test_nested_enum_carries_enclosing_visibility() {
3287 let source = r"
3291 class Outer {
3292 private:
3293 enum class Secret { A, B };
3294 public:
3295 enum class Pub { X, Y };
3296 };
3297 ";
3298 let staging = build_cpp(source);
3299
3300 let secret = cpp_find_added_node(&staging, "Outer::Secret")
3301 .expect("nested enum Outer::Secret must be staged");
3302 assert_eq!(secret.kind, NodeKind::Enum, "Secret must be an Enum node");
3303 let secret_vis = staging.resolve_local_string(
3304 secret
3305 .visibility
3306 .expect("nested enum must carry a visibility id"),
3307 );
3308 assert_eq!(
3309 secret_vis,
3310 Some("private"),
3311 "nested enum under `private:` must be private"
3312 );
3313
3314 let pub_enum = cpp_find_added_node(&staging, "Outer::Pub")
3315 .expect("nested enum Outer::Pub must be staged");
3316 let pub_vis = staging.resolve_local_string(
3317 pub_enum
3318 .visibility
3319 .expect("nested enum must carry a visibility id"),
3320 );
3321 assert_eq!(
3322 pub_vis,
3323 Some("public"),
3324 "nested enum under `public:` must be public"
3325 );
3326 }
3327
3328 #[test]
3329 fn test_nested_class_emits_inheritance_edge() {
3330 let source = r"
3334 struct Base { virtual ~Base(); };
3335 class Outer {
3336 public:
3337 class Derived : public Base {};
3338 };
3339 ";
3340 let staging = build_cpp(source);
3341
3342 let derived_id = cpp_find_added_node_id(&staging, "Outer::Derived", NodeKind::Class)
3343 .expect("nested Derived class node must be staged");
3344
3345 let has_inherits = staging.operations().iter().any(|op| {
3346 matches!(
3347 op,
3348 StagingOp::AddEdge {
3349 source: src,
3350 kind: EdgeKind::Inherits,
3351 ..
3352 } if *src == derived_id
3353 )
3354 });
3355 assert!(
3356 has_inherits,
3357 "nested Derived must emit an Inherits edge to its base"
3358 );
3359 }
3360
3361 #[test]
3362 fn test_top_level_union_emits_struct_node() {
3363 let source = "union Value { int i; float f; };";
3367 let staging = build_cpp(source);
3368 assert_has_node_with_kind_exact(&staging, "Value", NodeKind::Struct);
3369 }
3370
3371 #[test]
3372 fn test_extract_function() {
3373 let source = r#"
3374 #include <cstdio>
3375 void hello() {
3376 std::printf("Hello");
3377 }
3378 "#;
3379 let tree = parse_cpp(source);
3380 let mut staging = StagingGraph::new();
3381 let builder = CppGraphBuilder::new();
3382
3383 let result = builder.build_graph(
3384 &tree,
3385 source.as_bytes(),
3386 Path::new("test.cpp"),
3387 &mut staging,
3388 );
3389
3390 assert!(result.is_ok());
3391 assert_has_node_with_kind(&staging, "hello", NodeKind::Function);
3392 }
3393
3394 #[test]
3395 fn test_extract_virtual_function() {
3396 let source = r"
3397 class Service {
3398 public:
3399 virtual void fetchData() {}
3400 };
3401 ";
3402 let tree = parse_cpp(source);
3403 let mut staging = StagingGraph::new();
3404 let builder = CppGraphBuilder::new();
3405
3406 let result = builder.build_graph(
3407 &tree,
3408 source.as_bytes(),
3409 Path::new("test.cpp"),
3410 &mut staging,
3411 );
3412
3413 assert!(result.is_ok());
3414 assert_has_node(&staging, "fetchData");
3415 }
3416
3417 #[test]
3418 fn test_extract_call_edge() {
3419 let source = r"
3420 void greet() {}
3421
3422 int main() {
3423 greet();
3424 return 0;
3425 }
3426 ";
3427 let tree = parse_cpp(source);
3428 let mut staging = StagingGraph::new();
3429 let builder = CppGraphBuilder::new();
3430
3431 let result = builder.build_graph(
3432 &tree,
3433 source.as_bytes(),
3434 Path::new("test.cpp"),
3435 &mut staging,
3436 );
3437
3438 assert!(result.is_ok());
3439 assert_has_node(&staging, "main");
3440 assert_has_node(&staging, "greet");
3441 let calls = collect_call_edges(&staging);
3442 assert!(!calls.is_empty());
3443 }
3444
3445 #[test]
3446 fn test_extract_member_call_edge() {
3447 let source = r"
3448 class Service {
3449 public:
3450 void helper() {}
3451 };
3452
3453 int main() {
3454 Service svc;
3455 svc.helper();
3456 return 0;
3457 }
3458 ";
3459 let tree = parse_cpp(source);
3460 let mut staging = StagingGraph::new();
3461 let builder = CppGraphBuilder::new();
3462
3463 let result = builder.build_graph(
3464 &tree,
3465 source.as_bytes(),
3466 Path::new("member.cpp"),
3467 &mut staging,
3468 );
3469
3470 assert!(result.is_ok());
3471 assert_has_node(&staging, "main");
3472 assert_has_node(&staging, "helper");
3473 let calls = collect_call_edges(&staging);
3474 assert!(!calls.is_empty());
3475 }
3476
3477 #[test]
3478 fn test_extract_namespace_map_simple() {
3479 let source = r"
3480 namespace demo {
3481 void func() {}
3482 }
3483 ";
3484 let tree = parse_cpp(source);
3485 let namespace_map = extract_namespace_map_for_test(&tree, source);
3486
3487 assert_eq!(namespace_map.len(), 1);
3489
3490 let (_, ns_prefix) = namespace_map.iter().next().unwrap();
3492 assert_eq!(ns_prefix, "demo::");
3493 }
3494
3495 #[test]
3496 fn test_extract_namespace_map_nested() {
3497 let source = r"
3498 namespace outer {
3499 namespace inner {
3500 void func() {}
3501 }
3502 }
3503 ";
3504 let tree = parse_cpp(source);
3505 let namespace_map = extract_namespace_map_for_test(&tree, source);
3506
3507 assert!(namespace_map.len() >= 2);
3509
3510 let ns_values: Vec<&String> = namespace_map.values().collect();
3512 assert!(ns_values.iter().any(|v| v.as_str() == "outer::"));
3513 assert!(ns_values.iter().any(|v| v.as_str() == "outer::inner::"));
3514 }
3515
3516 #[test]
3517 fn test_extract_namespace_map_multiple() {
3518 let source = r"
3519 namespace first {
3520 void func1() {}
3521 }
3522 namespace second {
3523 void func2() {}
3524 }
3525 ";
3526 let tree = parse_cpp(source);
3527 let namespace_map = extract_namespace_map_for_test(&tree, source);
3528
3529 assert_eq!(namespace_map.len(), 2);
3531
3532 let ns_values: Vec<&String> = namespace_map.values().collect();
3533 assert!(ns_values.iter().any(|v| v.as_str() == "first::"));
3534 assert!(ns_values.iter().any(|v| v.as_str() == "second::"));
3535 }
3536
3537 #[test]
3538 fn test_find_namespace_for_offset() {
3539 let source = r"
3540 namespace demo {
3541 void func() {}
3542 }
3543 ";
3544 let tree = parse_cpp(source);
3545 let namespace_map = extract_namespace_map_for_test(&tree, source);
3546
3547 let func_offset = source.find("func").unwrap();
3549 let ns = find_namespace_for_offset(func_offset, &namespace_map);
3550 assert_eq!(ns, "demo::");
3551
3552 let ns = find_namespace_for_offset(0, &namespace_map);
3554 assert_eq!(ns, "");
3555 }
3556
3557 #[test]
3558 fn test_extract_cpp_contexts_free_function() {
3559 let source = r"
3560 void helper() {}
3561 ";
3562 let tree = parse_cpp(source);
3563 let namespace_map = extract_namespace_map_for_test(&tree, source);
3564 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3565
3566 assert_eq!(contexts.len(), 1);
3567 assert_eq!(contexts[0].qualified_name, "helper");
3568 assert!(!contexts[0].is_static);
3569 assert!(!contexts[0].is_virtual);
3570 }
3571
3572 #[test]
3573 fn test_extract_cpp_contexts_namespace_function() {
3574 let source = r"
3575 namespace demo {
3576 void helper() {}
3577 }
3578 ";
3579 let tree = parse_cpp(source);
3580 let namespace_map = extract_namespace_map_for_test(&tree, source);
3581 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3582
3583 assert_eq!(contexts.len(), 1);
3584 assert_eq!(contexts[0].qualified_name, "demo::helper");
3585 assert_eq!(contexts[0].namespace_stack, vec!["demo"]);
3586 }
3587
3588 #[test]
3589 fn test_extract_cpp_contexts_class_method() {
3590 let source = r"
3591 class Service {
3592 public:
3593 void process() {}
3594 };
3595 ";
3596 let tree = parse_cpp(source);
3597 let namespace_map = extract_namespace_map_for_test(&tree, source);
3598 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3599
3600 assert_eq!(contexts.len(), 1);
3601 assert_eq!(contexts[0].qualified_name, "Service::process");
3602 assert_eq!(contexts[0].class_stack, vec!["Service"]);
3603 }
3604
3605 #[test]
3606 fn test_extract_cpp_contexts_namespace_and_class() {
3607 let source = r"
3608 namespace demo {
3609 class Service {
3610 public:
3611 void process() {}
3612 };
3613 }
3614 ";
3615 let tree = parse_cpp(source);
3616 let namespace_map = extract_namespace_map_for_test(&tree, source);
3617 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3618
3619 assert_eq!(contexts.len(), 1);
3620 assert_eq!(contexts[0].qualified_name, "demo::Service::process");
3621 assert_eq!(contexts[0].namespace_stack, vec!["demo"]);
3622 assert_eq!(contexts[0].class_stack, vec!["Service"]);
3623 }
3624
3625 #[test]
3626 fn test_extract_cpp_contexts_static_method() {
3627 let source = r"
3628 class Repository {
3629 public:
3630 static void save() {}
3631 };
3632 ";
3633 let tree = parse_cpp(source);
3634 let namespace_map = extract_namespace_map_for_test(&tree, source);
3635 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3636
3637 assert_eq!(contexts.len(), 1);
3638 assert_eq!(contexts[0].qualified_name, "Repository::save");
3639 assert!(contexts[0].is_static);
3640 }
3641
3642 #[test]
3643 fn test_extract_cpp_contexts_virtual_method() {
3644 let source = r"
3645 class Base {
3646 public:
3647 virtual void render() {}
3648 };
3649 ";
3650 let tree = parse_cpp(source);
3651 let namespace_map = extract_namespace_map_for_test(&tree, source);
3652 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3653
3654 assert_eq!(contexts.len(), 1);
3655 assert_eq!(contexts[0].qualified_name, "Base::render");
3656 assert!(contexts[0].is_virtual);
3657 }
3658
3659 #[test]
3660 fn test_extract_cpp_contexts_inline_function() {
3661 let source = r"
3662 inline void helper() {}
3663 ";
3664 let tree = parse_cpp(source);
3665 let namespace_map = extract_namespace_map_for_test(&tree, source);
3666 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3667
3668 assert_eq!(contexts.len(), 1);
3669 assert_eq!(contexts[0].qualified_name, "helper");
3670 assert!(contexts[0].is_inline);
3671 }
3672
3673 #[test]
3674 fn test_extract_cpp_contexts_out_of_line_definition() {
3675 let source = r"
3676 namespace demo {
3677 class Service {
3678 public:
3679 int process(int v);
3680 };
3681
3682 inline int Service::process(int v) {
3683 return v;
3684 }
3685 }
3686 ";
3687 let tree = parse_cpp(source);
3688 let namespace_map = extract_namespace_map_for_test(&tree, source);
3689 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3690
3691 assert_eq!(contexts.len(), 1);
3693 assert_eq!(contexts[0].qualified_name, "demo::Service::process");
3694 assert!(contexts[0].is_inline);
3695 }
3696
3697 #[test]
3698 fn test_extract_field_types_simple() {
3699 let source = r"
3700 class Service {
3701 public:
3702 Repository repo;
3703 };
3704 ";
3705 let tree = parse_cpp(source);
3706 let namespace_map = extract_namespace_map_for_test(&tree, source);
3707 let (field_types, _type_map) =
3708 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3709
3710 assert_eq!(field_types.len(), 1);
3712 assert_eq!(
3713 field_types.get(&("Service".to_string(), "repo".to_string())),
3714 Some(&"Repository".to_string())
3715 );
3716 }
3717
3718 #[test]
3719 fn test_extract_field_types_namespace() {
3720 let source = r"
3721 namespace demo {
3722 class Service {
3723 public:
3724 Repository repo;
3725 };
3726 }
3727 ";
3728 let tree = parse_cpp(source);
3729 let namespace_map = extract_namespace_map_for_test(&tree, source);
3730 let (field_types, _type_map) =
3731 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3732
3733 assert_eq!(field_types.len(), 1);
3735 assert_eq!(
3736 field_types.get(&("demo::Service".to_string(), "repo".to_string())),
3737 Some(&"Repository".to_string())
3738 );
3739 }
3740
3741 #[test]
3742 fn test_extract_field_types_no_collision() {
3743 let source = r"
3744 class ServiceA {
3745 public:
3746 Repository repo;
3747 };
3748
3749 class ServiceB {
3750 public:
3751 Repository repo;
3752 };
3753 ";
3754 let tree = parse_cpp(source);
3755 let namespace_map = extract_namespace_map_for_test(&tree, source);
3756 let (field_types, _type_map) =
3757 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3758
3759 assert_eq!(field_types.len(), 2);
3761 assert_eq!(
3762 field_types.get(&("ServiceA".to_string(), "repo".to_string())),
3763 Some(&"Repository".to_string())
3764 );
3765 assert_eq!(
3766 field_types.get(&("ServiceB".to_string(), "repo".to_string())),
3767 Some(&"Repository".to_string())
3768 );
3769 }
3770
3771 #[test]
3772 fn test_extract_using_declaration() {
3773 let source = r"
3774 using std::vector;
3775
3776 class Service {
3777 public:
3778 vector data;
3779 };
3780 ";
3781 let tree = parse_cpp(source);
3782 let namespace_map = extract_namespace_map_for_test(&tree, source);
3783 let (field_types, type_map) =
3784 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3785
3786 assert_eq!(field_types.len(), 1);
3788 assert_eq!(
3789 field_types.get(&("Service".to_string(), "data".to_string())),
3790 Some(&"std::vector".to_string()),
3791 "Field type should resolve 'vector' to 'std::vector' via using declaration"
3792 );
3793
3794 assert_eq!(
3796 type_map.get(&(String::new(), "vector".to_string())),
3797 Some(&"std::vector".to_string()),
3798 "Using declaration should map 'vector' to 'std::vector' in type_map"
3799 );
3800 }
3801
3802 #[test]
3803 fn test_extract_field_types_pointer() {
3804 let source = r"
3805 class Service {
3806 public:
3807 Repository* repo;
3808 };
3809 ";
3810 let tree = parse_cpp(source);
3811 let namespace_map = extract_namespace_map_for_test(&tree, source);
3812 let (field_types, _type_map) =
3813 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3814
3815 assert_eq!(field_types.len(), 1);
3817 assert_eq!(
3818 field_types.get(&("Service".to_string(), "repo".to_string())),
3819 Some(&"Repository".to_string())
3820 );
3821 }
3822
3823 #[test]
3824 fn test_extract_field_types_multiple_declarators() {
3825 let source = r"
3826 class Service {
3827 public:
3828 Repository repo_a, repo_b, repo_c;
3829 };
3830 ";
3831 let tree = parse_cpp(source);
3832 let namespace_map = extract_namespace_map_for_test(&tree, source);
3833 let (field_types, _type_map) =
3834 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3835
3836 assert_eq!(field_types.len(), 3);
3838 assert_eq!(
3839 field_types.get(&("Service".to_string(), "repo_a".to_string())),
3840 Some(&"Repository".to_string())
3841 );
3842 assert_eq!(
3843 field_types.get(&("Service".to_string(), "repo_b".to_string())),
3844 Some(&"Repository".to_string())
3845 );
3846 assert_eq!(
3847 field_types.get(&("Service".to_string(), "repo_c".to_string())),
3848 Some(&"Repository".to_string())
3849 );
3850 }
3851
3852 #[test]
3853 fn test_extract_field_types_nested_struct_with_parent_field() {
3854 let source = r"
3857 namespace demo {
3858 struct Outer {
3859 int outer_field;
3860 struct Inner {
3861 int inner_field;
3862 };
3863 Inner nested_instance;
3864 };
3865 }
3866 ";
3867 let tree = parse_cpp(source);
3868 let namespace_map = extract_namespace_map_for_test(&tree, source);
3869 let (field_types, _type_map) =
3870 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3871
3872 assert!(
3875 field_types.len() >= 2,
3876 "Expected at least outer_field and nested_instance"
3877 );
3878
3879 assert_eq!(
3881 field_types.get(&("demo::Outer".to_string(), "outer_field".to_string())),
3882 Some(&"int".to_string())
3883 );
3884
3885 assert_eq!(
3891 field_types.get(&("demo::Outer".to_string(), "nested_instance".to_string())),
3892 Some(&"demo::Outer::Inner".to_string())
3893 );
3894
3895 if field_types.contains_key(&("demo::Outer::Inner".to_string(), "inner_field".to_string()))
3897 {
3898 assert_eq!(
3900 field_types.get(&("demo::Outer::Inner".to_string(), "inner_field".to_string())),
3901 Some(&"int".to_string()),
3902 "Inner class fields must use parent-qualified FQN 'demo::Outer::Inner'"
3903 );
3904 }
3905 }
3906
3907 use sqry_core::graph::unified::build::staging::StagingOp;
3924 use sqry_core::graph::unified::edge::kind::{EdgeKind, TypeOfContext};
3925
3926 fn cpp_find_added_node<'a>(
3928 staging: &'a StagingGraph,
3929 canonical_name: &str,
3930 ) -> Option<&'a sqry_core::graph::unified::storage::arena::NodeEntry> {
3931 staging.operations().iter().find_map(|op| {
3932 if let StagingOp::AddNode { entry, .. } = op
3933 && staging.resolve_node_canonical_name(entry) == Some(canonical_name)
3934 {
3935 Some(entry)
3936 } else {
3937 None
3938 }
3939 })
3940 }
3941
3942 fn cpp_find_added_node_id(
3944 staging: &StagingGraph,
3945 canonical_name: &str,
3946 kind: NodeKind,
3947 ) -> Option<sqry_core::graph::unified::NodeId> {
3948 staging.operations().iter().find_map(|op| match op {
3949 StagingOp::AddNode {
3950 entry,
3951 expected_id: Some(id),
3952 } if entry.kind == kind
3953 && staging.resolve_node_canonical_name(entry) == Some(canonical_name) =>
3954 {
3955 Some(*id)
3956 }
3957 _ => None,
3958 })
3959 }
3960
3961 fn build_cpp(source: &str) -> StagingGraph {
3963 let tree = parse_cpp(source);
3964 let mut staging = StagingGraph::new();
3965 let builder = CppGraphBuilder::new();
3966 builder
3967 .build_graph(
3968 &tree,
3969 source.as_bytes(),
3970 Path::new("test.cpp"),
3971 &mut staging,
3972 )
3973 .expect("build_graph must succeed for the test fixture");
3974 staging
3975 }
3976
3977 fn staged_node_name_by_id(
3978 staging: &StagingGraph,
3979 id: sqry_core::graph::unified::NodeId,
3980 ) -> Option<&str> {
3981 staging.nodes().find_map(|node| {
3982 if node.expected_id == Some(id) {
3983 staging.resolve_node_canonical_name(node.entry)
3984 } else {
3985 None
3986 }
3987 })
3988 }
3989
3990 fn call_edge_pairs(staging: &StagingGraph) -> Vec<(String, String)> {
3991 staging
3992 .edges()
3993 .filter_map(|edge| {
3994 if matches!(edge.kind, EdgeKind::Calls { .. }) {
3995 let source = staged_node_name_by_id(staging, edge.source)?.to_string();
3996 let target = staged_node_name_by_id(staging, edge.target)?.to_string();
3997 Some((source, target))
3998 } else {
3999 None
4000 }
4001 })
4002 .collect()
4003 }
4004
4005 fn assert_has_call_edge(staging: &StagingGraph, caller: &str, callee: &str) {
4006 let calls = call_edge_pairs(staging);
4007 assert!(
4008 calls
4009 .iter()
4010 .any(|(source, target)| source == caller && target == callee),
4011 "expected Calls edge {caller} -> {callee}; staged Calls edges: {calls:?}"
4012 );
4013 }
4014
4015 fn assert_no_call_target(staging: &StagingGraph, forbidden_target: &str) {
4016 let calls = call_edge_pairs(staging);
4017 assert!(
4018 !calls.iter().any(|(_, target)| target == forbidden_target),
4019 "unexpected Calls edge target {forbidden_target}; staged Calls edges: {calls:?}"
4020 );
4021 }
4022
4023 fn assert_no_call_target_suffix(staging: &StagingGraph, forbidden_suffix: &str) {
4024 let calls = call_edge_pairs(staging);
4025 assert!(
4026 !calls
4027 .iter()
4028 .any(|(_, target)| target.ends_with(forbidden_suffix)),
4029 "unexpected Calls edge target ending with {forbidden_suffix}; staged Calls edges: {calls:?}"
4030 );
4031 }
4032
4033 #[test]
4034 fn test_issue_466_t1_member_call_through_field_resolves_to_method_fqn() {
4035 let source = r"
4036namespace demo {
4037 struct Repository { void save(); };
4038 struct Service { Repository repo; void run() { repo.save(); } };
4039}
4040";
4041 let staging = build_cpp(source);
4042
4043 assert_has_call_edge(&staging, "demo::Service::run", "demo::Repository::save");
4044 assert_no_call_target(&staging, "demo::repo.save");
4045 assert_no_call_target(&staging, "Repository::save");
4046 }
4047
4048 #[test]
4049 fn test_issue_466_t2_same_namespace_static_call_uses_fallback_prefix() {
4050 let source = r"
4051namespace demo {
4052 struct Repository { static void save(); };
4053 void use() { Repository::save(); }
4054}
4055";
4056 let staging = build_cpp(source);
4057
4058 assert_has_call_edge(&staging, "demo::use", "demo::Repository::save");
4059 }
4060
4061 #[test]
4062 fn test_issue_466_t3_using_declaration_alias_resolves_static_call() {
4063 let source = r"
4064namespace lib { struct Widget { static void make(); }; }
4065namespace app { using lib::Widget; void run() { Widget::make(); } }
4066";
4067 let staging = build_cpp(source);
4068
4069 assert_has_call_edge(&staging, "app::run", "lib::Widget::make");
4070 assert_no_call_target(&staging, "app::Widget::make");
4071 }
4072
4073 #[test]
4074 fn test_issue_466_t4_unknown_receiver_does_not_invent_member_target() {
4075 let source = r"
4076namespace demo {
4077 struct Service { void run(int* p) { p->frobnicate(); } };
4078}
4079";
4080 let staging = build_cpp(source);
4081
4082 assert_no_call_target_suffix(&staging, "::frobnicate");
4083 assert_has_call_edge(&staging, "demo::Service::run", "demo::p->frobnicate");
4084 }
4085
4086 #[test]
4087 fn test_issue_466_t5_qualified_and_ffi_fallback_behavior_is_unchanged() {
4088 let source = r#"
4089extern "C" { int printf(const char*); }
4090namespace demo { void helper() {} }
4091void run() {
4092 demo::helper();
4093 printf("x");
4094}
4095"#;
4096 let staging = build_cpp(source);
4097
4098 assert_has_call_edge(&staging, "run", "demo::helper");
4099 assert_has_ffi_call_edge(&staging, "run", "extern::C::printf");
4100 }
4101
4102 #[test]
4103 fn test_issue_466_t6_same_class_name_collision_does_not_cross_namespace() {
4104 let source = r"
4105namespace a { struct Repository { void save(); }; }
4106namespace b {
4107 struct Repository { void wipe(); };
4108 struct Service { Repository repo; void run() { repo.save(); } };
4109}
4110";
4111 let staging = build_cpp(source);
4112
4113 assert_no_call_target(&staging, "a::Repository::save");
4114 assert_has_call_edge(&staging, "b::Service::run", "b::Repository::save");
4115 }
4116
4117 #[test]
4118 fn test_issue_466_t7_nested_class_member_access_resolves() {
4119 let source = r"
4120namespace demo {
4121 struct Inner { void tick(); };
4122 struct Outer { struct Nested { Inner inner; void go() { inner.tick(); } }; };
4123}
4124";
4125 let staging = build_cpp(source);
4126
4127 assert_has_call_edge(&staging, "demo::Outer::Nested::go", "demo::Inner::tick");
4128 }
4129
4130 #[test]
4131 fn test_issue_466_t8_out_of_class_method_definition_resolves_member_field() {
4132 let source = r"
4133namespace demo {
4134 struct Repository { void save(); };
4135 struct Service { Repository repo; void run(); };
4136 void Service::run() { repo.save(); }
4137}
4138";
4139 let staging = build_cpp(source);
4140
4141 assert_has_call_edge(&staging, "demo::Service::run", "demo::Repository::save");
4142 }
4143
4144 #[test]
4145 fn test_issue_466_t9_same_named_fields_bind_to_enclosing_class() {
4146 let source = r"
4147namespace demo {
4148 struct Base { struct Handle { void base_op(); }; Handle h; };
4149 struct Repository { void save(); };
4150 struct Service { Repository h; void run() { h.save(); } };
4151}
4152";
4153 let staging = build_cpp(source);
4154
4155 assert_has_call_edge(&staging, "demo::Service::run", "demo::Repository::save");
4156 assert_no_call_target(&staging, "demo::Base::Handle::base_op");
4157 }
4158
4159 #[test]
4165 fn test_struct_field_emits_property_with_field_context() {
4166 let source = "struct Point { int x; int y; };";
4167 let staging = build_cpp(source);
4168
4169 assert_has_node_with_kind_exact(&staging, "Point.x", NodeKind::Property);
4171 assert_has_node_with_kind_exact(&staging, "Point.y", NodeKind::Property);
4172
4173 let entry =
4174 cpp_find_added_node(&staging, "Point.x").expect("Point.x should be staged as a node");
4175 assert_eq!(entry.kind, NodeKind::Property, "x must be Property");
4176 assert!(!entry.is_static, "instance field is_static must be false");
4177 let vis = staging.resolve_local_string(entry.visibility.expect("visibility id"));
4178 assert_eq!(
4179 vis,
4180 Some("public"),
4181 "struct default visibility must be 'public'"
4182 );
4183 assert!(entry.end_line > 0, "field end_line must be set (got 0)");
4189 assert!(
4190 entry.end_line > entry.start_line
4191 || (entry.end_line == entry.start_line && entry.end_column > entry.start_column),
4192 "field span must be non-empty: [{}:{}..{}:{}]",
4193 entry.start_line,
4194 entry.start_column,
4195 entry.end_line,
4196 entry.end_column,
4197 );
4198
4199 let x_id = cpp_find_added_node_id(&staging, "Point.x", NodeKind::Property)
4201 .expect("Point.x Property NodeId");
4202 let edge = staging.operations().iter().find_map(|op| {
4203 if let StagingOp::AddEdge {
4204 source: src,
4205 kind: EdgeKind::TypeOf { context, name, .. },
4206 ..
4207 } = op
4208 && *src == x_id
4209 {
4210 Some((*context, *name))
4211 } else {
4212 None
4213 }
4214 });
4215 let (ctx, name) = edge.expect("TypeOf edge from Point.x should be staged");
4216 assert_eq!(
4217 ctx,
4218 Some(TypeOfContext::Field),
4219 "TypeOf edge context must be Field"
4220 );
4221 let resolved_name = name.and_then(|sid| staging.resolve_local_string(sid));
4222 assert_eq!(
4223 resolved_name,
4224 Some("x"),
4225 "TypeOf edge name must be the bare field name 'x'"
4226 );
4227
4228 let stale_variable = staging.nodes().any(|n| {
4230 n.entry.kind == NodeKind::Variable
4231 && matches!(
4232 staging.resolve_node_name(n.entry),
4233 Some("Point.x" | "Point.y" | "Point::x" | "Point::y")
4234 )
4235 });
4236 assert!(
4237 !stale_variable,
4238 "Point fields must not be emitted as NodeKind::Variable"
4239 );
4240 }
4241
4242 #[test]
4244 fn test_class_field_default_visibility_is_private() {
4245 let source = "class Foo { int hidden; };";
4246 let staging = build_cpp(source);
4247
4248 let entry = cpp_find_added_node(&staging, "Foo.hidden")
4249 .expect("Foo.hidden should be staged as a node");
4250 assert_eq!(entry.kind, NodeKind::Property);
4251 let vis = staging.resolve_local_string(entry.visibility.expect("visibility id"));
4252 assert_eq!(
4253 vis,
4254 Some("private"),
4255 "class default visibility must be 'private'"
4256 );
4257 }
4258
4259 #[test]
4261 fn test_class_field_respects_explicit_access_specifier() {
4262 let source = "class Foo { public: int public_field; protected: int prot_field; };";
4263 let staging = build_cpp(source);
4264
4265 let pub_entry = cpp_find_added_node(&staging, "Foo.public_field")
4266 .expect("Foo.public_field should be staged");
4267 assert_eq!(
4268 staging.resolve_local_string(pub_entry.visibility.expect("vis")),
4269 Some("public")
4270 );
4271
4272 let prot_entry = cpp_find_added_node(&staging, "Foo.prot_field")
4273 .expect("Foo.prot_field should be staged");
4274 assert_eq!(
4275 staging.resolve_local_string(prot_entry.visibility.expect("vis")),
4276 Some("protected")
4277 );
4278 }
4279
4280 #[test]
4283 fn test_const_field_emits_constant() {
4284 let source = "class Foo { const int kMax = 0; };";
4285 let staging = build_cpp(source);
4286
4287 assert_has_node_with_kind_exact(&staging, "Foo.kMax", NodeKind::Constant);
4288 let entry = cpp_find_added_node(&staging, "Foo.kMax").expect("Foo.kMax");
4289 assert_eq!(entry.kind, NodeKind::Constant);
4290 assert!(
4291 !entry.is_static,
4292 "const (non-static) field is_static must be false; only `static` keyword sets is_static"
4293 );
4294 }
4295
4296 #[test]
4300 fn test_constexpr_field_emits_constant() {
4301 let source = "class Foo { constexpr static int kAnswer = 42; };";
4302 let staging = build_cpp(source);
4303
4304 assert_has_node_with_kind_exact(&staging, "Foo.kAnswer", NodeKind::Constant);
4305 let entry = cpp_find_added_node(&staging, "Foo.kAnswer").expect("Foo.kAnswer");
4306 assert_eq!(entry.kind, NodeKind::Constant);
4307 assert!(
4308 entry.is_static,
4309 "static constexpr member must have is_static = true"
4310 );
4311 }
4312
4313 #[test]
4316 fn test_static_field_sets_is_static_true() {
4317 let source = "class Foo { static int counter; };";
4318 let staging = build_cpp(source);
4319
4320 let entry = cpp_find_added_node(&staging, "Foo.counter").expect("Foo.counter");
4321 assert_eq!(entry.kind, NodeKind::Property);
4322 assert!(entry.is_static, "static keyword must set is_static = true");
4323 }
4324
4325 #[test]
4328 fn test_bitfield_emits_property() {
4329 let source = "struct Flags { unsigned int low : 4; unsigned int high : 4; };";
4330 let staging = build_cpp(source);
4331
4332 assert_has_node_with_kind_exact(&staging, "Flags.low", NodeKind::Property);
4333 assert_has_node_with_kind_exact(&staging, "Flags.high", NodeKind::Property);
4334 }
4335
4336 #[test]
4342 fn test_anonymous_union_member_fields_emit_property() {
4343 let source = r"
4344class Variant {
4345public:
4346 int tag;
4347 union {
4348 int as_int;
4349 float as_float;
4350 };
4351};
4352";
4353 let staging = build_cpp(source);
4354
4355 assert_has_node_with_kind_exact(&staging, "Variant.tag", NodeKind::Property);
4357
4358 assert_has_node_with_kind_exact(&staging, "Variant.as_int", NodeKind::Property);
4361 assert_has_node_with_kind_exact(&staging, "Variant.as_float", NodeKind::Property);
4362
4363 let as_int = cpp_find_added_node(&staging, "Variant.as_int")
4366 .expect("Variant.as_int should be staged");
4367 let vis = staging.resolve_local_string(as_int.visibility.expect("visibility id"));
4368 assert_eq!(
4369 vis,
4370 Some("public"),
4371 "anonymous-union members must inherit OUTER access (`public:` here)"
4372 );
4373
4374 let bogus = staging.nodes().any(|n| {
4377 staging
4378 .resolve_node_name(n.entry)
4379 .is_some_and(|name| name.contains("::.") || name.starts_with("Variant::."))
4380 });
4381 assert!(
4382 !bogus,
4383 "anonymous union must not produce a synthetic qualifier"
4384 );
4385
4386 let stale_variable = staging.nodes().any(|n| {
4388 n.entry.kind == NodeKind::Variable
4389 && matches!(
4390 staging.resolve_node_name(n.entry),
4391 Some("Variant.tag" | "Variant.as_int" | "Variant.as_float")
4392 )
4393 });
4394 assert!(
4395 !stale_variable,
4396 "anonymous-union members + outer fields must not stay as Variable"
4397 );
4398 }
4399
4400 #[test]
4404 fn test_templated_class_field_emits_property() {
4405 let source = r"
4406template<class T>
4407struct Box {
4408 T value;
4409};
4410";
4411 let staging = build_cpp(source);
4412
4413 assert_has_node_with_kind_exact(&staging, "Box.value", NodeKind::Property);
4414 let entry = cpp_find_added_node(&staging, "Box.value").expect("Box.value");
4415 assert_eq!(entry.kind, NodeKind::Property);
4416 assert!(!entry.is_static);
4417 }
4418
4419 #[test]
4425 fn test_outer_class_field_with_nested_class_present() {
4426 let source = r"
4427class Outer {
4428public:
4429 int outer_value;
4430 class Inner {
4431 public:
4432 int x;
4433 };
4434};
4435";
4436 let staging = build_cpp(source);
4437
4438 assert_has_node_with_kind_exact(&staging, "Outer.outer_value", NodeKind::Property);
4440
4441 assert_has_node_with_kind_exact(&staging, "Outer::Inner.x", NodeKind::Property);
4445
4446 let legacy_hits: Vec<_> = staging
4449 .nodes()
4450 .filter(|n| staging.resolve_node_name(n.entry) == Some("Outer::outer_value"))
4451 .collect();
4452 assert!(
4453 legacy_hits.is_empty(),
4454 "legacy `Outer::outer_value` lookup must return 0 hits"
4455 );
4456
4457 for legacy in ["Inner.x", "Outer::Inner::x", "Outer.Inner.x"] {
4461 let hits: Vec<_> = staging
4462 .nodes()
4463 .filter(|n| staging.resolve_node_name(n.entry) == Some(legacy))
4464 .collect();
4465 assert!(
4466 hits.is_empty(),
4467 "nested-class field `{legacy}` must not appear; expected only `Outer::Inner.x`"
4468 );
4469 }
4470 }
4471
4472 #[test]
4476 fn test_outer_class_with_nested_struct_emits_inner_field() {
4477 let source = r"
4478class Outer {
4479private:
4480 struct Inner {
4481 int y;
4482 };
4483};
4484";
4485 let staging = build_cpp(source);
4486
4487 assert_has_node_with_kind_exact(&staging, "Outer::Inner.y", NodeKind::Property);
4488
4489 let entry = cpp_find_added_node(&staging, "Outer::Inner.y")
4490 .expect("Outer::Inner.y should be staged");
4491 let vis = staging.resolve_local_string(entry.visibility.expect("visibility id"));
4492 assert_eq!(
4493 vis,
4494 Some("public"),
4495 "nested struct field default visibility must be 'public' \
4496 regardless of OUTER access state"
4497 );
4498 }
4499
4500 #[test]
4508 fn test_legacy_double_colon_field_lookup_returns_zero() {
4509 let source = r"
4510class Foo {
4511public:
4512 int bar;
4513 static int baz;
4514 const int qux = 0;
4515};
4516struct Quux {
4517 int corge;
4518};
4519";
4520 let staging = build_cpp(source);
4521
4522 assert_has_node_with_kind_exact(&staging, "Foo.bar", NodeKind::Property);
4524 assert_has_node_with_kind_exact(&staging, "Foo.baz", NodeKind::Property);
4525 assert_has_node_with_kind_exact(&staging, "Foo.qux", NodeKind::Constant);
4526 assert_has_node_with_kind_exact(&staging, "Quux.corge", NodeKind::Property);
4527
4528 for legacy in ["Foo::bar", "Foo::baz", "Foo::qux", "Quux::corge"] {
4531 let hits: Vec<_> = staging
4532 .nodes()
4533 .filter(|n| staging.resolve_node_name(n.entry) == Some(legacy))
4534 .collect();
4535 assert!(
4536 hits.is_empty(),
4537 "legacy lookup for {legacy:?} must return 0 hits, got {} node(s) ({:?})",
4538 hits.len(),
4539 hits.iter()
4540 .map(|n| (n.entry.kind, staging.resolve_node_name(n.entry)))
4541 .collect::<Vec<_>>()
4542 );
4543 }
4544 }
4545
4546 #[test]
4549 fn test_namespaced_class_field_qualified_name() {
4550 let source = r"
4551namespace demo {
4552 class Service {
4553 public:
4554 int counter;
4555 };
4556}
4557";
4558 let staging = build_cpp(source);
4559
4560 assert_has_node_with_kind_exact(&staging, "demo::Service.counter", NodeKind::Property);
4561 }
4562}
4563
4564#[cfg(test)]
4565mod shape_tests {
4566 use super::{cf_bucket_for_cpp_kind, cpp_shape_mapping};
4567 use sqry_core::graph::unified::build::shape::{
4568 CfBucket, ShapeBudget, ShapeMapping, compute_shape_descriptor,
4569 };
4570
4571 const SAMPLE: &str = include_str!(concat!(
4572 env!("CARGO_MANIFEST_DIR"),
4573 "/../test-fixtures/shape/reference/sample.cpp"
4574 ));
4575
4576 fn parse(src: &str) -> tree_sitter::Tree {
4577 let lang: tree_sitter::Language = tree_sitter_cpp::LANGUAGE.into();
4578 let mut p = tree_sitter::Parser::new();
4579 p.set_language(&lang).expect("load cpp grammar");
4580 p.parse(src, None).expect("parse")
4581 }
4582
4583 fn function_named<'t>(tree: &'t tree_sitter::Tree, name: &str) -> tree_sitter::Node<'t> {
4585 let root = tree.root_node();
4586 let mut stack = vec![root];
4587 while let Some(node) = stack.pop() {
4588 if node.kind() == "function_definition"
4589 && function_def_name(node).as_deref() == Some(name)
4590 {
4591 return node;
4592 }
4593 let mut c = node.walk();
4594 for ch in node.children(&mut c) {
4595 stack.push(ch);
4596 }
4597 }
4598 panic!("no function_definition named {name}");
4599 }
4600
4601 fn function_def_name(node: tree_sitter::Node) -> Option<String> {
4603 let mut decl = node.child_by_field_name("declarator")?;
4604 for _ in 0..8 {
4605 if decl.kind() == "function_declarator" {
4606 let inner = decl.child_by_field_name("declarator")?;
4607 return inner.utf8_text(SAMPLE.as_bytes()).ok().map(str::to_owned);
4608 }
4609 decl = decl.child_by_field_name("declarator")?;
4610 }
4611 None
4612 }
4613
4614 #[test]
4615 fn cf_table_is_non_empty() {
4616 let mapping = cpp_shape_mapping();
4617 let lang: tree_sitter::Language = tree_sitter_cpp::LANGUAGE.into();
4618 let mut covered = 0;
4619 for id in 0..lang.node_kind_count() {
4620 if mapping.cf_bucket(id as u16).is_some() {
4621 covered += 1;
4622 }
4623 }
4624 assert!(
4625 covered >= 10,
4626 "expected many C++ CF kinds mapped, got {covered}"
4627 );
4628 }
4629
4630 #[test]
4631 fn histogram_covers_real_control_flow() {
4632 let tree = parse(SAMPLE);
4633 let func = function_named(&tree, "classify");
4634 let d = compute_shape_descriptor(
4635 func,
4636 SAMPLE.as_bytes(),
4637 cpp_shape_mapping(),
4638 &ShapeBudget::default(),
4639 );
4640 assert!(!d.is_unhashable());
4641 for bucket in [
4642 CfBucket::Branch,
4643 CfBucket::Loop,
4644 CfBucket::Match,
4645 CfBucket::Try,
4646 CfBucket::Catch,
4647 CfBucket::Throw,
4648 CfBucket::Return,
4649 CfBucket::BreakContinue,
4650 CfBucket::Call,
4651 CfBucket::Assign,
4652 ] {
4653 assert!(
4654 d.cf_histogram[bucket.index()] >= 1,
4655 "classify must exercise {bucket:?}"
4656 );
4657 }
4658 }
4659
4660 #[test]
4661 fn lambda_body_covers_closure() {
4662 let tree = parse(SAMPLE);
4663 let func = function_named(&tree, "adder");
4664 let d = compute_shape_descriptor(
4665 func,
4666 SAMPLE.as_bytes(),
4667 cpp_shape_mapping(),
4668 &ShapeBudget::default(),
4669 );
4670 assert!(
4671 d.cf_histogram[CfBucket::Closure.index()] >= 1,
4672 "lambda closure"
4673 );
4674 }
4675
4676 #[test]
4677 fn signature_shape_reads_arity_defaults_return() {
4678 let tree = parse(SAMPLE);
4679 let func = function_named(&tree, "classify");
4680 let mapping = cpp_shape_mapping();
4681 let shape = mapping.signature_shape(func, SAMPLE.as_bytes());
4682 assert_eq!(shape.arity_positional, 2);
4684 assert!(shape.has_defaults, "threshold = 0");
4685 assert!(shape.has_return_annotation, "int return type");
4686 }
4687
4688 #[test]
4691 fn ac6_cpp_classify_histogram_well_formed() {
4692 let tree = parse(SAMPLE);
4693 let func = function_named(&tree, "classify");
4694 let d = compute_shape_descriptor(
4695 func,
4696 SAMPLE.as_bytes(),
4697 cpp_shape_mapping(),
4698 &ShapeBudget::default(),
4699 );
4700 assert_eq!(d.cf_histogram[CfBucket::Branch.index()], 2, "two if levels");
4702 assert!(d.cf_histogram[CfBucket::Loop.index()] >= 2, "for + while");
4703 assert!(d.cf_histogram[CfBucket::Return.index()] >= 2, "two returns");
4704 }
4705
4706 #[test]
4707 fn unknown_kind_maps_to_none() {
4708 assert!(cf_bucket_for_cpp_kind("translation_unit").is_none());
4709 assert!(cf_bucket_for_cpp_kind("identifier").is_none());
4710 }
4711}