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, 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#[derive(Debug)]
121struct ASTGraph {
122 contexts: Vec<FunctionContext>,
124 context_start_index: HashMap<usize, usize>,
126
127 field_types: QualifiedNameMap,
133
134 type_map: QualifiedNameMap,
141
142 namespace_map: HashMap<std::ops::Range<usize>, String>,
146}
147
148impl ASTGraph {
149 fn from_tree(root: Node, content: &[u8], budget: &mut BuildBudget) -> GraphResult<Self> {
151 let namespace_map = extract_namespace_map(root, content, budget)?;
153
154 let mut contexts = extract_cpp_contexts(root, content, &namespace_map, budget)?;
156 contexts.sort_by_key(|ctx| ctx.span.0);
157 let context_start_index = contexts
158 .iter()
159 .enumerate()
160 .map(|(idx, ctx)| (ctx.span.0, idx))
161 .collect();
162
163 let (field_types, type_map) =
165 extract_field_and_type_info(root, content, &namespace_map, budget)?;
166
167 Ok(Self {
168 contexts,
169 context_start_index,
170 field_types,
171 type_map,
172 namespace_map,
173 })
174 }
175
176 fn find_enclosing(&self, byte_pos: usize) -> Option<&FunctionContext> {
182 let insertion_point = self.contexts.partition_point(|ctx| ctx.span.0 <= byte_pos);
183 if insertion_point == 0 {
184 return None;
185 }
186
187 let candidate = &self.contexts[insertion_point - 1];
188 (byte_pos < candidate.span.1).then_some(candidate)
189 }
190
191 fn context_for_start(&self, start_byte: usize) -> Option<&FunctionContext> {
192 self.context_start_index
193 .get(&start_byte)
194 .and_then(|idx| self.contexts.get(*idx))
195 }
196}
197
198#[derive(Debug, Clone)]
200struct FunctionContext {
201 qualified_name: String,
203 span: (usize, usize),
205 is_static: bool,
208 #[allow(dead_code)]
212 is_virtual: bool,
213 #[allow(dead_code)]
217 is_inline: bool,
218 namespace_stack: Vec<String>,
220 class_stack: Vec<String>,
224 return_type: Option<String>,
226}
227
228impl FunctionContext {
229 #[allow(dead_code)] fn qualified_name(&self) -> &str {
231 &self.qualified_name
232 }
233}
234
235#[derive(Debug, Default, Clone, Copy)]
259pub struct CppGraphBuilder;
260
261impl CppGraphBuilder {
262 #[must_use]
264 pub fn new() -> Self {
265 Self
266 }
267
268 #[allow(clippy::unused_self)] #[allow(clippy::trivially_copy_pass_by_ref)] fn build_graph_with_budget(
271 #[allow(clippy::trivially_copy_pass_by_ref)] &self,
273 tree: &Tree,
274 content: &[u8],
275 file: &Path,
276 staging: &mut StagingGraph,
277 budget: &mut BuildBudget,
278 ) -> GraphResult<()> {
279 let mut helper = GraphBuildHelper::new(staging, file, Language::Cpp);
281
282 let ast_graph = ASTGraph::from_tree(tree.root_node(), content, budget)?;
284
285 let mut seen_includes: HashSet<String> = HashSet::new();
287
288 let mut namespace_stack: Vec<String> = Vec::new();
290 let mut class_stack: Vec<String> = Vec::new();
291
292 let mut ffi_registry = FfiRegistry::new();
295 collect_ffi_declarations(tree.root_node(), content, &mut ffi_registry, budget)?;
296
297 let mut pure_virtual_registry = PureVirtualRegistry::new();
299 collect_pure_virtual_interfaces(
300 tree.root_node(),
301 content,
302 &mut pure_virtual_registry,
303 budget,
304 )?;
305
306 walk_tree_for_graph(
308 tree.root_node(),
309 content,
310 &ast_graph,
311 &mut helper,
312 &mut seen_includes,
313 &mut namespace_stack,
314 &mut class_stack,
315 &ffi_registry,
316 &pure_virtual_registry,
317 budget,
318 )?;
319
320 Ok(())
321 }
322
323 #[allow(dead_code)] fn extract_class_attributes(node: &tree_sitter::Node, content: &[u8]) -> Vec<String> {
326 let mut attributes = Vec::new();
327 let mut cursor = node.walk();
328 for child in node.children(&mut cursor) {
329 if child.kind() == "modifiers" {
330 let mut mod_cursor = child.walk();
331 for modifier in child.children(&mut mod_cursor) {
332 if let Ok(mod_text) = modifier.utf8_text(content) {
333 match mod_text {
334 "template" => attributes.push("template".to_string()),
335 "sealed" => attributes.push("sealed".to_string()),
336 "abstract" => attributes.push("abstract".to_string()),
337 "open" => attributes.push("open".to_string()),
338 "final" => attributes.push("final".to_string()),
339 "inner" => attributes.push("inner".to_string()),
340 "value" => attributes.push("value".to_string()),
341 _ => {}
342 }
343 }
344 }
345 }
346 }
347 attributes
348 }
349
350 #[allow(dead_code)] fn extract_is_virtual(node: &tree_sitter::Node, content: &[u8]) -> bool {
353 if let Some(spec) = node.child_by_field_name("declaration_specifiers")
354 && let Ok(text) = spec.utf8_text(content)
355 && text.contains("virtual")
356 {
357 return true;
358 }
359
360 if let Ok(text) = node.utf8_text(content)
361 && text.contains("virtual")
362 {
363 return true;
364 }
365
366 if let Some(parent) = node.parent()
367 && (parent.kind() == "field_declaration" || parent.kind() == "declaration")
368 && let Ok(text) = parent.utf8_text(content)
369 && text.contains("virtual")
370 {
371 return true;
372 }
373
374 false
375 }
376
377 #[allow(dead_code)] fn extract_function_attributes(node: &tree_sitter::Node, content: &[u8]) -> Vec<String> {
380 let mut attributes = Vec::new();
381 for node_ref in [
382 node.child_by_field_name("declaration_specifiers"),
383 node.parent(),
384 ]
385 .into_iter()
386 .flatten()
387 {
388 if let Ok(text) = node_ref.utf8_text(content) {
389 for keyword in [
390 "virtual",
391 "inline",
392 "constexpr",
393 "operator",
394 "override",
395 "static",
396 ] {
397 if text.contains(keyword) && !attributes.contains(&keyword.to_string()) {
398 attributes.push(keyword.to_string());
399 }
400 }
401 }
402 }
403
404 if let Ok(text) = node.utf8_text(content) {
405 for keyword in [
406 "virtual",
407 "inline",
408 "constexpr",
409 "operator",
410 "override",
411 "static",
412 ] {
413 if text.contains(keyword) && !attributes.contains(&keyword.to_string()) {
414 attributes.push(keyword.to_string());
415 }
416 }
417 }
418
419 attributes
420 }
421}
422
423impl GraphBuilder for CppGraphBuilder {
424 fn language(&self) -> Language {
425 Language::Cpp
426 }
427
428 fn shape_mapping(&self) -> Option<&dyn ShapeMapping> {
429 Some(cpp_shape_mapping())
430 }
431
432 fn build_graph(
433 &self,
434 tree: &Tree,
435 content: &[u8],
436 file: &Path,
437 staging: &mut StagingGraph,
438 ) -> GraphResult<()> {
439 let mut budget = BuildBudget::new(file);
440 self.build_graph_with_budget(tree, content, file, staging, &mut budget)
441 }
442}
443
444pub struct CppShapeMapping {
453 cf_by_kind_id: Vec<Option<CfBucket>>,
454}
455
456impl CppShapeMapping {
457 fn build() -> Self {
459 let lang: tree_sitter::Language = tree_sitter_cpp::LANGUAGE.into();
460 let count = lang.node_kind_count();
461 let mut cf_by_kind_id = vec![None; count];
462 for (id, slot) in cf_by_kind_id.iter_mut().enumerate() {
463 let Ok(kind_id) = u16::try_from(id) else {
464 break;
465 };
466 if !lang.node_kind_is_named(kind_id) {
467 continue;
468 }
469 if let Some(name) = lang.node_kind_for_id(kind_id) {
470 *slot = cf_bucket_for_cpp_kind(name);
471 }
472 }
473 Self { cf_by_kind_id }
474 }
475}
476
477impl ShapeMapping for CppShapeMapping {
478 fn cf_bucket(&self, ts_node_kind_id: u16) -> Option<CfBucket> {
479 self.cf_by_kind_id
480 .get(ts_node_kind_id as usize)
481 .copied()
482 .flatten()
483 }
484
485 fn signature_shape(&self, fn_node: Node, _src: &[u8]) -> SignatureShape {
486 let mut shape = SignatureShape::default();
487 if let Some(params) = cpp_parameter_list(fn_node) {
491 let mut cursor = params.walk();
492 for child in params.named_children(&mut cursor) {
493 match child.kind() {
494 "parameter_declaration" => {
495 shape.arity_positional = shape.arity_positional.saturating_add(1);
496 }
497 "optional_parameter_declaration" => {
499 shape.arity_positional = shape.arity_positional.saturating_add(1);
500 shape.has_defaults = true;
501 }
502 "variadic_parameter_declaration" | "variadic_declarator" => {
504 shape.has_varargs = true;
505 }
506 _ => {}
507 }
508 }
509 }
510 shape.has_return_annotation = fn_node.child_by_field_name("type").is_some();
513 shape
514 }
515}
516
517fn cpp_parameter_list(fn_node: Node) -> Option<Node> {
521 let mut declarator = fn_node.child_by_field_name("declarator")?;
522 for _ in 0..8 {
525 if declarator.kind() == "function_declarator" {
526 return declarator.child_by_field_name("parameters");
527 }
528 match declarator.child_by_field_name("declarator") {
529 Some(inner) => declarator = inner,
530 None => break,
531 }
532 }
533 None
534}
535
536fn cf_bucket_for_cpp_kind(name: &str) -> Option<CfBucket> {
539 let bucket = match name {
540 "if_statement" | "conditional_expression" => CfBucket::Branch,
541 "for_statement" | "for_range_loop" | "while_statement" | "do_statement" => CfBucket::Loop,
542 "switch_statement" | "case_statement" => CfBucket::Match,
543 "try_statement" => CfBucket::Try,
544 "catch_clause" => CfBucket::Catch,
545 "throw_statement" | "throw_expression" => CfBucket::Throw,
546 "return_statement" | "co_return_statement" => CfBucket::Return,
547 "co_yield_expression" => CfBucket::Yield,
548 "co_await_expression" => CfBucket::Await,
549 "break_statement" | "continue_statement" | "goto_statement" => CfBucket::BreakContinue,
550 "call_expression" => CfBucket::Call,
551 "assignment_expression" | "init_declarator" | "declaration" => CfBucket::Assign,
552 "lambda_expression" => CfBucket::Closure,
553 _ => return None,
554 };
555 Some(bucket)
556}
557
558#[must_use]
560pub fn cpp_shape_mapping() -> &'static CppShapeMapping {
561 static MAPPING: OnceLock<CppShapeMapping> = OnceLock::new();
562 MAPPING.get_or_init(CppShapeMapping::build)
563}
564
565fn extract_namespace_map(
577 node: Node,
578 content: &[u8],
579 budget: &mut BuildBudget,
580) -> GraphResult<HashMap<std::ops::Range<usize>, String>> {
581 let mut map = HashMap::new();
582
583 let recursion_limits = sqry_core::config::RecursionLimits::load_or_default()
585 .expect("Failed to load recursion limits");
586 let file_ops_depth = recursion_limits
587 .effective_file_ops_depth()
588 .expect("Invalid file_ops_depth configuration");
589 let mut guard = sqry_core::query::security::RecursionGuard::new(file_ops_depth)
590 .expect("Failed to create recursion guard");
591
592 extract_namespaces_recursive(node, content, "", &mut map, &mut guard, budget).map_err(|e| {
593 match e {
594 timeout @ GraphBuilderError::BuildTimedOut { .. } => timeout,
595 other => GraphBuilderError::ParseError {
596 span: span_from_node(node),
597 reason: format!("C++ namespace extraction failed: {other}"),
598 },
599 }
600 })?;
601
602 Ok(map)
603}
604
605fn extract_namespaces_recursive(
611 node: Node,
612 content: &[u8],
613 current_ns: &str,
614 map: &mut HashMap<std::ops::Range<usize>, String>,
615 guard: &mut sqry_core::query::security::RecursionGuard,
616 budget: &mut BuildBudget,
617) -> GraphResult<()> {
618 budget.checkpoint("cpp:extract_namespace_map")?;
619 guard.enter().map_err(|e| GraphBuilderError::ParseError {
620 span: span_from_node(node),
621 reason: format!("C++ namespace extraction hit recursion limit: {e}"),
622 })?;
623
624 if node.kind() == "namespace_definition" {
625 let ns_name = if let Some(name_node) = node.child_by_field_name("name") {
627 extract_identifier(name_node, content)
628 } else {
629 String::from("anonymous")
631 };
632
633 let new_ns = if current_ns.is_empty() {
635 format!("{ns_name}::")
636 } else {
637 format!("{current_ns}{ns_name}::")
638 };
639
640 if let Some(body) = node.child_by_field_name("body") {
642 let range = body.start_byte()..body.end_byte();
643 map.insert(range, new_ns.clone());
644
645 let mut cursor = body.walk();
647 for child in body.children(&mut cursor) {
648 extract_namespaces_recursive(child, content, &new_ns, map, guard, budget)?;
649 }
650 }
651 } else {
652 let mut cursor = node.walk();
654 for child in node.children(&mut cursor) {
655 extract_namespaces_recursive(child, content, current_ns, map, guard, budget)?;
656 }
657 }
658
659 guard.exit();
660 Ok(())
661}
662
663fn extract_identifier(node: Node, content: &[u8]) -> String {
665 node.utf8_text(content).unwrap_or("").to_string()
666}
667
668fn find_namespace_for_offset(
670 byte_offset: usize,
671 namespace_map: &HashMap<std::ops::Range<usize>, String>,
672) -> String {
673 let mut matching_ranges: Vec<_> = namespace_map
675 .iter()
676 .filter(|(range, _)| range.contains(&byte_offset))
677 .collect();
678
679 matching_ranges.sort_by_key(|(range, _)| range.end - range.start);
681
682 matching_ranges
684 .first()
685 .map_or("", |(_, ns)| ns.as_str())
686 .to_string()
687}
688
689fn extract_cpp_contexts(
696 node: Node,
697 content: &[u8],
698 namespace_map: &HashMap<std::ops::Range<usize>, String>,
699 budget: &mut BuildBudget,
700) -> GraphResult<Vec<FunctionContext>> {
701 let mut contexts = Vec::new();
702 let mut class_stack = Vec::new();
703
704 let recursion_limits = sqry_core::config::RecursionLimits::load_or_default()
706 .expect("Failed to load recursion limits");
707 let file_ops_depth = recursion_limits
708 .effective_file_ops_depth()
709 .expect("Invalid file_ops_depth configuration");
710 let mut guard = sqry_core::query::security::RecursionGuard::new(file_ops_depth)
711 .expect("Failed to create recursion guard");
712
713 extract_contexts_recursive(
714 node,
715 content,
716 namespace_map,
717 &mut contexts,
718 &mut class_stack,
719 &mut guard,
720 budget,
721 )
722 .map_err(|e| match e {
723 timeout @ GraphBuilderError::BuildTimedOut { .. } => timeout,
724 other => GraphBuilderError::ParseError {
725 span: span_from_node(node),
726 reason: format!("C++ context extraction failed: {other}"),
727 },
728 })?;
729
730 Ok(contexts)
731}
732
733fn extract_contexts_recursive(
738 node: Node,
739 content: &[u8],
740 namespace_map: &HashMap<std::ops::Range<usize>, String>,
741 contexts: &mut Vec<FunctionContext>,
742 class_stack: &mut Vec<String>,
743 guard: &mut sqry_core::query::security::RecursionGuard,
744 budget: &mut BuildBudget,
745) -> GraphResult<()> {
746 budget.checkpoint("cpp:extract_contexts")?;
747 guard.enter().map_err(|e| GraphBuilderError::ParseError {
748 span: span_from_node(node),
749 reason: format!("C++ context extraction hit recursion limit: {e}"),
750 })?;
751
752 match node.kind() {
753 "class_specifier" | "struct_specifier" => {
754 if let Some(name_node) = node.child_by_field_name("name") {
756 let class_name = extract_identifier(name_node, content);
757 class_stack.push(class_name);
758
759 if let Some(body) = node.child_by_field_name("body") {
761 let mut cursor = body.walk();
762 for child in body.children(&mut cursor) {
763 extract_contexts_recursive(
764 child,
765 content,
766 namespace_map,
767 contexts,
768 class_stack,
769 guard,
770 budget,
771 )?;
772 }
773 }
774
775 class_stack.pop();
776 }
777 }
778
779 "function_definition" => {
780 if let Some(declarator) = node.child_by_field_name("declarator") {
782 let (func_name, class_prefix) =
783 extract_function_name_with_class(declarator, content);
784
785 let namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
787 let namespace_stack: Vec<String> = if namespace.is_empty() {
788 Vec::new()
789 } else {
790 namespace
791 .trim_end_matches("::")
792 .split("::")
793 .map(String::from)
794 .collect()
795 };
796
797 let effective_class_stack: Vec<String> = if !class_stack.is_empty() {
801 class_stack.clone()
802 } else if let Some(ref prefix) = class_prefix {
803 vec![prefix.clone()]
804 } else {
805 Vec::new()
806 };
807
808 let qualified_name =
810 build_qualified_name(&namespace_stack, &effective_class_stack, &func_name);
811
812 let is_static = is_static_function(node, content);
814 let is_virtual = is_virtual_function(node, content);
815 let is_inline = is_inline_function(node, content);
816
817 let return_type = node
819 .child_by_field_name("type")
820 .and_then(|type_node| type_node.utf8_text(content).ok())
821 .map(std::string::ToString::to_string);
822
823 let span = (node.start_byte(), node.end_byte());
825
826 contexts.push(FunctionContext {
827 qualified_name,
828 span,
829 is_static,
830 is_virtual,
831 is_inline,
832 namespace_stack,
833 class_stack: effective_class_stack,
834 return_type,
835 });
836 }
837
838 }
840
841 _ => {
842 let mut cursor = node.walk();
844 for child in node.children(&mut cursor) {
845 extract_contexts_recursive(
846 child,
847 content,
848 namespace_map,
849 contexts,
850 class_stack,
851 guard,
852 budget,
853 )?;
854 }
855 }
856 }
857
858 guard.exit();
859 Ok(())
860}
861
862fn build_qualified_name(namespace_stack: &[String], class_stack: &[String], name: &str) -> String {
867 let mut parts = Vec::new();
868
869 parts.extend(namespace_stack.iter().cloned());
871
872 for class_name in class_stack {
874 parts.push(class_name.clone());
875 }
876
877 parts.push(name.to_string());
879
880 parts.join("::")
881}
882
883fn extract_function_name_with_class(declarator: Node, content: &[u8]) -> (String, Option<String>) {
888 match declarator.kind() {
896 "function_declarator" => {
897 if let Some(declarator_inner) = declarator.child_by_field_name("declarator") {
899 extract_function_name_with_class(declarator_inner, content)
900 } else {
901 (extract_identifier(declarator, content), None)
902 }
903 }
904 "qualified_identifier" => {
905 let name = if let Some(name_node) = declarator.child_by_field_name("name") {
907 extract_identifier(name_node, content)
908 } else {
909 extract_identifier(declarator, content)
910 };
911
912 let class_prefix = declarator
914 .child_by_field_name("scope")
915 .map(|scope_node| extract_identifier(scope_node, content));
916
917 (name, class_prefix)
918 }
919 "field_identifier" | "identifier" | "destructor_name" | "operator_name" => {
920 (extract_identifier(declarator, content), None)
921 }
922 _ => {
923 (extract_identifier(declarator, content), None)
925 }
926 }
927}
928
929#[allow(dead_code)]
931fn extract_function_name(declarator: Node, content: &[u8]) -> String {
932 extract_function_name_with_class(declarator, content).0
933}
934
935fn is_static_function(node: Node, content: &[u8]) -> bool {
937 has_specifier(node, "static", content)
938}
939
940fn is_virtual_function(node: Node, content: &[u8]) -> bool {
942 has_specifier(node, "virtual", content)
943}
944
945fn is_inline_function(node: Node, content: &[u8]) -> bool {
947 has_specifier(node, "inline", content)
948}
949
950fn has_specifier(node: Node, specifier: &str, content: &[u8]) -> bool {
952 let mut cursor = node.walk();
954 for child in node.children(&mut cursor) {
955 if (child.kind() == "storage_class_specifier"
956 || child.kind() == "type_qualifier"
957 || child.kind() == "virtual"
958 || child.kind() == "inline")
959 && let Ok(text) = child.utf8_text(content)
960 && text == specifier
961 {
962 return true;
963 }
964 }
965 false
966}
967
968fn extract_field_and_type_info(
978 node: Node,
979 content: &[u8],
980 namespace_map: &HashMap<std::ops::Range<usize>, String>,
981 budget: &mut BuildBudget,
982) -> GraphResult<(QualifiedNameMap, QualifiedNameMap)> {
983 let mut field_types = HashMap::new();
984 let mut type_map = HashMap::new();
985 let mut class_stack = Vec::new();
986
987 let mut declared_classes: HashSet<String> = HashSet::new();
995 let mut collect_stack: Vec<String> = Vec::new();
996 collect_declared_class_fqns(
997 node,
998 content,
999 namespace_map,
1000 &mut declared_classes,
1001 &mut collect_stack,
1002 budget,
1003 )?;
1004
1005 extract_fields_recursive(
1006 node,
1007 content,
1008 namespace_map,
1009 &declared_classes,
1010 &mut field_types,
1011 &mut type_map,
1012 &mut class_stack,
1013 budget,
1014 )?;
1015
1016 Ok((field_types, type_map))
1017}
1018
1019fn build_class_fqn(class_name: &str, namespace: &str, class_stack: &[String]) -> String {
1026 if let Some(parent_fqn) = class_stack.last() {
1027 format!("{parent_fqn}::{class_name}")
1028 } else if namespace.is_empty() {
1029 class_name.to_string()
1030 } else {
1031 format!("{}::{}", namespace.trim_end_matches("::"), class_name)
1032 }
1033}
1034
1035fn collect_declared_class_fqns(
1039 node: Node,
1040 content: &[u8],
1041 namespace_map: &HashMap<std::ops::Range<usize>, String>,
1042 declared: &mut HashSet<String>,
1043 class_stack: &mut Vec<String>,
1044 budget: &mut BuildBudget,
1045) -> GraphResult<()> {
1046 budget.checkpoint("cpp:collect_declared_classes")?;
1047 match node.kind() {
1048 "class_specifier" | "struct_specifier" => {
1049 if let Some(name_node) = node.child_by_field_name("name") {
1050 let class_name = extract_identifier(name_node, content);
1051 let namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
1052 let class_fqn = build_class_fqn(&class_name, &namespace, class_stack);
1053
1054 declared.insert(class_fqn.clone());
1055 class_stack.push(class_fqn);
1056
1057 let mut cursor = node.walk();
1058 for child in node.children(&mut cursor) {
1059 collect_declared_class_fqns(
1060 child,
1061 content,
1062 namespace_map,
1063 declared,
1064 class_stack,
1065 budget,
1066 )?;
1067 }
1068
1069 class_stack.pop();
1070 }
1071 }
1072 _ => {
1073 let mut cursor = node.walk();
1074 for child in node.children(&mut cursor) {
1075 collect_declared_class_fqns(
1076 child,
1077 content,
1078 namespace_map,
1079 declared,
1080 class_stack,
1081 budget,
1082 )?;
1083 }
1084 }
1085 }
1086 Ok(())
1087}
1088
1089fn is_cpp_primitive(name: &str) -> bool {
1093 matches!(
1094 name,
1095 "int"
1096 | "void"
1097 | "bool"
1098 | "char"
1099 | "double"
1100 | "float"
1101 | "long"
1102 | "short"
1103 | "unsigned"
1104 | "signed"
1105 | "wchar_t"
1106 | "auto"
1107 | "char8_t"
1108 | "char16_t"
1109 | "char32_t"
1110 | "int8_t"
1111 | "int16_t"
1112 | "int32_t"
1113 | "int64_t"
1114 | "uint8_t"
1115 | "uint16_t"
1116 | "uint32_t"
1117 | "uint64_t"
1118 | "intptr_t"
1119 | "uintptr_t"
1120 | "size_t"
1121 | "ssize_t"
1122 | "ptrdiff_t"
1123 )
1124}
1125
1126fn qualify_field_type(
1141 resolved_type: &str,
1142 class_fqn: &str,
1143 namespace: &str,
1144 declared_classes: &HashSet<String>,
1145) -> String {
1146 if resolved_type.contains("::") || is_cpp_primitive(resolved_type) {
1148 return resolved_type.to_string();
1149 }
1150
1151 let candidate = format!("{class_fqn}::{resolved_type}");
1153 if declared_classes.contains(&candidate) {
1154 return candidate;
1155 }
1156
1157 let mut prefix = class_fqn;
1159 while let Some(idx) = prefix.rfind("::") {
1160 prefix = &prefix[..idx];
1161 let candidate = format!("{prefix}::{resolved_type}");
1162 if declared_classes.contains(&candidate) {
1163 return candidate;
1164 }
1165 }
1166
1167 let namespace_key = namespace.trim_end_matches("::");
1169 if !namespace_key.is_empty() {
1170 let candidate = format!("{namespace_key}::{resolved_type}");
1171 if declared_classes.contains(&candidate) {
1172 return candidate;
1173 }
1174 }
1175
1176 resolved_type.to_string()
1180}
1181
1182fn extract_fields_recursive(
1184 node: Node,
1185 content: &[u8],
1186 namespace_map: &HashMap<std::ops::Range<usize>, String>,
1187 declared_classes: &HashSet<String>,
1188 field_types: &mut HashMap<(String, String), String>,
1189 type_map: &mut HashMap<(String, String), String>,
1190 class_stack: &mut Vec<String>,
1191 budget: &mut BuildBudget,
1192) -> GraphResult<()> {
1193 budget.checkpoint("cpp:extract_fields")?;
1194 match node.kind() {
1195 "class_specifier" | "struct_specifier" => {
1196 if let Some(name_node) = node.child_by_field_name("name") {
1198 let class_name = extract_identifier(name_node, content);
1199 let namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
1200
1201 let class_fqn = build_class_fqn(&class_name, &namespace, class_stack);
1203
1204 class_stack.push(class_fqn.clone());
1205
1206 let mut cursor = node.walk();
1208 for child in node.children(&mut cursor) {
1209 extract_fields_recursive(
1210 child,
1211 content,
1212 namespace_map,
1213 declared_classes,
1214 field_types,
1215 type_map,
1216 class_stack,
1217 budget,
1218 )?;
1219 }
1220
1221 class_stack.pop();
1222 }
1223 }
1224
1225 "field_declaration" => {
1226 if let Some(class_fqn) = class_stack.last() {
1228 extract_field_declaration(
1229 node,
1230 content,
1231 class_fqn,
1232 namespace_map,
1233 declared_classes,
1234 field_types,
1235 type_map,
1236 );
1237 }
1238
1239 let mut cursor = node.walk();
1244 for child in node.children(&mut cursor) {
1245 extract_fields_recursive(
1246 child,
1247 content,
1248 namespace_map,
1249 declared_classes,
1250 field_types,
1251 type_map,
1252 class_stack,
1253 budget,
1254 )?;
1255 }
1256 }
1257
1258 "using_directive" => {
1259 extract_using_directive(node, content, namespace_map, type_map);
1261 }
1262
1263 "using_declaration" => {
1264 extract_using_declaration(node, content, namespace_map, type_map);
1266 }
1267
1268 _ => {
1269 let mut cursor = node.walk();
1271 for child in node.children(&mut cursor) {
1272 extract_fields_recursive(
1273 child,
1274 content,
1275 namespace_map,
1276 declared_classes,
1277 field_types,
1278 type_map,
1279 class_stack,
1280 budget,
1281 )?;
1282 }
1283 }
1284 }
1285
1286 Ok(())
1287}
1288
1289fn extract_field_declaration(
1291 node: Node,
1292 content: &[u8],
1293 class_fqn: &str,
1294 namespace_map: &HashMap<std::ops::Range<usize>, String>,
1295 declared_classes: &HashSet<String>,
1296 field_types: &mut HashMap<(String, String), String>,
1297 type_map: &HashMap<(String, String), String>,
1298) {
1299 let mut field_type = None;
1304 let mut field_names = Vec::new();
1305
1306 let mut cursor = node.walk();
1307 for child in node.children(&mut cursor) {
1308 match child.kind() {
1309 "type_identifier" | "primitive_type" | "qualified_identifier" | "template_type" => {
1310 field_type = Some(extract_type_name(child, content));
1311 }
1312 "field_identifier" => {
1313 field_names.push(extract_identifier(child, content));
1315 }
1316 "field_declarator"
1317 | "init_declarator"
1318 | "pointer_declarator"
1319 | "reference_declarator"
1320 | "array_declarator" => {
1321 if let Some(name) = extract_field_name(child, content) {
1323 field_names.push(name);
1324 }
1325 }
1326 _ => {}
1327 }
1328 }
1329
1330 if let Some(ftype) = field_type {
1334 let namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
1335 let resolved = resolve_type_to_fqn(&ftype, &namespace, type_map);
1336 let field_type_fqn = qualify_field_type(&resolved, class_fqn, &namespace, declared_classes);
1337
1338 for fname in field_names {
1340 field_types.insert((class_fqn.to_string(), fname), field_type_fqn.clone());
1341 }
1342 }
1343}
1344
1345fn extract_type_name(type_node: Node, content: &[u8]) -> String {
1347 match type_node.kind() {
1348 "type_identifier" | "primitive_type" => extract_identifier(type_node, content),
1349 "qualified_identifier" => {
1350 extract_identifier(type_node, content)
1352 }
1353 "template_type" => {
1354 if let Some(name) = type_node.child_by_field_name("name") {
1356 extract_identifier(name, content)
1357 } else {
1358 extract_identifier(type_node, content)
1359 }
1360 }
1361 _ => {
1362 extract_identifier(type_node, content)
1364 }
1365 }
1366}
1367
1368fn extract_field_name(declarator: Node, content: &[u8]) -> Option<String> {
1370 match declarator.kind() {
1371 "field_declarator" => {
1372 if let Some(declarator_inner) = declarator.child_by_field_name("declarator") {
1374 extract_field_name(declarator_inner, content)
1375 } else {
1376 Some(extract_identifier(declarator, content))
1377 }
1378 }
1379 "field_identifier" | "identifier" => Some(extract_identifier(declarator, content)),
1380 "pointer_declarator" | "reference_declarator" | "array_declarator" => {
1381 if let Some(declarator_inner) = declarator.child_by_field_name("declarator") {
1383 extract_field_name(declarator_inner, content)
1384 } else {
1385 None
1386 }
1387 }
1388 "init_declarator" => {
1389 if let Some(declarator_inner) = declarator.child_by_field_name("declarator") {
1391 extract_field_name(declarator_inner, content)
1392 } else {
1393 None
1394 }
1395 }
1396 _ => None,
1397 }
1398}
1399
1400fn resolve_type_to_fqn(
1402 type_name: &str,
1403 namespace: &str,
1404 type_map: &HashMap<(String, String), String>,
1405) -> String {
1406 if type_name.contains("::") {
1408 return type_name.to_string();
1409 }
1410
1411 let namespace_key = namespace.trim_end_matches("::").to_string();
1413 if let Some(fqn) = type_map.get(&(namespace_key.clone(), type_name.to_string())) {
1414 return fqn.clone();
1415 }
1416
1417 if let Some(fqn) = type_map.get(&(String::new(), type_name.to_string())) {
1419 return fqn.clone();
1420 }
1421
1422 type_name.to_string()
1424}
1425
1426fn extract_using_directive(
1428 node: Node,
1429 content: &[u8],
1430 namespace_map: &HashMap<std::ops::Range<usize>, String>,
1431 _type_map: &mut HashMap<(String, String), String>,
1432) {
1433 let _namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
1437
1438 if let Some(name_node) = node.child_by_field_name("name") {
1440 let _using_ns = extract_identifier(name_node, content);
1441 }
1445}
1446
1447fn extract_using_declaration(
1452 node: Node,
1453 content: &[u8],
1454 namespace_map: &HashMap<std::ops::Range<usize>, String>,
1455 type_map: &mut HashMap<(String, String), String>,
1456) {
1457 let namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
1458 let namespace_key = namespace.trim_end_matches("::").to_string();
1459
1460 let mut cursor = node.walk();
1462 for child in node.children(&mut cursor) {
1463 if child.kind() == "qualified_identifier" || child.kind() == "identifier" {
1464 let fqn = extract_identifier(child, content);
1465
1466 if let Some(simple_name) = fqn.split("::").last() {
1468 type_map.insert((namespace_key, simple_name.to_string()), fqn);
1470 }
1471 break;
1472 }
1473 }
1474}
1475
1476fn resolve_callee_name(
1498 function_node: Node<'_>,
1499 callee_name: &str,
1500 caller_ctx: &FunctionContext,
1501 ast_graph: &ASTGraph,
1502 content: &[u8],
1503) -> String {
1504 if function_node.kind() == "field_expression" {
1506 if let Some(fqn) = resolve_member_call(function_node, caller_ctx, ast_graph, content) {
1507 return fqn;
1508 }
1509 return resolve_callee_name_namespace_prefixed(callee_name, caller_ctx);
1510 }
1511
1512 if !callee_name.starts_with("::")
1516 && callee_name.contains("::")
1517 && let Some(fqn) = resolve_static_call(function_node, callee_name, ast_graph)
1518 {
1519 return fqn;
1520 }
1521
1522 resolve_callee_name_namespace_prefixed(callee_name, caller_ctx)
1523}
1524
1525fn resolve_callee_name_namespace_prefixed(
1530 callee_name: &str,
1531 caller_ctx: &FunctionContext,
1532) -> String {
1533 if callee_name.starts_with("::") {
1535 return callee_name.trim_start_matches("::").to_string();
1536 }
1537
1538 if callee_name.contains("::") {
1540 if !caller_ctx.namespace_stack.is_empty() {
1542 let namespace_prefix = caller_ctx.namespace_stack.join("::");
1543 return format!("{namespace_prefix}::{callee_name}");
1544 }
1545 return callee_name.to_string();
1546 }
1547
1548 let mut parts = Vec::new();
1550
1551 if !caller_ctx.namespace_stack.is_empty() {
1553 parts.extend(caller_ctx.namespace_stack.iter().cloned());
1554 }
1555
1556 parts.push(callee_name.to_string());
1561
1562 parts.join("::")
1563}
1564
1565fn enclosing_class_fqn(caller_ctx: &FunctionContext) -> Option<String> {
1574 if caller_ctx.class_stack.is_empty() {
1575 return None;
1576 }
1577 let mut parts: Vec<&str> = Vec::new();
1578 parts.extend(caller_ctx.namespace_stack.iter().map(String::as_str));
1579 parts.extend(caller_ctx.class_stack.iter().map(String::as_str));
1580 Some(parts.join("::"))
1581}
1582
1583fn resolve_member_call(
1592 function_node: Node<'_>,
1593 caller_ctx: &FunctionContext,
1594 ast_graph: &ASTGraph,
1595 content: &[u8],
1596) -> Option<String> {
1597 let receiver_node = function_node.child_by_field_name("argument")?;
1598 let method_node = function_node.child_by_field_name("field")?;
1599
1600 if !matches!(receiver_node.kind(), "identifier" | "field_identifier") {
1603 return None;
1604 }
1605
1606 let receiver_text = receiver_node.utf8_text(content).ok()?.trim();
1607 let method_text = method_node.utf8_text(content).ok()?.trim();
1608 if receiver_text.is_empty() || method_text.is_empty() {
1609 return None;
1610 }
1611
1612 let class_fqn = enclosing_class_fqn(caller_ctx)?;
1613 let field_type = ast_graph
1614 .field_types
1615 .get(&(class_fqn, receiver_text.to_string()))?;
1616
1617 Some(format!("{field_type}::{method_text}"))
1618}
1619
1620fn resolve_static_call(
1626 function_node: Node<'_>,
1627 callee_name: &str,
1628 ast_graph: &ASTGraph,
1629) -> Option<String> {
1630 let (qualifier, method) = callee_name.rsplit_once("::")?;
1631 if qualifier.is_empty() || qualifier.contains("::") || method.is_empty() {
1633 return None;
1634 }
1635
1636 let namespace = find_namespace_for_offset(function_node.start_byte(), &ast_graph.namespace_map);
1637 let namespace_key = namespace.trim_end_matches("::").to_string();
1638
1639 let resolved_qualifier = ast_graph
1640 .type_map
1641 .get(&(namespace_key, qualifier.to_string()))
1642 .or_else(|| {
1643 ast_graph
1644 .type_map
1645 .get(&(String::new(), qualifier.to_string()))
1646 })?;
1647
1648 Some(format!("{resolved_qualifier}::{method}"))
1649}
1650
1651fn strip_type_qualifiers(type_text: &str) -> String {
1658 let mut result = type_text.trim().to_string();
1659
1660 result = result.replace("const ", "");
1662 result = result.replace("volatile ", "");
1663 result = result.replace("mutable ", "");
1664 result = result.replace("constexpr ", "");
1665
1666 result = result.replace(" const", "");
1668 result = result.replace(" volatile", "");
1669 result = result.replace(" mutable", "");
1670 result = result.replace(" constexpr", "");
1671
1672 result = result.replace(['*', '&'], "");
1674
1675 result = result.trim().to_string();
1677
1678 if let Some(last_part) = result.split("::").last() {
1680 result = last_part.to_string();
1681 }
1682
1683 if let Some(open_bracket) = result.find('<') {
1685 result = result[..open_bracket].to_string();
1686 }
1687
1688 result.trim().to_string()
1689}
1690
1691#[allow(clippy::unnecessary_wraps, clippy::too_many_lines)]
1707fn process_field_declaration(
1708 node: Node,
1709 content: &[u8],
1710 class_qualified_name: &str,
1711 visibility: &str,
1712 helper: &mut GraphBuildHelper,
1713) -> GraphResult<()> {
1714 let mut field_type_text = None;
1716 let mut field_names = Vec::new();
1717 let mut is_static_kw = false;
1720 let mut is_const = false;
1721 let mut is_constexpr = false;
1722
1723 let mut cursor = node.walk();
1724 for child in node.children(&mut cursor) {
1725 match child.kind() {
1726 "type_identifier" | "primitive_type" => {
1727 if let Ok(text) = child.utf8_text(content) {
1728 field_type_text = Some(text.to_string());
1729 }
1730 }
1731 "qualified_identifier" => {
1732 if let Ok(text) = child.utf8_text(content) {
1734 field_type_text = Some(text.to_string());
1735 }
1736 }
1737 "template_type" => {
1738 if let Ok(text) = child.utf8_text(content) {
1740 field_type_text = Some(text.to_string());
1741 }
1742 }
1743 "sized_type_specifier" => {
1744 if let Ok(text) = child.utf8_text(content) {
1746 field_type_text = Some(text.to_string());
1747 }
1748 }
1749 "type_qualifier" => {
1750 if let Ok(text) = child.utf8_text(content) {
1757 let trimmed = text.trim();
1758 if trimmed == "const" {
1759 is_const = true;
1760 } else if trimmed == "constexpr" {
1761 is_constexpr = true;
1762 }
1763 if field_type_text.is_none() {
1764 field_type_text = Some(text.to_string());
1765 }
1766 }
1767 }
1768 "storage_class_specifier" => {
1769 if let Ok(text) = child.utf8_text(content) {
1772 let trimmed = text.trim();
1773 if trimmed == "static" {
1774 is_static_kw = true;
1775 } else if trimmed == "constexpr" {
1776 is_constexpr = true;
1777 }
1778 }
1779 }
1780 "auto" => {
1781 field_type_text = Some("auto".to_string());
1783 }
1784 "decltype" => {
1785 if let Ok(text) = child.utf8_text(content) {
1787 field_type_text = Some(text.to_string());
1788 }
1789 }
1790 "struct_specifier" | "class_specifier" | "enum_specifier" | "union_specifier" => {
1791 if let Ok(text) = child.utf8_text(content) {
1793 field_type_text = Some(text.to_string());
1794 }
1795 }
1796 "field_identifier" => {
1797 if let Ok(name) = child.utf8_text(content) {
1798 field_names.push(name.trim().to_string());
1799 }
1800 }
1801 "field_declarator"
1802 | "pointer_declarator"
1803 | "reference_declarator"
1804 | "init_declarator" => {
1805 if let Some(name) = extract_field_name(child, content) {
1807 field_names.push(name);
1808 }
1809 }
1810 _ => {}
1811 }
1812 }
1813
1814 if let Some(type_text) = field_type_text {
1816 let base_type = strip_type_qualifiers(&type_text);
1817 let is_constant = is_const || is_constexpr;
1818
1819 for field_name in field_names {
1820 let field_qualified = format!("{class_qualified_name}.{field_name}");
1823 let span = span_from_node(node);
1824
1825 let field_id = if is_constant {
1829 helper.add_constant_with_name_static_and_visibility(
1830 &field_name,
1831 &field_qualified,
1832 Some(span),
1833 is_static_kw,
1834 Some(visibility),
1835 )
1836 } else {
1837 helper.add_property_with_name_static_and_visibility(
1838 &field_name,
1839 &field_qualified,
1840 Some(span),
1841 is_static_kw,
1842 Some(visibility),
1843 )
1844 };
1845
1846 let type_id = helper.add_type(&base_type, None);
1848
1849 helper.add_typeof_edge_with_context(
1851 field_id,
1852 type_id,
1853 Some(sqry_core::graph::unified::edge::kind::TypeOfContext::Field),
1854 None,
1855 Some(&field_name),
1856 );
1857
1858 helper.add_reference_edge(field_id, type_id);
1861 }
1862 }
1863
1864 Ok(())
1865}
1866
1867#[allow(clippy::unnecessary_wraps)]
1869fn process_global_variable_declaration(
1870 node: Node,
1871 content: &[u8],
1872 namespace_stack: &[String],
1873 helper: &mut GraphBuildHelper,
1874) -> GraphResult<()> {
1875 if node.kind() != "declaration" {
1877 return Ok(());
1878 }
1879
1880 let mut cursor_check = node.walk();
1883 for child in node.children(&mut cursor_check) {
1884 if child.kind() == "function_declarator" {
1885 return Ok(());
1886 }
1887 }
1888
1889 let mut type_text = None;
1891 let mut var_names = Vec::new();
1892
1893 let mut cursor = node.walk();
1894 for child in node.children(&mut cursor) {
1895 match child.kind() {
1896 "type_identifier" | "primitive_type" | "qualified_identifier" | "template_type" => {
1897 if let Ok(text) = child.utf8_text(content) {
1898 type_text = Some(text.to_string());
1899 }
1900 }
1901 "init_declarator" => {
1902 if let Some(declarator) = child.child_by_field_name("declarator")
1904 && let Some(name) = extract_declarator_name(declarator, content)
1905 {
1906 var_names.push(name);
1907 }
1908 }
1909 "pointer_declarator" | "reference_declarator" => {
1910 if let Some(name) = extract_declarator_name(child, content) {
1911 var_names.push(name);
1912 }
1913 }
1914 "identifier" => {
1915 if let Ok(name) = child.utf8_text(content) {
1917 var_names.push(name.to_string());
1918 }
1919 }
1920 _ => {}
1921 }
1922 }
1923
1924 if let Some(type_text) = type_text {
1925 let base_type = strip_type_qualifiers(&type_text);
1926
1927 for var_name in var_names {
1928 let qualified = if namespace_stack.is_empty() {
1930 var_name.clone()
1931 } else {
1932 format!("{}::{}", namespace_stack.join("::"), var_name)
1933 };
1934
1935 let span = span_from_node(node);
1936
1937 let var_id = helper.add_node_with_visibility(
1939 &qualified,
1940 Some(span),
1941 sqry_core::graph::unified::node::NodeKind::Variable,
1942 Some("public"),
1943 );
1944 helper.mark_definition(var_id);
1946
1947 let type_id = helper.add_type(&base_type, None);
1949
1950 helper.add_typeof_edge(var_id, type_id);
1952 helper.add_reference_edge(var_id, type_id);
1953 }
1954 }
1955
1956 Ok(())
1957}
1958
1959fn extract_declarator_name(node: Node, content: &[u8]) -> Option<String> {
1961 match node.kind() {
1962 "identifier" => {
1963 if let Ok(name) = node.utf8_text(content) {
1964 Some(name.to_string())
1965 } else {
1966 None
1967 }
1968 }
1969 "pointer_declarator" | "reference_declarator" | "array_declarator" => {
1970 if let Some(inner) = node.child_by_field_name("declarator") {
1972 extract_declarator_name(inner, content)
1973 } else {
1974 let mut cursor = node.walk();
1976 for child in node.children(&mut cursor) {
1977 if child.kind() == "identifier"
1978 && let Ok(name) = child.utf8_text(content)
1979 {
1980 return Some(name.to_string());
1981 }
1982 }
1983 None
1984 }
1985 }
1986 "init_declarator" => {
1987 if let Some(inner) = node.child_by_field_name("declarator") {
1989 extract_declarator_name(inner, content)
1990 } else {
1991 None
1992 }
1993 }
1994 "field_declarator" => {
1995 if let Some(inner) = node.child_by_field_name("declarator") {
1997 extract_declarator_name(inner, content)
1998 } else {
1999 if let Ok(name) = node.utf8_text(content) {
2001 Some(name.to_string())
2002 } else {
2003 None
2004 }
2005 }
2006 }
2007 _ => None,
2008 }
2009}
2010
2011#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
2013fn walk_class_body(
2014 body_node: Node,
2015 content: &[u8],
2016 class_qualified_name: &str,
2017 is_struct: bool,
2018 ast_graph: &ASTGraph,
2019 helper: &mut GraphBuildHelper,
2020 seen_includes: &mut HashSet<String>,
2021 namespace_stack: &mut Vec<String>,
2022 class_stack: &mut Vec<String>,
2023 ffi_registry: &FfiRegistry,
2024 pure_virtual_registry: &PureVirtualRegistry,
2025 budget: &mut BuildBudget,
2026) -> GraphResult<()> {
2027 let mut current_visibility = if is_struct { "public" } else { "private" };
2029
2030 let mut cursor = body_node.walk();
2031 for child in body_node.children(&mut cursor) {
2032 budget.checkpoint("cpp:walk_class_body")?;
2033 match child.kind() {
2034 "access_specifier" => {
2035 if let Ok(text) = child.utf8_text(content) {
2037 let spec = text.trim().trim_end_matches(':').trim();
2038 current_visibility = spec;
2039 }
2040 }
2041 "field_declaration" => {
2042 let mut handled_nested = false;
2058 let mut inner_cursor = child.walk();
2059 for inner in child.children(&mut inner_cursor) {
2060 let kind = inner.kind();
2061 if !matches!(
2062 kind,
2063 "class_specifier"
2064 | "struct_specifier"
2065 | "union_specifier"
2066 | "enum_specifier"
2067 ) {
2068 continue;
2069 }
2070
2071 let is_struct_or_union = matches!(kind, "struct_specifier" | "union_specifier");
2072
2073 if let Some(name_node) = inner.child_by_field_name("name") {
2074 if let Ok(inner_name) = name_node.utf8_text(content) {
2086 let inner_name = inner_name.trim();
2087 let nested_qualified = format!("{class_qualified_name}::{inner_name}");
2088 let nested_span = span_from_node(inner);
2089
2090 if kind == "enum_specifier" {
2094 helper.add_enum_with_visibility(
2098 &nested_qualified,
2099 Some(nested_span),
2100 Some(current_visibility),
2101 );
2102 } else {
2103 let nested_id = if is_struct_or_union {
2104 helper.add_struct_with_visibility(
2105 &nested_qualified,
2106 Some(nested_span),
2107 Some(current_visibility),
2108 )
2109 } else {
2110 helper.add_class_with_visibility(
2111 &nested_qualified,
2112 Some(nested_span),
2113 Some(current_visibility),
2114 )
2115 };
2116 build_inheritance_and_implements_edges(
2117 inner,
2118 content,
2119 &nested_qualified,
2120 nested_id,
2121 helper,
2122 namespace_stack,
2123 pure_virtual_registry,
2124 )?;
2125 }
2126
2127 if matches!(
2132 kind,
2133 "class_specifier" | "struct_specifier" | "union_specifier"
2134 ) && let Some(body) = inner.child_by_field_name("body")
2135 {
2136 walk_class_body(
2137 body,
2138 content,
2139 &nested_qualified,
2140 is_struct_or_union,
2141 ast_graph,
2142 helper,
2143 seen_includes,
2144 namespace_stack,
2145 class_stack,
2146 ffi_registry,
2147 pure_virtual_registry,
2148 budget,
2149 )?;
2150 }
2151 handled_nested = true;
2152 }
2153 } else if let Some(body) = inner.child_by_field_name("body") {
2154 let mut anon_cursor = body.walk();
2159 for anon_child in body.children(&mut anon_cursor) {
2160 if anon_child.kind() == "field_declaration" {
2161 process_field_declaration(
2162 anon_child,
2163 content,
2164 class_qualified_name,
2165 current_visibility,
2166 helper,
2167 )?;
2168 }
2169 }
2170 handled_nested = true;
2171 }
2172 }
2173
2174 let _ = handled_nested;
2187 process_field_declaration(
2188 child,
2189 content,
2190 class_qualified_name,
2191 current_visibility,
2192 helper,
2193 )?;
2194 }
2195 "function_definition" => {
2196 if let Some(context) = ast_graph.context_for_start(child.start_byte()) {
2199 let span = span_from_node(child);
2200 helper.add_method_with_signature(
2201 &context.qualified_name,
2202 Some(span),
2203 false, context.is_static,
2205 Some(current_visibility),
2206 context.return_type.as_deref(),
2207 );
2208 }
2209 walk_tree_for_graph(
2211 child,
2212 content,
2213 ast_graph,
2214 helper,
2215 seen_includes,
2216 namespace_stack,
2217 class_stack,
2218 ffi_registry,
2219 pure_virtual_registry,
2220 budget,
2221 )?;
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 }
2240
2241 Ok(())
2242}
2243
2244#[allow(clippy::too_many_arguments)]
2246#[allow(clippy::too_many_lines)] fn walk_tree_for_graph(
2248 node: Node,
2249 content: &[u8],
2250 ast_graph: &ASTGraph,
2251 helper: &mut GraphBuildHelper,
2252 seen_includes: &mut HashSet<String>,
2253 namespace_stack: &mut Vec<String>,
2254 class_stack: &mut Vec<String>,
2255 ffi_registry: &FfiRegistry,
2256 pure_virtual_registry: &PureVirtualRegistry,
2257 budget: &mut BuildBudget,
2258) -> GraphResult<()> {
2259 budget.checkpoint("cpp:walk_tree_for_graph")?;
2260 match node.kind() {
2261 "preproc_include" => {
2262 build_import_edge(node, content, helper, seen_includes)?;
2264 }
2265 "linkage_specification" => {
2266 build_ffi_block_for_staging(node, content, helper, namespace_stack);
2268 }
2269 "namespace_definition" => {
2270 if let Some(name_node) = node.child_by_field_name("name")
2272 && let Ok(ns_name) = name_node.utf8_text(content)
2273 {
2274 namespace_stack.push(ns_name.trim().to_string());
2275
2276 let mut cursor = node.walk();
2278 for child in node.children(&mut cursor) {
2279 walk_tree_for_graph(
2280 child,
2281 content,
2282 ast_graph,
2283 helper,
2284 seen_includes,
2285 namespace_stack,
2286 class_stack,
2287 ffi_registry,
2288 pure_virtual_registry,
2289 budget,
2290 )?;
2291 }
2292
2293 namespace_stack.pop();
2294 return Ok(());
2295 }
2296 }
2297 "class_specifier" | "struct_specifier" | "union_specifier" => {
2298 if let Some(name_node) = node.child_by_field_name("name")
2300 && let Ok(class_name) = name_node.utf8_text(content)
2301 {
2302 let class_name = class_name.trim();
2303 let span = span_from_node(node);
2304 let is_struct = matches!(node.kind(), "struct_specifier" | "union_specifier");
2307
2308 let qualified_class =
2310 build_qualified_name(namespace_stack, class_stack, class_name);
2311
2312 let visibility = "public";
2314 let class_id = if is_struct {
2315 helper.add_struct_with_visibility(
2316 &qualified_class,
2317 Some(span),
2318 Some(visibility),
2319 )
2320 } else {
2321 helper.add_class_with_visibility(&qualified_class, Some(span), Some(visibility))
2322 };
2323
2324 build_inheritance_and_implements_edges(
2327 node,
2328 content,
2329 &qualified_class,
2330 class_id,
2331 helper,
2332 namespace_stack,
2333 pure_virtual_registry,
2334 )?;
2335
2336 if class_stack.is_empty() {
2339 let module_id = helper.add_module(FILE_MODULE_NAME, None);
2340 helper.add_export_edge(module_id, class_id);
2341 }
2342
2343 class_stack.push(class_name.to_string());
2345
2346 if let Some(body) = node.child_by_field_name("body") {
2349 walk_class_body(
2350 body,
2351 content,
2352 &qualified_class,
2353 is_struct,
2354 ast_graph,
2355 helper,
2356 seen_includes,
2357 namespace_stack,
2358 class_stack,
2359 ffi_registry,
2360 pure_virtual_registry,
2361 budget,
2362 )?;
2363 }
2364
2365 class_stack.pop();
2366 return Ok(());
2367 }
2368 }
2369 "enum_specifier" => {
2370 if let Some(name_node) = node.child_by_field_name("name")
2371 && let Ok(enum_name) = name_node.utf8_text(content)
2372 {
2373 let enum_name = enum_name.trim();
2374 let span = span_from_node(node);
2375 let qualified_enum = build_qualified_name(namespace_stack, class_stack, enum_name);
2376 let enum_id = helper.add_enum(&qualified_enum, Some(span));
2377
2378 if class_stack.is_empty() {
2379 let module_id = helper.add_module(FILE_MODULE_NAME, None);
2380 helper.add_export_edge(module_id, enum_id);
2381 }
2382 }
2383 }
2384 "function_definition" => {
2385 if !class_stack.is_empty() {
2389 let mut cursor = node.walk();
2392 for child in node.children(&mut cursor) {
2393 walk_tree_for_graph(
2394 child,
2395 content,
2396 ast_graph,
2397 helper,
2398 seen_includes,
2399 namespace_stack,
2400 class_stack,
2401 ffi_registry,
2402 pure_virtual_registry,
2403 budget,
2404 )?;
2405 }
2406 return Ok(());
2407 }
2408
2409 if let Some(context) = ast_graph.context_for_start(node.start_byte()) {
2411 let span = span_from_node(node);
2412
2413 if context.class_stack.is_empty() {
2415 let visibility = if context.is_static {
2418 "private"
2419 } else {
2420 "public"
2421 };
2422 let fn_id = helper.add_function_with_signature(
2423 &context.qualified_name,
2424 Some(span),
2425 false, false, Some(visibility),
2428 context.return_type.as_deref(),
2429 );
2430
2431 if !context.is_static {
2433 let module_id = helper.add_module(FILE_MODULE_NAME, None);
2434 helper.add_export_edge(module_id, fn_id);
2435 }
2436 } else {
2437 helper.add_method_with_signature(
2442 &context.qualified_name,
2443 Some(span),
2444 false, context.is_static,
2446 Some("public"), context.return_type.as_deref(),
2448 );
2449 }
2450 }
2451 }
2452 "call_expression" => {
2453 if let Ok(Some((caller_qname, callee_qname, argument_count, span))) =
2455 build_call_for_staging(ast_graph, node, content)
2456 {
2457 let caller_function_id =
2459 helper.ensure_callee(&caller_qname, span, CalleeKindHint::Function);
2460 let argument_count = u8::try_from(argument_count).unwrap_or(u8::MAX);
2461
2462 let is_unqualified = !callee_qname.contains("::");
2465 if is_unqualified {
2466 if let Some((ffi_qualified, ffi_convention)) = ffi_registry.get(&callee_qname) {
2467 let ffi_target_id =
2469 helper.ensure_callee(ffi_qualified, span, CalleeKindHint::Function);
2470 helper.add_ffi_edge(caller_function_id, ffi_target_id, *ffi_convention);
2471 } else {
2472 let target_function_id =
2474 helper.ensure_callee(&callee_qname, span, CalleeKindHint::Function);
2475 helper.add_call_edge_full_with_span(
2476 caller_function_id,
2477 target_function_id,
2478 argument_count,
2479 false,
2480 vec![span],
2481 );
2482 }
2483 } else {
2484 let target_function_id =
2486 helper.ensure_callee(&callee_qname, span, CalleeKindHint::Function);
2487 helper.add_call_edge_full_with_span(
2488 caller_function_id,
2489 target_function_id,
2490 argument_count,
2491 false,
2492 vec![span],
2493 );
2494 }
2495 }
2496 }
2497 "declaration" => {
2498 if class_stack.is_empty() {
2501 process_global_variable_declaration(node, content, namespace_stack, helper)?;
2502 }
2503 }
2504 _ => {}
2505 }
2506
2507 let mut cursor = node.walk();
2509 for child in node.children(&mut cursor) {
2510 walk_tree_for_graph(
2511 child,
2512 content,
2513 ast_graph,
2514 helper,
2515 seen_includes,
2516 namespace_stack,
2517 class_stack,
2518 ffi_registry,
2519 pure_virtual_registry,
2520 budget,
2521 )?;
2522 }
2523
2524 Ok(())
2525}
2526
2527fn build_call_for_staging(
2529 ast_graph: &ASTGraph,
2530 call_node: Node<'_>,
2531 content: &[u8],
2532) -> GraphResult<Option<(String, String, usize, Span)>> {
2533 let call_context = ast_graph.find_enclosing(call_node.start_byte());
2535 let caller_qualified_name = if let Some(ctx) = call_context {
2536 ctx.qualified_name.clone()
2537 } else {
2538 return Ok(None);
2540 };
2541
2542 let Some(function_node) = call_node.child_by_field_name("function") else {
2543 return Ok(None);
2544 };
2545
2546 let callee_text = function_node
2547 .utf8_text(content)
2548 .map_err(|_| GraphBuilderError::ParseError {
2549 span: span_from_node(call_node),
2550 reason: "failed to read call expression".to_string(),
2551 })?
2552 .trim();
2553
2554 if callee_text.is_empty() {
2555 return Ok(None);
2556 }
2557
2558 let target_qualified_name = if let Some(ctx) = call_context {
2560 resolve_callee_name(function_node, callee_text, ctx, ast_graph, content)
2561 } else {
2562 callee_text.to_string()
2563 };
2564
2565 let span = span_from_node(call_node);
2566 let argument_count = count_arguments(call_node);
2567
2568 Ok(Some((
2569 caller_qualified_name,
2570 target_qualified_name,
2571 argument_count,
2572 span,
2573 )))
2574}
2575
2576fn build_import_edge(
2583 include_node: Node<'_>,
2584 content: &[u8],
2585 helper: &mut GraphBuildHelper,
2586 seen_includes: &mut HashSet<String>,
2587) -> GraphResult<()> {
2588 let path_node = include_node.child_by_field_name("path").or_else(|| {
2590 let mut cursor = include_node.walk();
2592 include_node.children(&mut cursor).find(|child| {
2593 matches!(
2594 child.kind(),
2595 "system_lib_string" | "string_literal" | "string_content"
2596 )
2597 })
2598 });
2599
2600 let Some(path_node) = path_node else {
2601 return Ok(());
2602 };
2603
2604 let include_path = path_node
2605 .utf8_text(content)
2606 .map_err(|_| GraphBuilderError::ParseError {
2607 span: span_from_node(include_node),
2608 reason: "failed to read include path".to_string(),
2609 })?
2610 .trim();
2611
2612 if include_path.is_empty() {
2613 return Ok(());
2614 }
2615
2616 let is_system_include = include_path.starts_with('<') && include_path.ends_with('>');
2618 let cleaned_path = if is_system_include {
2619 include_path.trim_start_matches('<').trim_end_matches('>')
2621 } else {
2622 include_path.trim_start_matches('"').trim_end_matches('"')
2624 };
2625
2626 if cleaned_path.is_empty() {
2627 return Ok(());
2628 }
2629
2630 if !seen_includes.insert(cleaned_path.to_string()) {
2632 return Ok(()); }
2634
2635 let file_module_id = helper.add_module("<file>", None);
2637
2638 let span = span_from_node(include_node);
2640 let import_id = helper.add_import(cleaned_path, Some(span));
2641
2642 helper.add_import_edge(file_module_id, import_id);
2645
2646 Ok(())
2647}
2648
2649fn collect_ffi_declarations(
2660 node: Node<'_>,
2661 content: &[u8],
2662 ffi_registry: &mut FfiRegistry,
2663 budget: &mut BuildBudget,
2664) -> GraphResult<()> {
2665 budget.checkpoint("cpp:collect_ffi_declarations")?;
2666 if node.kind() == "linkage_specification" {
2667 let abi = extract_ffi_abi(node, content);
2669 let convention = abi_to_convention(&abi);
2670
2671 if let Some(body_node) = node.child_by_field_name("body") {
2673 collect_ffi_from_body(body_node, content, &abi, convention, ffi_registry);
2674 }
2675 }
2676
2677 let mut cursor = node.walk();
2679 for child in node.children(&mut cursor) {
2680 collect_ffi_declarations(child, content, ffi_registry, budget)?;
2681 }
2682
2683 Ok(())
2684}
2685
2686fn collect_ffi_from_body(
2688 body_node: Node<'_>,
2689 content: &[u8],
2690 abi: &str,
2691 convention: FfiConvention,
2692 ffi_registry: &mut FfiRegistry,
2693) {
2694 match body_node.kind() {
2695 "declaration_list" => {
2696 let mut cursor = body_node.walk();
2698 for decl in body_node.children(&mut cursor) {
2699 if decl.kind() == "declaration"
2700 && let Some(fn_name) = extract_ffi_function_name(decl, content)
2701 {
2702 let qualified = format!("extern::{abi}::{fn_name}");
2703 ffi_registry.insert(fn_name, (qualified, convention));
2704 }
2705 }
2706 }
2707 "declaration" => {
2708 if let Some(fn_name) = extract_ffi_function_name(body_node, content) {
2710 let qualified = format!("extern::{abi}::{fn_name}");
2711 ffi_registry.insert(fn_name, (qualified, convention));
2712 }
2713 }
2714 _ => {}
2715 }
2716}
2717
2718fn extract_ffi_function_name(decl_node: Node<'_>, content: &[u8]) -> Option<String> {
2720 if let Some(declarator_node) = decl_node.child_by_field_name("declarator") {
2722 return extract_function_name_from_declarator(declarator_node, content);
2723 }
2724 None
2725}
2726
2727fn extract_function_name_from_declarator(node: Node<'_>, content: &[u8]) -> Option<String> {
2729 match node.kind() {
2730 "function_declarator" => {
2731 if let Some(inner) = node.child_by_field_name("declarator") {
2733 return extract_function_name_from_declarator(inner, content);
2734 }
2735 }
2736 "identifier" => {
2737 if let Ok(name) = node.utf8_text(content) {
2739 let name = name.trim();
2740 if !name.is_empty() {
2741 return Some(name.to_string());
2742 }
2743 }
2744 }
2745 "pointer_declarator" | "reference_declarator" => {
2746 if let Some(inner) = node.child_by_field_name("declarator") {
2748 return extract_function_name_from_declarator(inner, content);
2749 }
2750 }
2751 "parenthesized_declarator" => {
2752 let mut cursor = node.walk();
2754 for child in node.children(&mut cursor) {
2755 if let Some(name) = extract_function_name_from_declarator(child, content) {
2756 return Some(name);
2757 }
2758 }
2759 }
2760 _ => {}
2761 }
2762 None
2763}
2764
2765fn extract_ffi_abi(node: Node<'_>, content: &[u8]) -> String {
2769 if let Some(value_node) = node.child_by_field_name("value")
2771 && value_node.kind() == "string_literal"
2772 {
2773 let mut cursor = value_node.walk();
2775 for child in value_node.children(&mut cursor) {
2776 if child.kind() == "string_content"
2777 && let Ok(text) = child.utf8_text(content)
2778 {
2779 let trimmed = text.trim();
2780 if !trimmed.is_empty() {
2781 return trimmed.to_string();
2782 }
2783 }
2784 }
2785 }
2786 "C".to_string()
2788}
2789
2790fn abi_to_convention(abi: &str) -> FfiConvention {
2792 match abi.to_lowercase().as_str() {
2793 "system" => FfiConvention::System,
2794 "stdcall" => FfiConvention::Stdcall,
2795 "fastcall" => FfiConvention::Fastcall,
2796 "cdecl" => FfiConvention::Cdecl,
2797 _ => FfiConvention::C, }
2799}
2800
2801fn build_ffi_block_for_staging(
2805 node: Node<'_>,
2806 content: &[u8],
2807 helper: &mut GraphBuildHelper,
2808 namespace_stack: &[String],
2809) {
2810 let abi = extract_ffi_abi(node, content);
2812
2813 if let Some(body_node) = node.child_by_field_name("body") {
2815 build_ffi_from_body(body_node, content, &abi, helper, namespace_stack);
2816 }
2817}
2818
2819fn build_ffi_from_body(
2821 body_node: Node<'_>,
2822 content: &[u8],
2823 abi: &str,
2824 helper: &mut GraphBuildHelper,
2825 namespace_stack: &[String],
2826) {
2827 match body_node.kind() {
2828 "declaration_list" => {
2829 let mut cursor = body_node.walk();
2831 for decl in body_node.children(&mut cursor) {
2832 if decl.kind() == "declaration"
2833 && let Some(fn_name) = extract_ffi_function_name(decl, content)
2834 {
2835 let span = span_from_node(decl);
2836 let qualified = if namespace_stack.is_empty() {
2838 format!("extern::{abi}::{fn_name}")
2839 } else {
2840 format!("{}::extern::{abi}::{fn_name}", namespace_stack.join("::"))
2841 };
2842 helper.add_function(
2844 &qualified,
2845 Some(span),
2846 false, true, );
2849 }
2850 }
2851 }
2852 "declaration" => {
2853 if let Some(fn_name) = extract_ffi_function_name(body_node, content) {
2855 let span = span_from_node(body_node);
2856 let qualified = if namespace_stack.is_empty() {
2857 format!("extern::{abi}::{fn_name}")
2858 } else {
2859 format!("{}::extern::{abi}::{fn_name}", namespace_stack.join("::"))
2860 };
2861 helper.add_function(&qualified, Some(span), false, true);
2862 }
2863 }
2864 _ => {}
2865 }
2866}
2867
2868fn collect_pure_virtual_interfaces(
2878 node: Node<'_>,
2879 content: &[u8],
2880 registry: &mut PureVirtualRegistry,
2881 budget: &mut BuildBudget,
2882) -> GraphResult<()> {
2883 budget.checkpoint("cpp:collect_pure_virtual_interfaces")?;
2884 if matches!(node.kind(), "class_specifier" | "struct_specifier")
2885 && let Some(name_node) = node.child_by_field_name("name")
2886 && let Ok(class_name) = name_node.utf8_text(content)
2887 {
2888 let class_name = class_name.trim();
2889 if !class_name.is_empty() && has_pure_virtual_methods(node, content) {
2890 registry.insert(class_name.to_string());
2891 }
2892 }
2893
2894 let mut cursor = node.walk();
2896 for child in node.children(&mut cursor) {
2897 collect_pure_virtual_interfaces(child, content, registry, budget)?;
2898 }
2899
2900 Ok(())
2901}
2902
2903fn has_pure_virtual_methods(class_node: Node<'_>, content: &[u8]) -> bool {
2907 if let Some(body) = class_node.child_by_field_name("body") {
2908 let mut cursor = body.walk();
2909 for child in body.children(&mut cursor) {
2910 if child.kind() == "field_declaration" && is_pure_virtual_declaration(child, content) {
2912 return true;
2913 }
2914 }
2915 }
2916 false
2917}
2918
2919fn is_pure_virtual_declaration(decl_node: Node<'_>, content: &[u8]) -> bool {
2921 let mut has_virtual = false;
2922 let mut has_pure_specifier = false;
2923
2924 let mut cursor = decl_node.walk();
2926 for child in decl_node.children(&mut cursor) {
2927 match child.kind() {
2928 "virtual" => {
2929 has_virtual = true;
2930 }
2931 "number_literal" => {
2932 if let Ok(text) = child.utf8_text(content)
2935 && text.trim() == "0"
2936 {
2937 has_pure_specifier = true;
2938 }
2939 }
2940 _ => {}
2941 }
2942 }
2943
2944 has_virtual && has_pure_specifier
2945}
2946
2947fn build_inheritance_and_implements_edges(
2953 class_node: Node<'_>,
2954 content: &[u8],
2955 _qualified_class_name: &str,
2956 child_id: sqry_core::graph::unified::node::NodeId,
2957 helper: &mut GraphBuildHelper,
2958 namespace_stack: &[String],
2959 pure_virtual_registry: &PureVirtualRegistry,
2960) -> GraphResult<()> {
2961 let mut cursor = class_node.walk();
2963 let base_clause = class_node
2964 .children(&mut cursor)
2965 .find(|child| child.kind() == "base_class_clause");
2966
2967 let Some(base_clause) = base_clause else {
2968 return Ok(()); };
2970
2971 let mut clause_cursor = base_clause.walk();
2973 for child in base_clause.children(&mut clause_cursor) {
2974 match child.kind() {
2975 "type_identifier" => {
2976 let base_name = child
2977 .utf8_text(content)
2978 .map_err(|_| GraphBuilderError::ParseError {
2979 span: span_from_node(child),
2980 reason: "failed to read base class name".to_string(),
2981 })?
2982 .trim();
2983
2984 if !base_name.is_empty() {
2985 let qualified_base = if namespace_stack.is_empty() {
2987 base_name.to_string()
2988 } else {
2989 format!("{}::{}", namespace_stack.join("::"), base_name)
2990 };
2991
2992 if pure_virtual_registry.contains(base_name) {
2994 let interface_id = helper.add_interface(&qualified_base, None);
2996 helper.add_implements_edge(child_id, interface_id);
2997 } else {
2998 let parent_id = helper.add_class(&qualified_base, None);
3000 helper.add_inherits_edge(child_id, parent_id);
3001 }
3002 }
3003 }
3004 "qualified_identifier" => {
3005 let base_name = child
3007 .utf8_text(content)
3008 .map_err(|_| GraphBuilderError::ParseError {
3009 span: span_from_node(child),
3010 reason: "failed to read base class name".to_string(),
3011 })?
3012 .trim();
3013
3014 if !base_name.is_empty() {
3015 let simple_name = base_name.rsplit("::").next().unwrap_or(base_name);
3017
3018 if pure_virtual_registry.contains(simple_name) {
3019 let interface_id = helper.add_interface(base_name, None);
3020 helper.add_implements_edge(child_id, interface_id);
3021 } else {
3022 let parent_id = helper.add_class(base_name, None);
3023 helper.add_inherits_edge(child_id, parent_id);
3024 }
3025 }
3026 }
3027 "template_type" => {
3028 if let Some(template_name_node) = child.child_by_field_name("name")
3030 && let Ok(base_name) = template_name_node.utf8_text(content)
3031 {
3032 let base_name = base_name.trim();
3033 if !base_name.is_empty() {
3034 let qualified_base =
3035 if base_name.contains("::") || namespace_stack.is_empty() {
3036 base_name.to_string()
3037 } else {
3038 format!("{}::{}", namespace_stack.join("::"), base_name)
3039 };
3040
3041 if pure_virtual_registry.contains(base_name) {
3044 let interface_id = helper.add_interface(&qualified_base, None);
3045 helper.add_implements_edge(child_id, interface_id);
3046 } else {
3047 let parent_id = helper.add_class(&qualified_base, None);
3048 helper.add_inherits_edge(child_id, parent_id);
3049 }
3050 }
3051 }
3052 }
3053 _ => {
3054 }
3056 }
3057 }
3058
3059 Ok(())
3060}
3061
3062fn span_from_node(node: Node<'_>) -> Span {
3063 let start = node.start_position();
3064 let end = node.end_position();
3065 Span::new(
3066 sqry_core::graph::node::Position::new(start.row, start.column),
3067 sqry_core::graph::node::Position::new(end.row, end.column),
3068 )
3069}
3070
3071fn count_arguments(node: Node<'_>) -> usize {
3072 node.child_by_field_name("arguments").map_or(0, |args| {
3073 let mut count = 0;
3074 let mut cursor = args.walk();
3075 for child in args.children(&mut cursor) {
3076 if !matches!(child.kind(), "(" | ")" | ",") {
3077 count += 1;
3078 }
3079 }
3080 count
3081 })
3082}
3083
3084#[cfg(test)]
3085mod tests {
3086 use super::*;
3087 use sqry_core::graph::unified::build::test_helpers::{
3088 assert_has_ffi_call_edge, assert_has_node, assert_has_node_with_kind,
3089 assert_has_node_with_kind_exact, collect_call_edges,
3090 };
3091 use sqry_core::graph::unified::node::NodeKind;
3092 use tree_sitter::Parser;
3093
3094 fn parse_cpp(source: &str) -> Tree {
3095 let mut parser = Parser::new();
3096 parser
3097 .set_language(&tree_sitter_cpp::LANGUAGE.into())
3098 .expect("Failed to set Cpp language");
3099 parser
3100 .parse(source.as_bytes(), None)
3101 .expect("Failed to parse Cpp source")
3102 }
3103
3104 fn test_budget() -> BuildBudget {
3105 BuildBudget::new(Path::new("test.cpp"))
3106 }
3107
3108 fn extract_namespace_map_for_test(
3109 tree: &Tree,
3110 source: &str,
3111 ) -> HashMap<std::ops::Range<usize>, String> {
3112 let mut budget = test_budget();
3113 extract_namespace_map(tree.root_node(), source.as_bytes(), &mut budget)
3114 .expect("namespace extraction should succeed in tests")
3115 }
3116
3117 fn extract_cpp_contexts_for_test(
3118 tree: &Tree,
3119 source: &str,
3120 namespace_map: &HashMap<std::ops::Range<usize>, String>,
3121 ) -> Vec<FunctionContext> {
3122 let mut budget = test_budget();
3123 extract_cpp_contexts(
3124 tree.root_node(),
3125 source.as_bytes(),
3126 namespace_map,
3127 &mut budget,
3128 )
3129 .expect("context extraction should succeed in tests")
3130 }
3131
3132 fn extract_field_and_type_info_for_test(
3133 tree: &Tree,
3134 source: &str,
3135 namespace_map: &HashMap<std::ops::Range<usize>, String>,
3136 ) -> (QualifiedNameMap, QualifiedNameMap) {
3137 let mut budget = test_budget();
3138 extract_field_and_type_info(
3139 tree.root_node(),
3140 source.as_bytes(),
3141 namespace_map,
3142 &mut budget,
3143 )
3144 .expect("field/type extraction should succeed in tests")
3145 }
3146
3147 #[test]
3148 fn test_build_graph_times_out_with_expired_budget() {
3149 let source = r"
3150 namespace demo {
3151 class Service {
3152 public:
3153 void process() {}
3154 };
3155 }
3156 ";
3157 let tree = parse_cpp(source);
3158 let builder = CppGraphBuilder::new();
3159 let mut staging = StagingGraph::new();
3160 let mut budget = BuildBudget::already_expired(Path::new("timeout.cpp"));
3161
3162 let err = builder
3163 .build_graph_with_budget(
3164 &tree,
3165 source.as_bytes(),
3166 Path::new("timeout.cpp"),
3167 &mut staging,
3168 &mut budget,
3169 )
3170 .expect_err("expired budget should force timeout");
3171
3172 match err {
3173 GraphBuilderError::BuildTimedOut {
3174 file,
3175 phase,
3176 timeout_ms,
3177 } => {
3178 assert_eq!(file, PathBuf::from("timeout.cpp"));
3179 assert_eq!(phase, "cpp:extract_namespace_map");
3180 assert_eq!(timeout_ms, 1_000);
3181 }
3182 other => panic!("expected BuildTimedOut, got {other:?}"),
3183 }
3184 }
3185
3186 #[test]
3187 fn test_extract_class() {
3188 let source = "class User { }";
3189 let tree = parse_cpp(source);
3190 let mut staging = StagingGraph::new();
3191 let builder = CppGraphBuilder::new();
3192
3193 let result = builder.build_graph(
3194 &tree,
3195 source.as_bytes(),
3196 Path::new("test.cpp"),
3197 &mut staging,
3198 );
3199
3200 assert!(result.is_ok());
3201 assert_has_node_with_kind(&staging, "User", NodeKind::Class);
3202 }
3203
3204 #[test]
3205 fn test_extract_template_class() {
3206 let source = r"
3207 template <typename T>
3208 class Person {
3209 public:
3210 T name;
3211 T age;
3212 };
3213 ";
3214 let tree = parse_cpp(source);
3215 let mut staging = StagingGraph::new();
3216 let builder = CppGraphBuilder::new();
3217
3218 let result = builder.build_graph(
3219 &tree,
3220 source.as_bytes(),
3221 Path::new("test.cpp"),
3222 &mut staging,
3223 );
3224
3225 assert!(result.is_ok());
3226 assert_has_node_with_kind(&staging, "Person", NodeKind::Class);
3227 }
3228
3229 #[test]
3230 fn test_nested_named_types_emit_nodes() {
3231 let source = r"
3236 class Outer {
3237 public:
3238 class Inner { int z; };
3239 struct InnerS { int w; };
3240 union InnerU { int i; float f; };
3241 enum class InnerE { A, B };
3242 class L1 { public: class L2 { int q; }; };
3243 };
3244 namespace ns {
3245 class NsOuter { public: class NsInner { int n; }; };
3246 }
3247 ";
3248 let staging = build_cpp(source);
3249
3250 assert_has_node_with_kind_exact(&staging, "Outer::Inner", NodeKind::Class);
3252 assert_has_node_with_kind_exact(&staging, "Outer::InnerS", NodeKind::Struct);
3253 assert_has_node_with_kind_exact(&staging, "Outer::InnerU", NodeKind::Struct);
3255 assert_has_node_with_kind_exact(&staging, "Outer::InnerE", NodeKind::Enum);
3256 assert_has_node_with_kind_exact(&staging, "Outer::L1", NodeKind::Class);
3258 assert_has_node_with_kind_exact(&staging, "Outer::L1::L2", NodeKind::Class);
3259 assert_has_node_with_kind_exact(&staging, "ns::NsOuter", NodeKind::Class);
3261 assert_has_node_with_kind_exact(&staging, "ns::NsOuter::NsInner", NodeKind::Class);
3262
3263 assert_has_node_with_kind_exact(&staging, "Outer::Inner.z", NodeKind::Property);
3266 assert_has_node_with_kind_exact(&staging, "Outer::L1::L2.q", NodeKind::Property);
3267 assert_has_node_with_kind_exact(&staging, "ns::NsOuter::NsInner.n", NodeKind::Property);
3268 }
3269
3270 #[test]
3271 fn test_nested_enum_carries_enclosing_visibility() {
3272 let source = r"
3276 class Outer {
3277 private:
3278 enum class Secret { A, B };
3279 public:
3280 enum class Pub { X, Y };
3281 };
3282 ";
3283 let staging = build_cpp(source);
3284
3285 let secret = cpp_find_added_node(&staging, "Outer::Secret")
3286 .expect("nested enum Outer::Secret must be staged");
3287 assert_eq!(secret.kind, NodeKind::Enum, "Secret must be an Enum node");
3288 let secret_vis = staging.resolve_local_string(
3289 secret
3290 .visibility
3291 .expect("nested enum must carry a visibility id"),
3292 );
3293 assert_eq!(
3294 secret_vis,
3295 Some("private"),
3296 "nested enum under `private:` must be private"
3297 );
3298
3299 let pub_enum = cpp_find_added_node(&staging, "Outer::Pub")
3300 .expect("nested enum Outer::Pub must be staged");
3301 let pub_vis = staging.resolve_local_string(
3302 pub_enum
3303 .visibility
3304 .expect("nested enum must carry a visibility id"),
3305 );
3306 assert_eq!(
3307 pub_vis,
3308 Some("public"),
3309 "nested enum under `public:` must be public"
3310 );
3311 }
3312
3313 #[test]
3314 fn test_nested_class_emits_inheritance_edge() {
3315 let source = r"
3319 struct Base { virtual ~Base(); };
3320 class Outer {
3321 public:
3322 class Derived : public Base {};
3323 };
3324 ";
3325 let staging = build_cpp(source);
3326
3327 let derived_id = cpp_find_added_node_id(&staging, "Outer::Derived", NodeKind::Class)
3328 .expect("nested Derived class node must be staged");
3329
3330 let has_inherits = staging.operations().iter().any(|op| {
3331 matches!(
3332 op,
3333 StagingOp::AddEdge {
3334 source: src,
3335 kind: EdgeKind::Inherits,
3336 ..
3337 } if *src == derived_id
3338 )
3339 });
3340 assert!(
3341 has_inherits,
3342 "nested Derived must emit an Inherits edge to its base"
3343 );
3344 }
3345
3346 #[test]
3347 fn test_top_level_union_emits_struct_node() {
3348 let source = "union Value { int i; float f; };";
3352 let staging = build_cpp(source);
3353 assert_has_node_with_kind_exact(&staging, "Value", NodeKind::Struct);
3354 }
3355
3356 #[test]
3357 fn test_extract_function() {
3358 let source = r#"
3359 #include <cstdio>
3360 void hello() {
3361 std::printf("Hello");
3362 }
3363 "#;
3364 let tree = parse_cpp(source);
3365 let mut staging = StagingGraph::new();
3366 let builder = CppGraphBuilder::new();
3367
3368 let result = builder.build_graph(
3369 &tree,
3370 source.as_bytes(),
3371 Path::new("test.cpp"),
3372 &mut staging,
3373 );
3374
3375 assert!(result.is_ok());
3376 assert_has_node_with_kind(&staging, "hello", NodeKind::Function);
3377 }
3378
3379 #[test]
3380 fn test_extract_virtual_function() {
3381 let source = r"
3382 class Service {
3383 public:
3384 virtual void fetchData() {}
3385 };
3386 ";
3387 let tree = parse_cpp(source);
3388 let mut staging = StagingGraph::new();
3389 let builder = CppGraphBuilder::new();
3390
3391 let result = builder.build_graph(
3392 &tree,
3393 source.as_bytes(),
3394 Path::new("test.cpp"),
3395 &mut staging,
3396 );
3397
3398 assert!(result.is_ok());
3399 assert_has_node(&staging, "fetchData");
3400 }
3401
3402 #[test]
3403 fn test_extract_call_edge() {
3404 let source = r"
3405 void greet() {}
3406
3407 int main() {
3408 greet();
3409 return 0;
3410 }
3411 ";
3412 let tree = parse_cpp(source);
3413 let mut staging = StagingGraph::new();
3414 let builder = CppGraphBuilder::new();
3415
3416 let result = builder.build_graph(
3417 &tree,
3418 source.as_bytes(),
3419 Path::new("test.cpp"),
3420 &mut staging,
3421 );
3422
3423 assert!(result.is_ok());
3424 assert_has_node(&staging, "main");
3425 assert_has_node(&staging, "greet");
3426 let calls = collect_call_edges(&staging);
3427 assert!(!calls.is_empty());
3428 }
3429
3430 #[test]
3431 fn test_extract_member_call_edge() {
3432 let source = r"
3433 class Service {
3434 public:
3435 void helper() {}
3436 };
3437
3438 int main() {
3439 Service svc;
3440 svc.helper();
3441 return 0;
3442 }
3443 ";
3444 let tree = parse_cpp(source);
3445 let mut staging = StagingGraph::new();
3446 let builder = CppGraphBuilder::new();
3447
3448 let result = builder.build_graph(
3449 &tree,
3450 source.as_bytes(),
3451 Path::new("member.cpp"),
3452 &mut staging,
3453 );
3454
3455 assert!(result.is_ok());
3456 assert_has_node(&staging, "main");
3457 assert_has_node(&staging, "helper");
3458 let calls = collect_call_edges(&staging);
3459 assert!(!calls.is_empty());
3460 }
3461
3462 #[test]
3463 fn test_extract_namespace_map_simple() {
3464 let source = r"
3465 namespace demo {
3466 void func() {}
3467 }
3468 ";
3469 let tree = parse_cpp(source);
3470 let namespace_map = extract_namespace_map_for_test(&tree, source);
3471
3472 assert_eq!(namespace_map.len(), 1);
3474
3475 let (_, ns_prefix) = namespace_map.iter().next().unwrap();
3477 assert_eq!(ns_prefix, "demo::");
3478 }
3479
3480 #[test]
3481 fn test_extract_namespace_map_nested() {
3482 let source = r"
3483 namespace outer {
3484 namespace inner {
3485 void func() {}
3486 }
3487 }
3488 ";
3489 let tree = parse_cpp(source);
3490 let namespace_map = extract_namespace_map_for_test(&tree, source);
3491
3492 assert!(namespace_map.len() >= 2);
3494
3495 let ns_values: Vec<&String> = namespace_map.values().collect();
3497 assert!(ns_values.iter().any(|v| v.as_str() == "outer::"));
3498 assert!(ns_values.iter().any(|v| v.as_str() == "outer::inner::"));
3499 }
3500
3501 #[test]
3502 fn test_extract_namespace_map_multiple() {
3503 let source = r"
3504 namespace first {
3505 void func1() {}
3506 }
3507 namespace second {
3508 void func2() {}
3509 }
3510 ";
3511 let tree = parse_cpp(source);
3512 let namespace_map = extract_namespace_map_for_test(&tree, source);
3513
3514 assert_eq!(namespace_map.len(), 2);
3516
3517 let ns_values: Vec<&String> = namespace_map.values().collect();
3518 assert!(ns_values.iter().any(|v| v.as_str() == "first::"));
3519 assert!(ns_values.iter().any(|v| v.as_str() == "second::"));
3520 }
3521
3522 #[test]
3523 fn test_find_namespace_for_offset() {
3524 let source = r"
3525 namespace demo {
3526 void func() {}
3527 }
3528 ";
3529 let tree = parse_cpp(source);
3530 let namespace_map = extract_namespace_map_for_test(&tree, source);
3531
3532 let func_offset = source.find("func").unwrap();
3534 let ns = find_namespace_for_offset(func_offset, &namespace_map);
3535 assert_eq!(ns, "demo::");
3536
3537 let ns = find_namespace_for_offset(0, &namespace_map);
3539 assert_eq!(ns, "");
3540 }
3541
3542 #[test]
3543 fn test_extract_cpp_contexts_free_function() {
3544 let source = r"
3545 void helper() {}
3546 ";
3547 let tree = parse_cpp(source);
3548 let namespace_map = extract_namespace_map_for_test(&tree, source);
3549 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3550
3551 assert_eq!(contexts.len(), 1);
3552 assert_eq!(contexts[0].qualified_name, "helper");
3553 assert!(!contexts[0].is_static);
3554 assert!(!contexts[0].is_virtual);
3555 }
3556
3557 #[test]
3558 fn test_extract_cpp_contexts_namespace_function() {
3559 let source = r"
3560 namespace demo {
3561 void helper() {}
3562 }
3563 ";
3564 let tree = parse_cpp(source);
3565 let namespace_map = extract_namespace_map_for_test(&tree, source);
3566 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3567
3568 assert_eq!(contexts.len(), 1);
3569 assert_eq!(contexts[0].qualified_name, "demo::helper");
3570 assert_eq!(contexts[0].namespace_stack, vec!["demo"]);
3571 }
3572
3573 #[test]
3574 fn test_extract_cpp_contexts_class_method() {
3575 let source = r"
3576 class Service {
3577 public:
3578 void process() {}
3579 };
3580 ";
3581 let tree = parse_cpp(source);
3582 let namespace_map = extract_namespace_map_for_test(&tree, source);
3583 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3584
3585 assert_eq!(contexts.len(), 1);
3586 assert_eq!(contexts[0].qualified_name, "Service::process");
3587 assert_eq!(contexts[0].class_stack, vec!["Service"]);
3588 }
3589
3590 #[test]
3591 fn test_extract_cpp_contexts_namespace_and_class() {
3592 let source = r"
3593 namespace demo {
3594 class Service {
3595 public:
3596 void process() {}
3597 };
3598 }
3599 ";
3600 let tree = parse_cpp(source);
3601 let namespace_map = extract_namespace_map_for_test(&tree, source);
3602 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3603
3604 assert_eq!(contexts.len(), 1);
3605 assert_eq!(contexts[0].qualified_name, "demo::Service::process");
3606 assert_eq!(contexts[0].namespace_stack, vec!["demo"]);
3607 assert_eq!(contexts[0].class_stack, vec!["Service"]);
3608 }
3609
3610 #[test]
3611 fn test_extract_cpp_contexts_static_method() {
3612 let source = r"
3613 class Repository {
3614 public:
3615 static void save() {}
3616 };
3617 ";
3618 let tree = parse_cpp(source);
3619 let namespace_map = extract_namespace_map_for_test(&tree, source);
3620 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3621
3622 assert_eq!(contexts.len(), 1);
3623 assert_eq!(contexts[0].qualified_name, "Repository::save");
3624 assert!(contexts[0].is_static);
3625 }
3626
3627 #[test]
3628 fn test_extract_cpp_contexts_virtual_method() {
3629 let source = r"
3630 class Base {
3631 public:
3632 virtual void render() {}
3633 };
3634 ";
3635 let tree = parse_cpp(source);
3636 let namespace_map = extract_namespace_map_for_test(&tree, source);
3637 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3638
3639 assert_eq!(contexts.len(), 1);
3640 assert_eq!(contexts[0].qualified_name, "Base::render");
3641 assert!(contexts[0].is_virtual);
3642 }
3643
3644 #[test]
3645 fn test_extract_cpp_contexts_inline_function() {
3646 let source = r"
3647 inline void helper() {}
3648 ";
3649 let tree = parse_cpp(source);
3650 let namespace_map = extract_namespace_map_for_test(&tree, source);
3651 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3652
3653 assert_eq!(contexts.len(), 1);
3654 assert_eq!(contexts[0].qualified_name, "helper");
3655 assert!(contexts[0].is_inline);
3656 }
3657
3658 #[test]
3659 fn test_extract_cpp_contexts_out_of_line_definition() {
3660 let source = r"
3661 namespace demo {
3662 class Service {
3663 public:
3664 int process(int v);
3665 };
3666
3667 inline int Service::process(int v) {
3668 return v;
3669 }
3670 }
3671 ";
3672 let tree = parse_cpp(source);
3673 let namespace_map = extract_namespace_map_for_test(&tree, source);
3674 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3675
3676 assert_eq!(contexts.len(), 1);
3678 assert_eq!(contexts[0].qualified_name, "demo::Service::process");
3679 assert!(contexts[0].is_inline);
3680 }
3681
3682 #[test]
3683 fn test_extract_field_types_simple() {
3684 let source = r"
3685 class Service {
3686 public:
3687 Repository repo;
3688 };
3689 ";
3690 let tree = parse_cpp(source);
3691 let namespace_map = extract_namespace_map_for_test(&tree, source);
3692 let (field_types, _type_map) =
3693 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3694
3695 assert_eq!(field_types.len(), 1);
3697 assert_eq!(
3698 field_types.get(&("Service".to_string(), "repo".to_string())),
3699 Some(&"Repository".to_string())
3700 );
3701 }
3702
3703 #[test]
3704 fn test_extract_field_types_namespace() {
3705 let source = r"
3706 namespace demo {
3707 class Service {
3708 public:
3709 Repository repo;
3710 };
3711 }
3712 ";
3713 let tree = parse_cpp(source);
3714 let namespace_map = extract_namespace_map_for_test(&tree, source);
3715 let (field_types, _type_map) =
3716 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3717
3718 assert_eq!(field_types.len(), 1);
3720 assert_eq!(
3721 field_types.get(&("demo::Service".to_string(), "repo".to_string())),
3722 Some(&"Repository".to_string())
3723 );
3724 }
3725
3726 #[test]
3727 fn test_extract_field_types_no_collision() {
3728 let source = r"
3729 class ServiceA {
3730 public:
3731 Repository repo;
3732 };
3733
3734 class ServiceB {
3735 public:
3736 Repository repo;
3737 };
3738 ";
3739 let tree = parse_cpp(source);
3740 let namespace_map = extract_namespace_map_for_test(&tree, source);
3741 let (field_types, _type_map) =
3742 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3743
3744 assert_eq!(field_types.len(), 2);
3746 assert_eq!(
3747 field_types.get(&("ServiceA".to_string(), "repo".to_string())),
3748 Some(&"Repository".to_string())
3749 );
3750 assert_eq!(
3751 field_types.get(&("ServiceB".to_string(), "repo".to_string())),
3752 Some(&"Repository".to_string())
3753 );
3754 }
3755
3756 #[test]
3757 fn test_extract_using_declaration() {
3758 let source = r"
3759 using std::vector;
3760
3761 class Service {
3762 public:
3763 vector data;
3764 };
3765 ";
3766 let tree = parse_cpp(source);
3767 let namespace_map = extract_namespace_map_for_test(&tree, source);
3768 let (field_types, type_map) =
3769 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3770
3771 assert_eq!(field_types.len(), 1);
3773 assert_eq!(
3774 field_types.get(&("Service".to_string(), "data".to_string())),
3775 Some(&"std::vector".to_string()),
3776 "Field type should resolve 'vector' to 'std::vector' via using declaration"
3777 );
3778
3779 assert_eq!(
3781 type_map.get(&(String::new(), "vector".to_string())),
3782 Some(&"std::vector".to_string()),
3783 "Using declaration should map 'vector' to 'std::vector' in type_map"
3784 );
3785 }
3786
3787 #[test]
3788 fn test_extract_field_types_pointer() {
3789 let source = r"
3790 class Service {
3791 public:
3792 Repository* repo;
3793 };
3794 ";
3795 let tree = parse_cpp(source);
3796 let namespace_map = extract_namespace_map_for_test(&tree, source);
3797 let (field_types, _type_map) =
3798 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3799
3800 assert_eq!(field_types.len(), 1);
3802 assert_eq!(
3803 field_types.get(&("Service".to_string(), "repo".to_string())),
3804 Some(&"Repository".to_string())
3805 );
3806 }
3807
3808 #[test]
3809 fn test_extract_field_types_multiple_declarators() {
3810 let source = r"
3811 class Service {
3812 public:
3813 Repository repo_a, repo_b, repo_c;
3814 };
3815 ";
3816 let tree = parse_cpp(source);
3817 let namespace_map = extract_namespace_map_for_test(&tree, source);
3818 let (field_types, _type_map) =
3819 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3820
3821 assert_eq!(field_types.len(), 3);
3823 assert_eq!(
3824 field_types.get(&("Service".to_string(), "repo_a".to_string())),
3825 Some(&"Repository".to_string())
3826 );
3827 assert_eq!(
3828 field_types.get(&("Service".to_string(), "repo_b".to_string())),
3829 Some(&"Repository".to_string())
3830 );
3831 assert_eq!(
3832 field_types.get(&("Service".to_string(), "repo_c".to_string())),
3833 Some(&"Repository".to_string())
3834 );
3835 }
3836
3837 #[test]
3838 fn test_extract_field_types_nested_struct_with_parent_field() {
3839 let source = r"
3842 namespace demo {
3843 struct Outer {
3844 int outer_field;
3845 struct Inner {
3846 int inner_field;
3847 };
3848 Inner nested_instance;
3849 };
3850 }
3851 ";
3852 let tree = parse_cpp(source);
3853 let namespace_map = extract_namespace_map_for_test(&tree, source);
3854 let (field_types, _type_map) =
3855 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3856
3857 assert!(
3860 field_types.len() >= 2,
3861 "Expected at least outer_field and nested_instance"
3862 );
3863
3864 assert_eq!(
3866 field_types.get(&("demo::Outer".to_string(), "outer_field".to_string())),
3867 Some(&"int".to_string())
3868 );
3869
3870 assert_eq!(
3876 field_types.get(&("demo::Outer".to_string(), "nested_instance".to_string())),
3877 Some(&"demo::Outer::Inner".to_string())
3878 );
3879
3880 if field_types.contains_key(&("demo::Outer::Inner".to_string(), "inner_field".to_string()))
3882 {
3883 assert_eq!(
3885 field_types.get(&("demo::Outer::Inner".to_string(), "inner_field".to_string())),
3886 Some(&"int".to_string()),
3887 "Inner class fields must use parent-qualified FQN 'demo::Outer::Inner'"
3888 );
3889 }
3890 }
3891
3892 use sqry_core::graph::unified::build::staging::StagingOp;
3909 use sqry_core::graph::unified::edge::kind::{EdgeKind, TypeOfContext};
3910
3911 fn cpp_find_added_node<'a>(
3913 staging: &'a StagingGraph,
3914 canonical_name: &str,
3915 ) -> Option<&'a sqry_core::graph::unified::storage::arena::NodeEntry> {
3916 staging.operations().iter().find_map(|op| {
3917 if let StagingOp::AddNode { entry, .. } = op
3918 && staging.resolve_node_canonical_name(entry) == Some(canonical_name)
3919 {
3920 Some(entry)
3921 } else {
3922 None
3923 }
3924 })
3925 }
3926
3927 fn cpp_find_added_node_id(
3929 staging: &StagingGraph,
3930 canonical_name: &str,
3931 kind: NodeKind,
3932 ) -> Option<sqry_core::graph::unified::NodeId> {
3933 staging.operations().iter().find_map(|op| match op {
3934 StagingOp::AddNode {
3935 entry,
3936 expected_id: Some(id),
3937 } if entry.kind == kind
3938 && staging.resolve_node_canonical_name(entry) == Some(canonical_name) =>
3939 {
3940 Some(*id)
3941 }
3942 _ => None,
3943 })
3944 }
3945
3946 fn build_cpp(source: &str) -> StagingGraph {
3948 let tree = parse_cpp(source);
3949 let mut staging = StagingGraph::new();
3950 let builder = CppGraphBuilder::new();
3951 builder
3952 .build_graph(
3953 &tree,
3954 source.as_bytes(),
3955 Path::new("test.cpp"),
3956 &mut staging,
3957 )
3958 .expect("build_graph must succeed for the test fixture");
3959 staging
3960 }
3961
3962 fn staged_node_name_by_id(
3963 staging: &StagingGraph,
3964 id: sqry_core::graph::unified::NodeId,
3965 ) -> Option<&str> {
3966 staging.nodes().find_map(|node| {
3967 if node.expected_id == Some(id) {
3968 staging.resolve_node_canonical_name(node.entry)
3969 } else {
3970 None
3971 }
3972 })
3973 }
3974
3975 fn call_edge_pairs(staging: &StagingGraph) -> Vec<(String, String)> {
3976 staging
3977 .edges()
3978 .filter_map(|edge| {
3979 if matches!(edge.kind, EdgeKind::Calls { .. }) {
3980 let source = staged_node_name_by_id(staging, edge.source)?.to_string();
3981 let target = staged_node_name_by_id(staging, edge.target)?.to_string();
3982 Some((source, target))
3983 } else {
3984 None
3985 }
3986 })
3987 .collect()
3988 }
3989
3990 fn assert_has_call_edge(staging: &StagingGraph, caller: &str, callee: &str) {
3991 let calls = call_edge_pairs(staging);
3992 assert!(
3993 calls
3994 .iter()
3995 .any(|(source, target)| source == caller && target == callee),
3996 "expected Calls edge {caller} -> {callee}; staged Calls edges: {calls:?}"
3997 );
3998 }
3999
4000 fn assert_no_call_target(staging: &StagingGraph, forbidden_target: &str) {
4001 let calls = call_edge_pairs(staging);
4002 assert!(
4003 !calls.iter().any(|(_, target)| target == forbidden_target),
4004 "unexpected Calls edge target {forbidden_target}; staged Calls edges: {calls:?}"
4005 );
4006 }
4007
4008 fn assert_no_call_target_suffix(staging: &StagingGraph, forbidden_suffix: &str) {
4009 let calls = call_edge_pairs(staging);
4010 assert!(
4011 !calls
4012 .iter()
4013 .any(|(_, target)| target.ends_with(forbidden_suffix)),
4014 "unexpected Calls edge target ending with {forbidden_suffix}; staged Calls edges: {calls:?}"
4015 );
4016 }
4017
4018 #[test]
4019 fn test_issue_466_t1_member_call_through_field_resolves_to_method_fqn() {
4020 let source = r"
4021namespace demo {
4022 struct Repository { void save(); };
4023 struct Service { Repository repo; void run() { repo.save(); } };
4024}
4025";
4026 let staging = build_cpp(source);
4027
4028 assert_has_call_edge(&staging, "demo::Service::run", "demo::Repository::save");
4029 assert_no_call_target(&staging, "demo::repo.save");
4030 assert_no_call_target(&staging, "Repository::save");
4031 }
4032
4033 #[test]
4034 fn test_issue_466_t2_same_namespace_static_call_uses_fallback_prefix() {
4035 let source = r"
4036namespace demo {
4037 struct Repository { static void save(); };
4038 void use() { Repository::save(); }
4039}
4040";
4041 let staging = build_cpp(source);
4042
4043 assert_has_call_edge(&staging, "demo::use", "demo::Repository::save");
4044 }
4045
4046 #[test]
4047 fn test_issue_466_t3_using_declaration_alias_resolves_static_call() {
4048 let source = r"
4049namespace lib { struct Widget { static void make(); }; }
4050namespace app { using lib::Widget; void run() { Widget::make(); } }
4051";
4052 let staging = build_cpp(source);
4053
4054 assert_has_call_edge(&staging, "app::run", "lib::Widget::make");
4055 assert_no_call_target(&staging, "app::Widget::make");
4056 }
4057
4058 #[test]
4059 fn test_issue_466_t4_unknown_receiver_does_not_invent_member_target() {
4060 let source = r"
4061namespace demo {
4062 struct Service { void run(int* p) { p->frobnicate(); } };
4063}
4064";
4065 let staging = build_cpp(source);
4066
4067 assert_no_call_target_suffix(&staging, "::frobnicate");
4068 assert_has_call_edge(&staging, "demo::Service::run", "demo::p->frobnicate");
4069 }
4070
4071 #[test]
4072 fn test_issue_466_t5_qualified_and_ffi_fallback_behavior_is_unchanged() {
4073 let source = r#"
4074extern "C" { int printf(const char*); }
4075namespace demo { void helper() {} }
4076void run() {
4077 demo::helper();
4078 printf("x");
4079}
4080"#;
4081 let staging = build_cpp(source);
4082
4083 assert_has_call_edge(&staging, "run", "demo::helper");
4084 assert_has_ffi_call_edge(&staging, "run", "extern::C::printf");
4085 }
4086
4087 #[test]
4088 fn test_issue_466_t6_same_class_name_collision_does_not_cross_namespace() {
4089 let source = r"
4090namespace a { struct Repository { void save(); }; }
4091namespace b {
4092 struct Repository { void wipe(); };
4093 struct Service { Repository repo; void run() { repo.save(); } };
4094}
4095";
4096 let staging = build_cpp(source);
4097
4098 assert_no_call_target(&staging, "a::Repository::save");
4099 assert_has_call_edge(&staging, "b::Service::run", "b::Repository::save");
4100 }
4101
4102 #[test]
4103 fn test_issue_466_t7_nested_class_member_access_resolves() {
4104 let source = r"
4105namespace demo {
4106 struct Inner { void tick(); };
4107 struct Outer { struct Nested { Inner inner; void go() { inner.tick(); } }; };
4108}
4109";
4110 let staging = build_cpp(source);
4111
4112 assert_has_call_edge(&staging, "demo::Outer::Nested::go", "demo::Inner::tick");
4113 }
4114
4115 #[test]
4116 fn test_issue_466_t8_out_of_class_method_definition_resolves_member_field() {
4117 let source = r"
4118namespace demo {
4119 struct Repository { void save(); };
4120 struct Service { Repository repo; void run(); };
4121 void Service::run() { repo.save(); }
4122}
4123";
4124 let staging = build_cpp(source);
4125
4126 assert_has_call_edge(&staging, "demo::Service::run", "demo::Repository::save");
4127 }
4128
4129 #[test]
4130 fn test_issue_466_t9_same_named_fields_bind_to_enclosing_class() {
4131 let source = r"
4132namespace demo {
4133 struct Base { struct Handle { void base_op(); }; Handle h; };
4134 struct Repository { void save(); };
4135 struct Service { Repository h; void run() { h.save(); } };
4136}
4137";
4138 let staging = build_cpp(source);
4139
4140 assert_has_call_edge(&staging, "demo::Service::run", "demo::Repository::save");
4141 assert_no_call_target(&staging, "demo::Base::Handle::base_op");
4142 }
4143
4144 #[test]
4150 fn test_struct_field_emits_property_with_field_context() {
4151 let source = "struct Point { int x; int y; };";
4152 let staging = build_cpp(source);
4153
4154 assert_has_node_with_kind_exact(&staging, "Point.x", NodeKind::Property);
4156 assert_has_node_with_kind_exact(&staging, "Point.y", NodeKind::Property);
4157
4158 let entry =
4159 cpp_find_added_node(&staging, "Point.x").expect("Point.x should be staged as a node");
4160 assert_eq!(entry.kind, NodeKind::Property, "x must be Property");
4161 assert!(!entry.is_static, "instance field is_static must be false");
4162 let vis = staging.resolve_local_string(entry.visibility.expect("visibility id"));
4163 assert_eq!(
4164 vis,
4165 Some("public"),
4166 "struct default visibility must be 'public'"
4167 );
4168 assert!(entry.end_line > 0, "field end_line must be set (got 0)");
4174 assert!(
4175 entry.end_line > entry.start_line
4176 || (entry.end_line == entry.start_line && entry.end_column > entry.start_column),
4177 "field span must be non-empty: [{}:{}..{}:{}]",
4178 entry.start_line,
4179 entry.start_column,
4180 entry.end_line,
4181 entry.end_column,
4182 );
4183
4184 let x_id = cpp_find_added_node_id(&staging, "Point.x", NodeKind::Property)
4186 .expect("Point.x Property NodeId");
4187 let edge = staging.operations().iter().find_map(|op| {
4188 if let StagingOp::AddEdge {
4189 source: src,
4190 kind: EdgeKind::TypeOf { context, name, .. },
4191 ..
4192 } = op
4193 && *src == x_id
4194 {
4195 Some((*context, *name))
4196 } else {
4197 None
4198 }
4199 });
4200 let (ctx, name) = edge.expect("TypeOf edge from Point.x should be staged");
4201 assert_eq!(
4202 ctx,
4203 Some(TypeOfContext::Field),
4204 "TypeOf edge context must be Field"
4205 );
4206 let resolved_name = name.and_then(|sid| staging.resolve_local_string(sid));
4207 assert_eq!(
4208 resolved_name,
4209 Some("x"),
4210 "TypeOf edge name must be the bare field name 'x'"
4211 );
4212
4213 let stale_variable = staging.nodes().any(|n| {
4215 n.entry.kind == NodeKind::Variable
4216 && matches!(
4217 staging.resolve_node_name(n.entry),
4218 Some("Point.x" | "Point.y" | "Point::x" | "Point::y")
4219 )
4220 });
4221 assert!(
4222 !stale_variable,
4223 "Point fields must not be emitted as NodeKind::Variable"
4224 );
4225 }
4226
4227 #[test]
4229 fn test_class_field_default_visibility_is_private() {
4230 let source = "class Foo { int hidden; };";
4231 let staging = build_cpp(source);
4232
4233 let entry = cpp_find_added_node(&staging, "Foo.hidden")
4234 .expect("Foo.hidden should be staged as a node");
4235 assert_eq!(entry.kind, NodeKind::Property);
4236 let vis = staging.resolve_local_string(entry.visibility.expect("visibility id"));
4237 assert_eq!(
4238 vis,
4239 Some("private"),
4240 "class default visibility must be 'private'"
4241 );
4242 }
4243
4244 #[test]
4246 fn test_class_field_respects_explicit_access_specifier() {
4247 let source = "class Foo { public: int public_field; protected: int prot_field; };";
4248 let staging = build_cpp(source);
4249
4250 let pub_entry = cpp_find_added_node(&staging, "Foo.public_field")
4251 .expect("Foo.public_field should be staged");
4252 assert_eq!(
4253 staging.resolve_local_string(pub_entry.visibility.expect("vis")),
4254 Some("public")
4255 );
4256
4257 let prot_entry = cpp_find_added_node(&staging, "Foo.prot_field")
4258 .expect("Foo.prot_field should be staged");
4259 assert_eq!(
4260 staging.resolve_local_string(prot_entry.visibility.expect("vis")),
4261 Some("protected")
4262 );
4263 }
4264
4265 #[test]
4268 fn test_const_field_emits_constant() {
4269 let source = "class Foo { const int kMax = 0; };";
4270 let staging = build_cpp(source);
4271
4272 assert_has_node_with_kind_exact(&staging, "Foo.kMax", NodeKind::Constant);
4273 let entry = cpp_find_added_node(&staging, "Foo.kMax").expect("Foo.kMax");
4274 assert_eq!(entry.kind, NodeKind::Constant);
4275 assert!(
4276 !entry.is_static,
4277 "const (non-static) field is_static must be false; only `static` keyword sets is_static"
4278 );
4279 }
4280
4281 #[test]
4285 fn test_constexpr_field_emits_constant() {
4286 let source = "class Foo { constexpr static int kAnswer = 42; };";
4287 let staging = build_cpp(source);
4288
4289 assert_has_node_with_kind_exact(&staging, "Foo.kAnswer", NodeKind::Constant);
4290 let entry = cpp_find_added_node(&staging, "Foo.kAnswer").expect("Foo.kAnswer");
4291 assert_eq!(entry.kind, NodeKind::Constant);
4292 assert!(
4293 entry.is_static,
4294 "static constexpr member must have is_static = true"
4295 );
4296 }
4297
4298 #[test]
4301 fn test_static_field_sets_is_static_true() {
4302 let source = "class Foo { static int counter; };";
4303 let staging = build_cpp(source);
4304
4305 let entry = cpp_find_added_node(&staging, "Foo.counter").expect("Foo.counter");
4306 assert_eq!(entry.kind, NodeKind::Property);
4307 assert!(entry.is_static, "static keyword must set is_static = true");
4308 }
4309
4310 #[test]
4313 fn test_bitfield_emits_property() {
4314 let source = "struct Flags { unsigned int low : 4; unsigned int high : 4; };";
4315 let staging = build_cpp(source);
4316
4317 assert_has_node_with_kind_exact(&staging, "Flags.low", NodeKind::Property);
4318 assert_has_node_with_kind_exact(&staging, "Flags.high", NodeKind::Property);
4319 }
4320
4321 #[test]
4327 fn test_anonymous_union_member_fields_emit_property() {
4328 let source = r"
4329class Variant {
4330public:
4331 int tag;
4332 union {
4333 int as_int;
4334 float as_float;
4335 };
4336};
4337";
4338 let staging = build_cpp(source);
4339
4340 assert_has_node_with_kind_exact(&staging, "Variant.tag", NodeKind::Property);
4342
4343 assert_has_node_with_kind_exact(&staging, "Variant.as_int", NodeKind::Property);
4346 assert_has_node_with_kind_exact(&staging, "Variant.as_float", NodeKind::Property);
4347
4348 let as_int = cpp_find_added_node(&staging, "Variant.as_int")
4351 .expect("Variant.as_int should be staged");
4352 let vis = staging.resolve_local_string(as_int.visibility.expect("visibility id"));
4353 assert_eq!(
4354 vis,
4355 Some("public"),
4356 "anonymous-union members must inherit OUTER access (`public:` here)"
4357 );
4358
4359 let bogus = staging.nodes().any(|n| {
4362 staging
4363 .resolve_node_name(n.entry)
4364 .is_some_and(|name| name.contains("::.") || name.starts_with("Variant::."))
4365 });
4366 assert!(
4367 !bogus,
4368 "anonymous union must not produce a synthetic qualifier"
4369 );
4370
4371 let stale_variable = staging.nodes().any(|n| {
4373 n.entry.kind == NodeKind::Variable
4374 && matches!(
4375 staging.resolve_node_name(n.entry),
4376 Some("Variant.tag" | "Variant.as_int" | "Variant.as_float")
4377 )
4378 });
4379 assert!(
4380 !stale_variable,
4381 "anonymous-union members + outer fields must not stay as Variable"
4382 );
4383 }
4384
4385 #[test]
4389 fn test_templated_class_field_emits_property() {
4390 let source = r"
4391template<class T>
4392struct Box {
4393 T value;
4394};
4395";
4396 let staging = build_cpp(source);
4397
4398 assert_has_node_with_kind_exact(&staging, "Box.value", NodeKind::Property);
4399 let entry = cpp_find_added_node(&staging, "Box.value").expect("Box.value");
4400 assert_eq!(entry.kind, NodeKind::Property);
4401 assert!(!entry.is_static);
4402 }
4403
4404 #[test]
4410 fn test_outer_class_field_with_nested_class_present() {
4411 let source = r"
4412class Outer {
4413public:
4414 int outer_value;
4415 class Inner {
4416 public:
4417 int x;
4418 };
4419};
4420";
4421 let staging = build_cpp(source);
4422
4423 assert_has_node_with_kind_exact(&staging, "Outer.outer_value", NodeKind::Property);
4425
4426 assert_has_node_with_kind_exact(&staging, "Outer::Inner.x", NodeKind::Property);
4430
4431 let legacy_hits: Vec<_> = staging
4434 .nodes()
4435 .filter(|n| staging.resolve_node_name(n.entry) == Some("Outer::outer_value"))
4436 .collect();
4437 assert!(
4438 legacy_hits.is_empty(),
4439 "legacy `Outer::outer_value` lookup must return 0 hits"
4440 );
4441
4442 for legacy in ["Inner.x", "Outer::Inner::x", "Outer.Inner.x"] {
4446 let hits: Vec<_> = staging
4447 .nodes()
4448 .filter(|n| staging.resolve_node_name(n.entry) == Some(legacy))
4449 .collect();
4450 assert!(
4451 hits.is_empty(),
4452 "nested-class field `{legacy}` must not appear; expected only `Outer::Inner.x`"
4453 );
4454 }
4455 }
4456
4457 #[test]
4461 fn test_outer_class_with_nested_struct_emits_inner_field() {
4462 let source = r"
4463class Outer {
4464private:
4465 struct Inner {
4466 int y;
4467 };
4468};
4469";
4470 let staging = build_cpp(source);
4471
4472 assert_has_node_with_kind_exact(&staging, "Outer::Inner.y", NodeKind::Property);
4473
4474 let entry = cpp_find_added_node(&staging, "Outer::Inner.y")
4475 .expect("Outer::Inner.y should be staged");
4476 let vis = staging.resolve_local_string(entry.visibility.expect("visibility id"));
4477 assert_eq!(
4478 vis,
4479 Some("public"),
4480 "nested struct field default visibility must be 'public' \
4481 regardless of OUTER access state"
4482 );
4483 }
4484
4485 #[test]
4493 fn test_legacy_double_colon_field_lookup_returns_zero() {
4494 let source = r"
4495class Foo {
4496public:
4497 int bar;
4498 static int baz;
4499 const int qux = 0;
4500};
4501struct Quux {
4502 int corge;
4503};
4504";
4505 let staging = build_cpp(source);
4506
4507 assert_has_node_with_kind_exact(&staging, "Foo.bar", NodeKind::Property);
4509 assert_has_node_with_kind_exact(&staging, "Foo.baz", NodeKind::Property);
4510 assert_has_node_with_kind_exact(&staging, "Foo.qux", NodeKind::Constant);
4511 assert_has_node_with_kind_exact(&staging, "Quux.corge", NodeKind::Property);
4512
4513 for legacy in ["Foo::bar", "Foo::baz", "Foo::qux", "Quux::corge"] {
4516 let hits: Vec<_> = staging
4517 .nodes()
4518 .filter(|n| staging.resolve_node_name(n.entry) == Some(legacy))
4519 .collect();
4520 assert!(
4521 hits.is_empty(),
4522 "legacy lookup for {legacy:?} must return 0 hits, got {} node(s) ({:?})",
4523 hits.len(),
4524 hits.iter()
4525 .map(|n| (n.entry.kind, staging.resolve_node_name(n.entry)))
4526 .collect::<Vec<_>>()
4527 );
4528 }
4529 }
4530
4531 #[test]
4534 fn test_namespaced_class_field_qualified_name() {
4535 let source = r"
4536namespace demo {
4537 class Service {
4538 public:
4539 int counter;
4540 };
4541}
4542";
4543 let staging = build_cpp(source);
4544
4545 assert_has_node_with_kind_exact(&staging, "demo::Service.counter", NodeKind::Property);
4546 }
4547}
4548
4549#[cfg(test)]
4550mod shape_tests {
4551 use super::{cf_bucket_for_cpp_kind, cpp_shape_mapping};
4552 use sqry_core::graph::unified::build::shape::{
4553 CfBucket, ShapeBudget, ShapeMapping, compute_shape_descriptor,
4554 };
4555
4556 const SAMPLE: &str = include_str!(concat!(
4557 env!("CARGO_MANIFEST_DIR"),
4558 "/../test-fixtures/shape/reference/sample.cpp"
4559 ));
4560
4561 fn parse(src: &str) -> tree_sitter::Tree {
4562 let lang: tree_sitter::Language = tree_sitter_cpp::LANGUAGE.into();
4563 let mut p = tree_sitter::Parser::new();
4564 p.set_language(&lang).expect("load cpp grammar");
4565 p.parse(src, None).expect("parse")
4566 }
4567
4568 fn function_named<'t>(tree: &'t tree_sitter::Tree, name: &str) -> tree_sitter::Node<'t> {
4570 let root = tree.root_node();
4571 let mut stack = vec![root];
4572 while let Some(node) = stack.pop() {
4573 if node.kind() == "function_definition"
4574 && function_def_name(node).as_deref() == Some(name)
4575 {
4576 return node;
4577 }
4578 let mut c = node.walk();
4579 for ch in node.children(&mut c) {
4580 stack.push(ch);
4581 }
4582 }
4583 panic!("no function_definition named {name}");
4584 }
4585
4586 fn function_def_name(node: tree_sitter::Node) -> Option<String> {
4588 let mut decl = node.child_by_field_name("declarator")?;
4589 for _ in 0..8 {
4590 if decl.kind() == "function_declarator" {
4591 let inner = decl.child_by_field_name("declarator")?;
4592 return inner.utf8_text(SAMPLE.as_bytes()).ok().map(str::to_owned);
4593 }
4594 decl = decl.child_by_field_name("declarator")?;
4595 }
4596 None
4597 }
4598
4599 #[test]
4600 fn cf_table_is_non_empty() {
4601 let mapping = cpp_shape_mapping();
4602 let lang: tree_sitter::Language = tree_sitter_cpp::LANGUAGE.into();
4603 let mut covered = 0;
4604 for id in 0..lang.node_kind_count() {
4605 if mapping.cf_bucket(id as u16).is_some() {
4606 covered += 1;
4607 }
4608 }
4609 assert!(
4610 covered >= 10,
4611 "expected many C++ CF kinds mapped, got {covered}"
4612 );
4613 }
4614
4615 #[test]
4616 fn histogram_covers_real_control_flow() {
4617 let tree = parse(SAMPLE);
4618 let func = function_named(&tree, "classify");
4619 let d = compute_shape_descriptor(
4620 func,
4621 SAMPLE.as_bytes(),
4622 cpp_shape_mapping(),
4623 &ShapeBudget::default(),
4624 );
4625 assert!(!d.is_unhashable());
4626 for bucket in [
4627 CfBucket::Branch,
4628 CfBucket::Loop,
4629 CfBucket::Match,
4630 CfBucket::Try,
4631 CfBucket::Catch,
4632 CfBucket::Throw,
4633 CfBucket::Return,
4634 CfBucket::BreakContinue,
4635 CfBucket::Call,
4636 CfBucket::Assign,
4637 ] {
4638 assert!(
4639 d.cf_histogram[bucket.index()] >= 1,
4640 "classify must exercise {bucket:?}"
4641 );
4642 }
4643 }
4644
4645 #[test]
4646 fn lambda_body_covers_closure() {
4647 let tree = parse(SAMPLE);
4648 let func = function_named(&tree, "adder");
4649 let d = compute_shape_descriptor(
4650 func,
4651 SAMPLE.as_bytes(),
4652 cpp_shape_mapping(),
4653 &ShapeBudget::default(),
4654 );
4655 assert!(
4656 d.cf_histogram[CfBucket::Closure.index()] >= 1,
4657 "lambda closure"
4658 );
4659 }
4660
4661 #[test]
4662 fn signature_shape_reads_arity_defaults_return() {
4663 let tree = parse(SAMPLE);
4664 let func = function_named(&tree, "classify");
4665 let mapping = cpp_shape_mapping();
4666 let shape = mapping.signature_shape(func, SAMPLE.as_bytes());
4667 assert_eq!(shape.arity_positional, 2);
4669 assert!(shape.has_defaults, "threshold = 0");
4670 assert!(shape.has_return_annotation, "int return type");
4671 }
4672
4673 #[test]
4676 fn ac6_cpp_classify_histogram_well_formed() {
4677 let tree = parse(SAMPLE);
4678 let func = function_named(&tree, "classify");
4679 let d = compute_shape_descriptor(
4680 func,
4681 SAMPLE.as_bytes(),
4682 cpp_shape_mapping(),
4683 &ShapeBudget::default(),
4684 );
4685 assert_eq!(d.cf_histogram[CfBucket::Branch.index()], 2, "two if levels");
4687 assert!(d.cf_histogram[CfBucket::Loop.index()] >= 2, "for + while");
4688 assert!(d.cf_histogram[CfBucket::Return.index()] >= 2, "two returns");
4689 }
4690
4691 #[test]
4692 fn unknown_kind_maps_to_none() {
4693 assert!(cf_bucket_for_cpp_kind("translation_unit").is_none());
4694 assert!(cf_bucket_for_cpp_kind("identifier").is_none());
4695 }
4696}