1use sqry_core::graph::unified::build::helper::CalleeKindHint;
19use sqry_core::graph::unified::build::shape::{CfBucket, ShapeMapping};
20use sqry_core::graph::unified::storage::shape::SignatureShape;
21use sqry_core::graph::unified::{FfiConvention, GraphBuildHelper, StagingGraph};
22use sqry_core::graph::{GraphBuilder, GraphBuilderError, GraphResult, Language, Position, Span};
23use std::{
24 collections::{HashMap, HashSet},
25 path::{Path, PathBuf},
26 sync::OnceLock,
27 time::{Duration, Instant},
28};
29use tree_sitter::{Node, Tree};
30
31const FILE_MODULE_NAME: &str = "<file_module>";
34
35type QualifiedNameMap = HashMap<(String, String), String>;
38
39type FfiRegistry = HashMap<String, (String, FfiConvention)>;
46
47type PureVirtualRegistry = HashSet<String>;
51
52const DEFAULT_GRAPH_BUILD_TIMEOUT_MS: u64 = 10_000;
53const MIN_GRAPH_BUILD_TIMEOUT_MS: u64 = 1_000;
54const MAX_GRAPH_BUILD_TIMEOUT_MS: u64 = 60_000;
55const BUDGET_CHECK_INTERVAL: u32 = 1024;
56
57fn cpp_graph_build_timeout() -> Duration {
58 let timeout_ms = std::env::var("SQRY_CPP_GRAPH_BUILD_TIMEOUT_MS")
59 .ok()
60 .and_then(|value| value.parse::<u64>().ok())
61 .unwrap_or(DEFAULT_GRAPH_BUILD_TIMEOUT_MS)
62 .clamp(MIN_GRAPH_BUILD_TIMEOUT_MS, MAX_GRAPH_BUILD_TIMEOUT_MS);
63 Duration::from_millis(timeout_ms)
64}
65
66struct BuildBudget {
67 file: PathBuf,
68 phase_timeout: Duration,
69 started_at: Instant,
70 checkpoints: u32,
71}
72
73impl BuildBudget {
74 fn new(file: &Path) -> Self {
75 Self {
76 file: file.to_path_buf(),
77 phase_timeout: cpp_graph_build_timeout(),
78 started_at: Instant::now(),
79 checkpoints: 0,
80 }
81 }
82
83 #[cfg(test)]
84 fn already_expired(file: &Path) -> Self {
85 Self {
86 file: file.to_path_buf(),
87 phase_timeout: Duration::from_secs(1),
88 started_at: Instant::now().checked_sub(Duration::from_secs(60)).unwrap(),
89 checkpoints: BUDGET_CHECK_INTERVAL - 1,
90 }
91 }
92
93 fn checkpoint(&mut self, phase: &'static str) -> GraphResult<()> {
94 self.checkpoints = self.checkpoints.wrapping_add(1);
95 if self.checkpoints.is_multiple_of(BUDGET_CHECK_INTERVAL)
96 && self.started_at.elapsed() > self.phase_timeout
97 {
98 return Err(GraphBuilderError::BuildTimedOut {
99 file: self.file.clone(),
100 phase,
101 #[allow(clippy::cast_possible_truncation)] timeout_ms: self.phase_timeout.as_millis() as u64,
103 });
104 }
105 Ok(())
106 }
107}
108
109#[allow(dead_code)] trait SpanExt {
112 fn from_node(node: &tree_sitter::Node) -> Self;
113}
114
115impl SpanExt for Span {
116 fn from_node(node: &tree_sitter::Node) -> Self {
117 Span::new(
118 Position::new(node.start_position().row, node.start_position().column),
119 Position::new(node.end_position().row, node.end_position().column),
120 )
121 }
122}
123
124#[derive(Debug)]
136struct ASTGraph {
137 contexts: Vec<FunctionContext>,
139 context_start_index: HashMap<usize, usize>,
141
142 #[allow(dead_code)]
148 field_types: QualifiedNameMap,
149
150 #[allow(dead_code)]
157 type_map: QualifiedNameMap,
158
159 #[allow(dead_code)]
163 namespace_map: HashMap<std::ops::Range<usize>, String>,
164}
165
166impl ASTGraph {
167 fn from_tree(root: Node, content: &[u8], budget: &mut BuildBudget) -> GraphResult<Self> {
169 let namespace_map = extract_namespace_map(root, content, budget)?;
171
172 let mut contexts = extract_cpp_contexts(root, content, &namespace_map, budget)?;
174 contexts.sort_by_key(|ctx| ctx.span.0);
175 let context_start_index = contexts
176 .iter()
177 .enumerate()
178 .map(|(idx, ctx)| (ctx.span.0, idx))
179 .collect();
180
181 let (field_types, type_map) =
183 extract_field_and_type_info(root, content, &namespace_map, budget)?;
184
185 Ok(Self {
186 contexts,
187 context_start_index,
188 field_types,
189 type_map,
190 namespace_map,
191 })
192 }
193
194 fn find_enclosing(&self, byte_pos: usize) -> Option<&FunctionContext> {
200 let insertion_point = self.contexts.partition_point(|ctx| ctx.span.0 <= byte_pos);
201 if insertion_point == 0 {
202 return None;
203 }
204
205 let candidate = &self.contexts[insertion_point - 1];
206 (byte_pos < candidate.span.1).then_some(candidate)
207 }
208
209 fn context_for_start(&self, start_byte: usize) -> Option<&FunctionContext> {
210 self.context_start_index
211 .get(&start_byte)
212 .and_then(|idx| self.contexts.get(*idx))
213 }
214}
215
216#[derive(Debug, Clone)]
218struct FunctionContext {
219 qualified_name: String,
221 span: (usize, usize),
223 is_static: bool,
226 #[allow(dead_code)]
229 is_virtual: bool,
230 #[allow(dead_code)]
233 is_inline: bool,
234 namespace_stack: Vec<String>,
236 #[allow(dead_code)] class_stack: Vec<String>,
240 return_type: Option<String>,
242}
243
244impl FunctionContext {
245 #[allow(dead_code)] fn qualified_name(&self) -> &str {
247 &self.qualified_name
248 }
249}
250
251#[derive(Debug, Default, Clone, Copy)]
275pub struct CppGraphBuilder;
276
277impl CppGraphBuilder {
278 #[must_use]
280 pub fn new() -> Self {
281 Self
282 }
283
284 #[allow(clippy::unused_self)] #[allow(clippy::trivially_copy_pass_by_ref)] fn build_graph_with_budget(
287 #[allow(clippy::trivially_copy_pass_by_ref)] &self,
289 tree: &Tree,
290 content: &[u8],
291 file: &Path,
292 staging: &mut StagingGraph,
293 budget: &mut BuildBudget,
294 ) -> GraphResult<()> {
295 let mut helper = GraphBuildHelper::new(staging, file, Language::Cpp);
297
298 let ast_graph = ASTGraph::from_tree(tree.root_node(), content, budget)?;
300
301 let mut seen_includes: HashSet<String> = HashSet::new();
303
304 let mut namespace_stack: Vec<String> = Vec::new();
306 let mut class_stack: Vec<String> = Vec::new();
307
308 let mut ffi_registry = FfiRegistry::new();
311 collect_ffi_declarations(tree.root_node(), content, &mut ffi_registry, budget)?;
312
313 let mut pure_virtual_registry = PureVirtualRegistry::new();
315 collect_pure_virtual_interfaces(
316 tree.root_node(),
317 content,
318 &mut pure_virtual_registry,
319 budget,
320 )?;
321
322 walk_tree_for_graph(
324 tree.root_node(),
325 content,
326 &ast_graph,
327 &mut helper,
328 &mut seen_includes,
329 &mut namespace_stack,
330 &mut class_stack,
331 &ffi_registry,
332 &pure_virtual_registry,
333 budget,
334 )?;
335
336 Ok(())
337 }
338
339 #[allow(dead_code)] fn extract_class_attributes(node: &tree_sitter::Node, content: &[u8]) -> Vec<String> {
342 let mut attributes = Vec::new();
343 let mut cursor = node.walk();
344 for child in node.children(&mut cursor) {
345 if child.kind() == "modifiers" {
346 let mut mod_cursor = child.walk();
347 for modifier in child.children(&mut mod_cursor) {
348 if let Ok(mod_text) = modifier.utf8_text(content) {
349 match mod_text {
350 "template" => attributes.push("template".to_string()),
351 "sealed" => attributes.push("sealed".to_string()),
352 "abstract" => attributes.push("abstract".to_string()),
353 "open" => attributes.push("open".to_string()),
354 "final" => attributes.push("final".to_string()),
355 "inner" => attributes.push("inner".to_string()),
356 "value" => attributes.push("value".to_string()),
357 _ => {}
358 }
359 }
360 }
361 }
362 }
363 attributes
364 }
365
366 #[allow(dead_code)] fn extract_is_virtual(node: &tree_sitter::Node, content: &[u8]) -> bool {
369 if let Some(spec) = node.child_by_field_name("declaration_specifiers")
370 && let Ok(text) = spec.utf8_text(content)
371 && text.contains("virtual")
372 {
373 return true;
374 }
375
376 if let Ok(text) = node.utf8_text(content)
377 && text.contains("virtual")
378 {
379 return true;
380 }
381
382 if let Some(parent) = node.parent()
383 && (parent.kind() == "field_declaration" || parent.kind() == "declaration")
384 && let Ok(text) = parent.utf8_text(content)
385 && text.contains("virtual")
386 {
387 return true;
388 }
389
390 false
391 }
392
393 #[allow(dead_code)] fn extract_function_attributes(node: &tree_sitter::Node, content: &[u8]) -> Vec<String> {
396 let mut attributes = Vec::new();
397 for node_ref in [
398 node.child_by_field_name("declaration_specifiers"),
399 node.parent(),
400 ]
401 .into_iter()
402 .flatten()
403 {
404 if let Ok(text) = node_ref.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
420 if let Ok(text) = node.utf8_text(content) {
421 for keyword in [
422 "virtual",
423 "inline",
424 "constexpr",
425 "operator",
426 "override",
427 "static",
428 ] {
429 if text.contains(keyword) && !attributes.contains(&keyword.to_string()) {
430 attributes.push(keyword.to_string());
431 }
432 }
433 }
434
435 attributes
436 }
437}
438
439impl GraphBuilder for CppGraphBuilder {
440 fn language(&self) -> Language {
441 Language::Cpp
442 }
443
444 fn shape_mapping(&self) -> Option<&dyn ShapeMapping> {
445 Some(cpp_shape_mapping())
446 }
447
448 fn build_graph(
449 &self,
450 tree: &Tree,
451 content: &[u8],
452 file: &Path,
453 staging: &mut StagingGraph,
454 ) -> GraphResult<()> {
455 let mut budget = BuildBudget::new(file);
456 self.build_graph_with_budget(tree, content, file, staging, &mut budget)
457 }
458}
459
460pub struct CppShapeMapping {
469 cf_by_kind_id: Vec<Option<CfBucket>>,
470}
471
472impl CppShapeMapping {
473 fn build() -> Self {
475 let lang: tree_sitter::Language = tree_sitter_cpp::LANGUAGE.into();
476 let count = lang.node_kind_count();
477 let mut cf_by_kind_id = vec![None; count];
478 for (id, slot) in cf_by_kind_id.iter_mut().enumerate() {
479 let Ok(kind_id) = u16::try_from(id) else {
480 break;
481 };
482 if !lang.node_kind_is_named(kind_id) {
483 continue;
484 }
485 if let Some(name) = lang.node_kind_for_id(kind_id) {
486 *slot = cf_bucket_for_cpp_kind(name);
487 }
488 }
489 Self { cf_by_kind_id }
490 }
491}
492
493impl ShapeMapping for CppShapeMapping {
494 fn cf_bucket(&self, ts_node_kind_id: u16) -> Option<CfBucket> {
495 self.cf_by_kind_id
496 .get(ts_node_kind_id as usize)
497 .copied()
498 .flatten()
499 }
500
501 fn signature_shape(&self, fn_node: Node, _src: &[u8]) -> SignatureShape {
502 let mut shape = SignatureShape::default();
503 if let Some(params) = cpp_parameter_list(fn_node) {
507 let mut cursor = params.walk();
508 for child in params.named_children(&mut cursor) {
509 match child.kind() {
510 "parameter_declaration" => {
511 shape.arity_positional = shape.arity_positional.saturating_add(1);
512 }
513 "optional_parameter_declaration" => {
515 shape.arity_positional = shape.arity_positional.saturating_add(1);
516 shape.has_defaults = true;
517 }
518 "variadic_parameter_declaration" | "variadic_declarator" => {
520 shape.has_varargs = true;
521 }
522 _ => {}
523 }
524 }
525 }
526 shape.has_return_annotation = fn_node.child_by_field_name("type").is_some();
529 shape
530 }
531}
532
533fn cpp_parameter_list(fn_node: Node) -> Option<Node> {
537 let mut declarator = fn_node.child_by_field_name("declarator")?;
538 for _ in 0..8 {
541 if declarator.kind() == "function_declarator" {
542 return declarator.child_by_field_name("parameters");
543 }
544 match declarator.child_by_field_name("declarator") {
545 Some(inner) => declarator = inner,
546 None => break,
547 }
548 }
549 None
550}
551
552fn cf_bucket_for_cpp_kind(name: &str) -> Option<CfBucket> {
555 let bucket = match name {
556 "if_statement" | "conditional_expression" => CfBucket::Branch,
557 "for_statement" | "for_range_loop" | "while_statement" | "do_statement" => CfBucket::Loop,
558 "switch_statement" | "case_statement" => CfBucket::Match,
559 "try_statement" => CfBucket::Try,
560 "catch_clause" => CfBucket::Catch,
561 "throw_statement" | "throw_expression" => CfBucket::Throw,
562 "return_statement" | "co_return_statement" => CfBucket::Return,
563 "co_yield_expression" => CfBucket::Yield,
564 "co_await_expression" => CfBucket::Await,
565 "break_statement" | "continue_statement" | "goto_statement" => CfBucket::BreakContinue,
566 "call_expression" => CfBucket::Call,
567 "assignment_expression" | "init_declarator" | "declaration" => CfBucket::Assign,
568 "lambda_expression" => CfBucket::Closure,
569 _ => return None,
570 };
571 Some(bucket)
572}
573
574#[must_use]
576pub fn cpp_shape_mapping() -> &'static CppShapeMapping {
577 static MAPPING: OnceLock<CppShapeMapping> = OnceLock::new();
578 MAPPING.get_or_init(CppShapeMapping::build)
579}
580
581fn extract_namespace_map(
593 node: Node,
594 content: &[u8],
595 budget: &mut BuildBudget,
596) -> GraphResult<HashMap<std::ops::Range<usize>, String>> {
597 let mut map = HashMap::new();
598
599 let recursion_limits = sqry_core::config::RecursionLimits::load_or_default()
601 .expect("Failed to load recursion limits");
602 let file_ops_depth = recursion_limits
603 .effective_file_ops_depth()
604 .expect("Invalid file_ops_depth configuration");
605 let mut guard = sqry_core::query::security::RecursionGuard::new(file_ops_depth)
606 .expect("Failed to create recursion guard");
607
608 extract_namespaces_recursive(node, content, "", &mut map, &mut guard, budget).map_err(|e| {
609 match e {
610 timeout @ GraphBuilderError::BuildTimedOut { .. } => timeout,
611 other => GraphBuilderError::ParseError {
612 span: span_from_node(node),
613 reason: format!("C++ namespace extraction failed: {other}"),
614 },
615 }
616 })?;
617
618 Ok(map)
619}
620
621fn extract_namespaces_recursive(
627 node: Node,
628 content: &[u8],
629 current_ns: &str,
630 map: &mut HashMap<std::ops::Range<usize>, String>,
631 guard: &mut sqry_core::query::security::RecursionGuard,
632 budget: &mut BuildBudget,
633) -> GraphResult<()> {
634 budget.checkpoint("cpp:extract_namespace_map")?;
635 guard.enter().map_err(|e| GraphBuilderError::ParseError {
636 span: span_from_node(node),
637 reason: format!("C++ namespace extraction hit recursion limit: {e}"),
638 })?;
639
640 if node.kind() == "namespace_definition" {
641 let ns_name = if let Some(name_node) = node.child_by_field_name("name") {
643 extract_identifier(name_node, content)
644 } else {
645 String::from("anonymous")
647 };
648
649 let new_ns = if current_ns.is_empty() {
651 format!("{ns_name}::")
652 } else {
653 format!("{current_ns}{ns_name}::")
654 };
655
656 if let Some(body) = node.child_by_field_name("body") {
658 let range = body.start_byte()..body.end_byte();
659 map.insert(range, new_ns.clone());
660
661 let mut cursor = body.walk();
663 for child in body.children(&mut cursor) {
664 extract_namespaces_recursive(child, content, &new_ns, map, guard, budget)?;
665 }
666 }
667 } else {
668 let mut cursor = node.walk();
670 for child in node.children(&mut cursor) {
671 extract_namespaces_recursive(child, content, current_ns, map, guard, budget)?;
672 }
673 }
674
675 guard.exit();
676 Ok(())
677}
678
679fn extract_identifier(node: Node, content: &[u8]) -> String {
681 node.utf8_text(content).unwrap_or("").to_string()
682}
683
684fn find_namespace_for_offset(
686 byte_offset: usize,
687 namespace_map: &HashMap<std::ops::Range<usize>, String>,
688) -> String {
689 let mut matching_ranges: Vec<_> = namespace_map
691 .iter()
692 .filter(|(range, _)| range.contains(&byte_offset))
693 .collect();
694
695 matching_ranges.sort_by_key(|(range, _)| range.end - range.start);
697
698 matching_ranges
700 .first()
701 .map_or("", |(_, ns)| ns.as_str())
702 .to_string()
703}
704
705fn extract_cpp_contexts(
712 node: Node,
713 content: &[u8],
714 namespace_map: &HashMap<std::ops::Range<usize>, String>,
715 budget: &mut BuildBudget,
716) -> GraphResult<Vec<FunctionContext>> {
717 let mut contexts = Vec::new();
718 let mut class_stack = Vec::new();
719
720 let recursion_limits = sqry_core::config::RecursionLimits::load_or_default()
722 .expect("Failed to load recursion limits");
723 let file_ops_depth = recursion_limits
724 .effective_file_ops_depth()
725 .expect("Invalid file_ops_depth configuration");
726 let mut guard = sqry_core::query::security::RecursionGuard::new(file_ops_depth)
727 .expect("Failed to create recursion guard");
728
729 extract_contexts_recursive(
730 node,
731 content,
732 namespace_map,
733 &mut contexts,
734 &mut class_stack,
735 &mut guard,
736 budget,
737 )
738 .map_err(|e| match e {
739 timeout @ GraphBuilderError::BuildTimedOut { .. } => timeout,
740 other => GraphBuilderError::ParseError {
741 span: span_from_node(node),
742 reason: format!("C++ context extraction failed: {other}"),
743 },
744 })?;
745
746 Ok(contexts)
747}
748
749fn extract_contexts_recursive(
754 node: Node,
755 content: &[u8],
756 namespace_map: &HashMap<std::ops::Range<usize>, String>,
757 contexts: &mut Vec<FunctionContext>,
758 class_stack: &mut Vec<String>,
759 guard: &mut sqry_core::query::security::RecursionGuard,
760 budget: &mut BuildBudget,
761) -> GraphResult<()> {
762 budget.checkpoint("cpp:extract_contexts")?;
763 guard.enter().map_err(|e| GraphBuilderError::ParseError {
764 span: span_from_node(node),
765 reason: format!("C++ context extraction hit recursion limit: {e}"),
766 })?;
767
768 match node.kind() {
769 "class_specifier" | "struct_specifier" => {
770 if let Some(name_node) = node.child_by_field_name("name") {
772 let class_name = extract_identifier(name_node, content);
773 class_stack.push(class_name);
774
775 if let Some(body) = node.child_by_field_name("body") {
777 let mut cursor = body.walk();
778 for child in body.children(&mut cursor) {
779 extract_contexts_recursive(
780 child,
781 content,
782 namespace_map,
783 contexts,
784 class_stack,
785 guard,
786 budget,
787 )?;
788 }
789 }
790
791 class_stack.pop();
792 }
793 }
794
795 "function_definition" => {
796 if let Some(declarator) = node.child_by_field_name("declarator") {
798 let (func_name, class_prefix) =
799 extract_function_name_with_class(declarator, content);
800
801 let namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
803 let namespace_stack: Vec<String> = if namespace.is_empty() {
804 Vec::new()
805 } else {
806 namespace
807 .trim_end_matches("::")
808 .split("::")
809 .map(String::from)
810 .collect()
811 };
812
813 let effective_class_stack: Vec<String> = if !class_stack.is_empty() {
817 class_stack.clone()
818 } else if let Some(ref prefix) = class_prefix {
819 vec![prefix.clone()]
820 } else {
821 Vec::new()
822 };
823
824 let qualified_name =
826 build_qualified_name(&namespace_stack, &effective_class_stack, &func_name);
827
828 let is_static = is_static_function(node, content);
830 let is_virtual = is_virtual_function(node, content);
831 let is_inline = is_inline_function(node, content);
832
833 let return_type = node
835 .child_by_field_name("type")
836 .and_then(|type_node| type_node.utf8_text(content).ok())
837 .map(std::string::ToString::to_string);
838
839 let span = (node.start_byte(), node.end_byte());
841
842 contexts.push(FunctionContext {
843 qualified_name,
844 span,
845 is_static,
846 is_virtual,
847 is_inline,
848 namespace_stack,
849 class_stack: effective_class_stack,
850 return_type,
851 });
852 }
853
854 }
856
857 _ => {
858 let mut cursor = node.walk();
860 for child in node.children(&mut cursor) {
861 extract_contexts_recursive(
862 child,
863 content,
864 namespace_map,
865 contexts,
866 class_stack,
867 guard,
868 budget,
869 )?;
870 }
871 }
872 }
873
874 guard.exit();
875 Ok(())
876}
877
878fn build_qualified_name(namespace_stack: &[String], class_stack: &[String], name: &str) -> String {
883 let mut parts = Vec::new();
884
885 parts.extend(namespace_stack.iter().cloned());
887
888 for class_name in class_stack {
890 parts.push(class_name.clone());
891 }
892
893 parts.push(name.to_string());
895
896 parts.join("::")
897}
898
899fn extract_function_name_with_class(declarator: Node, content: &[u8]) -> (String, Option<String>) {
904 match declarator.kind() {
912 "function_declarator" => {
913 if let Some(declarator_inner) = declarator.child_by_field_name("declarator") {
915 extract_function_name_with_class(declarator_inner, content)
916 } else {
917 (extract_identifier(declarator, content), None)
918 }
919 }
920 "qualified_identifier" => {
921 let name = if let Some(name_node) = declarator.child_by_field_name("name") {
923 extract_identifier(name_node, content)
924 } else {
925 extract_identifier(declarator, content)
926 };
927
928 let class_prefix = declarator
930 .child_by_field_name("scope")
931 .map(|scope_node| extract_identifier(scope_node, content));
932
933 (name, class_prefix)
934 }
935 "field_identifier" | "identifier" | "destructor_name" | "operator_name" => {
936 (extract_identifier(declarator, content), None)
937 }
938 _ => {
939 (extract_identifier(declarator, content), None)
941 }
942 }
943}
944
945#[allow(dead_code)]
947fn extract_function_name(declarator: Node, content: &[u8]) -> String {
948 extract_function_name_with_class(declarator, content).0
949}
950
951fn is_static_function(node: Node, content: &[u8]) -> bool {
953 has_specifier(node, "static", content)
954}
955
956fn is_virtual_function(node: Node, content: &[u8]) -> bool {
958 has_specifier(node, "virtual", content)
959}
960
961fn is_inline_function(node: Node, content: &[u8]) -> bool {
963 has_specifier(node, "inline", content)
964}
965
966fn has_specifier(node: Node, specifier: &str, content: &[u8]) -> bool {
968 let mut cursor = node.walk();
970 for child in node.children(&mut cursor) {
971 if (child.kind() == "storage_class_specifier"
972 || child.kind() == "type_qualifier"
973 || child.kind() == "virtual"
974 || child.kind() == "inline")
975 && let Ok(text) = child.utf8_text(content)
976 && text == specifier
977 {
978 return true;
979 }
980 }
981 false
982}
983
984fn extract_field_and_type_info(
994 node: Node,
995 content: &[u8],
996 namespace_map: &HashMap<std::ops::Range<usize>, String>,
997 budget: &mut BuildBudget,
998) -> GraphResult<(QualifiedNameMap, QualifiedNameMap)> {
999 let mut field_types = HashMap::new();
1000 let mut type_map = HashMap::new();
1001 let mut class_stack = Vec::new();
1002
1003 extract_fields_recursive(
1004 node,
1005 content,
1006 namespace_map,
1007 &mut field_types,
1008 &mut type_map,
1009 &mut class_stack,
1010 budget,
1011 )?;
1012
1013 Ok((field_types, type_map))
1014}
1015
1016fn extract_fields_recursive(
1018 node: Node,
1019 content: &[u8],
1020 namespace_map: &HashMap<std::ops::Range<usize>, String>,
1021 field_types: &mut HashMap<(String, String), String>,
1022 type_map: &mut HashMap<(String, String), String>,
1023 class_stack: &mut Vec<String>,
1024 budget: &mut BuildBudget,
1025) -> GraphResult<()> {
1026 budget.checkpoint("cpp:extract_fields")?;
1027 match node.kind() {
1028 "class_specifier" | "struct_specifier" => {
1029 if let Some(name_node) = node.child_by_field_name("name") {
1031 let class_name = extract_identifier(name_node, content);
1032 let namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
1033
1034 let class_fqn = if class_stack.is_empty() {
1036 if namespace.is_empty() {
1038 class_name.clone()
1039 } else {
1040 format!("{}::{}", namespace.trim_end_matches("::"), class_name)
1041 }
1042 } else {
1043 format!("{}::{}", class_stack.last().unwrap(), class_name)
1045 };
1046
1047 class_stack.push(class_fqn.clone());
1048
1049 let mut cursor = node.walk();
1051 for child in node.children(&mut cursor) {
1052 extract_fields_recursive(
1053 child,
1054 content,
1055 namespace_map,
1056 field_types,
1057 type_map,
1058 class_stack,
1059 budget,
1060 )?;
1061 }
1062
1063 class_stack.pop();
1064 }
1065 }
1066
1067 "field_declaration" => {
1068 if let Some(class_fqn) = class_stack.last() {
1070 extract_field_declaration(
1071 node,
1072 content,
1073 class_fqn,
1074 namespace_map,
1075 field_types,
1076 type_map,
1077 );
1078 }
1079 }
1080
1081 "using_directive" => {
1082 extract_using_directive(node, content, namespace_map, type_map);
1084 }
1085
1086 "using_declaration" => {
1087 extract_using_declaration(node, content, namespace_map, type_map);
1089 }
1090
1091 _ => {
1092 let mut cursor = node.walk();
1094 for child in node.children(&mut cursor) {
1095 extract_fields_recursive(
1096 child,
1097 content,
1098 namespace_map,
1099 field_types,
1100 type_map,
1101 class_stack,
1102 budget,
1103 )?;
1104 }
1105 }
1106 }
1107
1108 Ok(())
1109}
1110
1111fn extract_field_declaration(
1113 node: Node,
1114 content: &[u8],
1115 class_fqn: &str,
1116 namespace_map: &HashMap<std::ops::Range<usize>, String>,
1117 field_types: &mut HashMap<(String, String), String>,
1118 type_map: &HashMap<(String, String), String>,
1119) {
1120 let mut field_type = None;
1125 let mut field_names = Vec::new();
1126
1127 let mut cursor = node.walk();
1128 for child in node.children(&mut cursor) {
1129 match child.kind() {
1130 "type_identifier" | "primitive_type" | "qualified_identifier" | "template_type" => {
1131 field_type = Some(extract_type_name(child, content));
1132 }
1133 "field_identifier" => {
1134 field_names.push(extract_identifier(child, content));
1136 }
1137 "field_declarator"
1138 | "init_declarator"
1139 | "pointer_declarator"
1140 | "reference_declarator"
1141 | "array_declarator" => {
1142 if let Some(name) = extract_field_name(child, content) {
1144 field_names.push(name);
1145 }
1146 }
1147 _ => {}
1148 }
1149 }
1150
1151 if let Some(ftype) = field_type {
1153 let namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
1154 let field_type_fqn = resolve_type_to_fqn(&ftype, &namespace, type_map);
1155
1156 for fname in field_names {
1158 field_types.insert((class_fqn.to_string(), fname), field_type_fqn.clone());
1159 }
1160 }
1161}
1162
1163fn extract_type_name(type_node: Node, content: &[u8]) -> String {
1165 match type_node.kind() {
1166 "type_identifier" | "primitive_type" => extract_identifier(type_node, content),
1167 "qualified_identifier" => {
1168 extract_identifier(type_node, content)
1170 }
1171 "template_type" => {
1172 if let Some(name) = type_node.child_by_field_name("name") {
1174 extract_identifier(name, content)
1175 } else {
1176 extract_identifier(type_node, content)
1177 }
1178 }
1179 _ => {
1180 extract_identifier(type_node, content)
1182 }
1183 }
1184}
1185
1186fn extract_field_name(declarator: Node, content: &[u8]) -> Option<String> {
1188 match declarator.kind() {
1189 "field_declarator" => {
1190 if let Some(declarator_inner) = declarator.child_by_field_name("declarator") {
1192 extract_field_name(declarator_inner, content)
1193 } else {
1194 Some(extract_identifier(declarator, content))
1195 }
1196 }
1197 "field_identifier" | "identifier" => Some(extract_identifier(declarator, content)),
1198 "pointer_declarator" | "reference_declarator" | "array_declarator" => {
1199 if let Some(declarator_inner) = declarator.child_by_field_name("declarator") {
1201 extract_field_name(declarator_inner, content)
1202 } else {
1203 None
1204 }
1205 }
1206 "init_declarator" => {
1207 if let Some(declarator_inner) = declarator.child_by_field_name("declarator") {
1209 extract_field_name(declarator_inner, content)
1210 } else {
1211 None
1212 }
1213 }
1214 _ => None,
1215 }
1216}
1217
1218fn resolve_type_to_fqn(
1220 type_name: &str,
1221 namespace: &str,
1222 type_map: &HashMap<(String, String), String>,
1223) -> String {
1224 if type_name.contains("::") {
1226 return type_name.to_string();
1227 }
1228
1229 let namespace_key = namespace.trim_end_matches("::").to_string();
1231 if let Some(fqn) = type_map.get(&(namespace_key.clone(), type_name.to_string())) {
1232 return fqn.clone();
1233 }
1234
1235 if let Some(fqn) = type_map.get(&(String::new(), type_name.to_string())) {
1237 return fqn.clone();
1238 }
1239
1240 type_name.to_string()
1242}
1243
1244fn extract_using_directive(
1246 node: Node,
1247 content: &[u8],
1248 namespace_map: &HashMap<std::ops::Range<usize>, String>,
1249 _type_map: &mut HashMap<(String, String), String>,
1250) {
1251 let _namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
1255
1256 if let Some(name_node) = node.child_by_field_name("name") {
1258 let _using_ns = extract_identifier(name_node, content);
1259 }
1263}
1264
1265fn extract_using_declaration(
1270 node: Node,
1271 content: &[u8],
1272 namespace_map: &HashMap<std::ops::Range<usize>, String>,
1273 type_map: &mut HashMap<(String, String), String>,
1274) {
1275 let namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
1276 let namespace_key = namespace.trim_end_matches("::").to_string();
1277
1278 let mut cursor = node.walk();
1280 for child in node.children(&mut cursor) {
1281 if child.kind() == "qualified_identifier" || child.kind() == "identifier" {
1282 let fqn = extract_identifier(child, content);
1283
1284 if let Some(simple_name) = fqn.split("::").last() {
1286 type_map.insert((namespace_key, simple_name.to_string()), fqn);
1288 }
1289 break;
1290 }
1291 }
1292}
1293
1294fn resolve_callee_name(
1306 callee_name: &str,
1307 caller_ctx: &FunctionContext,
1308 _ast_graph: &ASTGraph,
1309) -> String {
1310 if callee_name.starts_with("::") {
1312 return callee_name.trim_start_matches("::").to_string();
1313 }
1314
1315 if callee_name.contains("::") {
1317 if !caller_ctx.namespace_stack.is_empty() {
1319 let namespace_prefix = caller_ctx.namespace_stack.join("::");
1320 return format!("{namespace_prefix}::{callee_name}");
1321 }
1322 return callee_name.to_string();
1323 }
1324
1325 let mut parts = Vec::new();
1327
1328 if !caller_ctx.namespace_stack.is_empty() {
1330 parts.extend(caller_ctx.namespace_stack.iter().cloned());
1331 }
1332
1333 parts.push(callee_name.to_string());
1339
1340 parts.join("::")
1341}
1342
1343fn strip_type_qualifiers(type_text: &str) -> String {
1350 let mut result = type_text.trim().to_string();
1351
1352 result = result.replace("const ", "");
1354 result = result.replace("volatile ", "");
1355 result = result.replace("mutable ", "");
1356 result = result.replace("constexpr ", "");
1357
1358 result = result.replace(" const", "");
1360 result = result.replace(" volatile", "");
1361 result = result.replace(" mutable", "");
1362 result = result.replace(" constexpr", "");
1363
1364 result = result.replace(['*', '&'], "");
1366
1367 result = result.trim().to_string();
1369
1370 if let Some(last_part) = result.split("::").last() {
1372 result = last_part.to_string();
1373 }
1374
1375 if let Some(open_bracket) = result.find('<') {
1377 result = result[..open_bracket].to_string();
1378 }
1379
1380 result.trim().to_string()
1381}
1382
1383#[allow(clippy::unnecessary_wraps, clippy::too_many_lines)]
1399fn process_field_declaration(
1400 node: Node,
1401 content: &[u8],
1402 class_qualified_name: &str,
1403 visibility: &str,
1404 helper: &mut GraphBuildHelper,
1405) -> GraphResult<()> {
1406 let mut field_type_text = None;
1408 let mut field_names = Vec::new();
1409 let mut is_static_kw = false;
1412 let mut is_const = false;
1413 let mut is_constexpr = false;
1414
1415 let mut cursor = node.walk();
1416 for child in node.children(&mut cursor) {
1417 match child.kind() {
1418 "type_identifier" | "primitive_type" => {
1419 if let Ok(text) = child.utf8_text(content) {
1420 field_type_text = Some(text.to_string());
1421 }
1422 }
1423 "qualified_identifier" => {
1424 if let Ok(text) = child.utf8_text(content) {
1426 field_type_text = Some(text.to_string());
1427 }
1428 }
1429 "template_type" => {
1430 if let Ok(text) = child.utf8_text(content) {
1432 field_type_text = Some(text.to_string());
1433 }
1434 }
1435 "sized_type_specifier" => {
1436 if let Ok(text) = child.utf8_text(content) {
1438 field_type_text = Some(text.to_string());
1439 }
1440 }
1441 "type_qualifier" => {
1442 if let Ok(text) = child.utf8_text(content) {
1449 let trimmed = text.trim();
1450 if trimmed == "const" {
1451 is_const = true;
1452 } else if trimmed == "constexpr" {
1453 is_constexpr = true;
1454 }
1455 if field_type_text.is_none() {
1456 field_type_text = Some(text.to_string());
1457 }
1458 }
1459 }
1460 "storage_class_specifier" => {
1461 if let Ok(text) = child.utf8_text(content) {
1464 let trimmed = text.trim();
1465 if trimmed == "static" {
1466 is_static_kw = true;
1467 } else if trimmed == "constexpr" {
1468 is_constexpr = true;
1469 }
1470 }
1471 }
1472 "auto" => {
1473 field_type_text = Some("auto".to_string());
1475 }
1476 "decltype" => {
1477 if let Ok(text) = child.utf8_text(content) {
1479 field_type_text = Some(text.to_string());
1480 }
1481 }
1482 "struct_specifier" | "class_specifier" | "enum_specifier" | "union_specifier" => {
1483 if let Ok(text) = child.utf8_text(content) {
1485 field_type_text = Some(text.to_string());
1486 }
1487 }
1488 "field_identifier" => {
1489 if let Ok(name) = child.utf8_text(content) {
1490 field_names.push(name.trim().to_string());
1491 }
1492 }
1493 "field_declarator"
1494 | "pointer_declarator"
1495 | "reference_declarator"
1496 | "init_declarator" => {
1497 if let Some(name) = extract_field_name(child, content) {
1499 field_names.push(name);
1500 }
1501 }
1502 _ => {}
1503 }
1504 }
1505
1506 if let Some(type_text) = field_type_text {
1508 let base_type = strip_type_qualifiers(&type_text);
1509 let is_constant = is_const || is_constexpr;
1510
1511 for field_name in field_names {
1512 let field_qualified = format!("{class_qualified_name}.{field_name}");
1515 let span = span_from_node(node);
1516
1517 let field_id = if is_constant {
1521 helper.add_constant_with_name_static_and_visibility(
1522 &field_name,
1523 &field_qualified,
1524 Some(span),
1525 is_static_kw,
1526 Some(visibility),
1527 )
1528 } else {
1529 helper.add_property_with_name_static_and_visibility(
1530 &field_name,
1531 &field_qualified,
1532 Some(span),
1533 is_static_kw,
1534 Some(visibility),
1535 )
1536 };
1537
1538 let type_id = helper.add_type(&base_type, None);
1540
1541 helper.add_typeof_edge_with_context(
1543 field_id,
1544 type_id,
1545 Some(sqry_core::graph::unified::edge::kind::TypeOfContext::Field),
1546 None,
1547 Some(&field_name),
1548 );
1549
1550 helper.add_reference_edge(field_id, type_id);
1553 }
1554 }
1555
1556 Ok(())
1557}
1558
1559#[allow(clippy::unnecessary_wraps)]
1561fn process_global_variable_declaration(
1562 node: Node,
1563 content: &[u8],
1564 namespace_stack: &[String],
1565 helper: &mut GraphBuildHelper,
1566) -> GraphResult<()> {
1567 if node.kind() != "declaration" {
1569 return Ok(());
1570 }
1571
1572 let mut cursor_check = node.walk();
1575 for child in node.children(&mut cursor_check) {
1576 if child.kind() == "function_declarator" {
1577 return Ok(());
1578 }
1579 }
1580
1581 let mut type_text = None;
1583 let mut var_names = Vec::new();
1584
1585 let mut cursor = node.walk();
1586 for child in node.children(&mut cursor) {
1587 match child.kind() {
1588 "type_identifier" | "primitive_type" | "qualified_identifier" | "template_type" => {
1589 if let Ok(text) = child.utf8_text(content) {
1590 type_text = Some(text.to_string());
1591 }
1592 }
1593 "init_declarator" => {
1594 if let Some(declarator) = child.child_by_field_name("declarator")
1596 && let Some(name) = extract_declarator_name(declarator, content)
1597 {
1598 var_names.push(name);
1599 }
1600 }
1601 "pointer_declarator" | "reference_declarator" => {
1602 if let Some(name) = extract_declarator_name(child, content) {
1603 var_names.push(name);
1604 }
1605 }
1606 "identifier" => {
1607 if let Ok(name) = child.utf8_text(content) {
1609 var_names.push(name.to_string());
1610 }
1611 }
1612 _ => {}
1613 }
1614 }
1615
1616 if let Some(type_text) = type_text {
1617 let base_type = strip_type_qualifiers(&type_text);
1618
1619 for var_name in var_names {
1620 let qualified = if namespace_stack.is_empty() {
1622 var_name.clone()
1623 } else {
1624 format!("{}::{}", namespace_stack.join("::"), var_name)
1625 };
1626
1627 let span = span_from_node(node);
1628
1629 let var_id = helper.add_node_with_visibility(
1631 &qualified,
1632 Some(span),
1633 sqry_core::graph::unified::node::NodeKind::Variable,
1634 Some("public"),
1635 );
1636
1637 let type_id = helper.add_type(&base_type, None);
1639
1640 helper.add_typeof_edge(var_id, type_id);
1642 helper.add_reference_edge(var_id, type_id);
1643 }
1644 }
1645
1646 Ok(())
1647}
1648
1649fn extract_declarator_name(node: Node, content: &[u8]) -> Option<String> {
1651 match node.kind() {
1652 "identifier" => {
1653 if let Ok(name) = node.utf8_text(content) {
1654 Some(name.to_string())
1655 } else {
1656 None
1657 }
1658 }
1659 "pointer_declarator" | "reference_declarator" | "array_declarator" => {
1660 if let Some(inner) = node.child_by_field_name("declarator") {
1662 extract_declarator_name(inner, content)
1663 } else {
1664 let mut cursor = node.walk();
1666 for child in node.children(&mut cursor) {
1667 if child.kind() == "identifier"
1668 && let Ok(name) = child.utf8_text(content)
1669 {
1670 return Some(name.to_string());
1671 }
1672 }
1673 None
1674 }
1675 }
1676 "init_declarator" => {
1677 if let Some(inner) = node.child_by_field_name("declarator") {
1679 extract_declarator_name(inner, content)
1680 } else {
1681 None
1682 }
1683 }
1684 "field_declarator" => {
1685 if let Some(inner) = node.child_by_field_name("declarator") {
1687 extract_declarator_name(inner, content)
1688 } else {
1689 if let Ok(name) = node.utf8_text(content) {
1691 Some(name.to_string())
1692 } else {
1693 None
1694 }
1695 }
1696 }
1697 _ => None,
1698 }
1699}
1700
1701#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
1703fn walk_class_body(
1704 body_node: Node,
1705 content: &[u8],
1706 class_qualified_name: &str,
1707 is_struct: bool,
1708 ast_graph: &ASTGraph,
1709 helper: &mut GraphBuildHelper,
1710 seen_includes: &mut HashSet<String>,
1711 namespace_stack: &mut Vec<String>,
1712 class_stack: &mut Vec<String>,
1713 ffi_registry: &FfiRegistry,
1714 pure_virtual_registry: &PureVirtualRegistry,
1715 budget: &mut BuildBudget,
1716) -> GraphResult<()> {
1717 let mut current_visibility = if is_struct { "public" } else { "private" };
1719
1720 let mut cursor = body_node.walk();
1721 for child in body_node.children(&mut cursor) {
1722 budget.checkpoint("cpp:walk_class_body")?;
1723 match child.kind() {
1724 "access_specifier" => {
1725 if let Ok(text) = child.utf8_text(content) {
1727 let spec = text.trim().trim_end_matches(':').trim();
1728 current_visibility = spec;
1729 }
1730 }
1731 "field_declaration" => {
1732 let mut handled_nested = false;
1748 let mut inner_cursor = child.walk();
1749 for inner in child.children(&mut inner_cursor) {
1750 let kind = inner.kind();
1751 if !matches!(
1752 kind,
1753 "class_specifier"
1754 | "struct_specifier"
1755 | "union_specifier"
1756 | "enum_specifier"
1757 ) {
1758 continue;
1759 }
1760
1761 let is_struct_or_union = matches!(kind, "struct_specifier" | "union_specifier");
1762
1763 if let Some(name_node) = inner.child_by_field_name("name") {
1764 if let Ok(inner_name) = name_node.utf8_text(content) {
1776 let inner_name = inner_name.trim();
1777 let nested_qualified = format!("{class_qualified_name}::{inner_name}");
1778 let nested_span = span_from_node(inner);
1779
1780 if kind == "enum_specifier" {
1784 helper.add_enum_with_visibility(
1788 &nested_qualified,
1789 Some(nested_span),
1790 Some(current_visibility),
1791 );
1792 } else {
1793 let nested_id = if is_struct_or_union {
1794 helper.add_struct_with_visibility(
1795 &nested_qualified,
1796 Some(nested_span),
1797 Some(current_visibility),
1798 )
1799 } else {
1800 helper.add_class_with_visibility(
1801 &nested_qualified,
1802 Some(nested_span),
1803 Some(current_visibility),
1804 )
1805 };
1806 build_inheritance_and_implements_edges(
1807 inner,
1808 content,
1809 &nested_qualified,
1810 nested_id,
1811 helper,
1812 namespace_stack,
1813 pure_virtual_registry,
1814 )?;
1815 }
1816
1817 if matches!(
1822 kind,
1823 "class_specifier" | "struct_specifier" | "union_specifier"
1824 ) && let Some(body) = inner.child_by_field_name("body")
1825 {
1826 walk_class_body(
1827 body,
1828 content,
1829 &nested_qualified,
1830 is_struct_or_union,
1831 ast_graph,
1832 helper,
1833 seen_includes,
1834 namespace_stack,
1835 class_stack,
1836 ffi_registry,
1837 pure_virtual_registry,
1838 budget,
1839 )?;
1840 }
1841 handled_nested = true;
1842 }
1843 } else if let Some(body) = inner.child_by_field_name("body") {
1844 let mut anon_cursor = body.walk();
1849 for anon_child in body.children(&mut anon_cursor) {
1850 if anon_child.kind() == "field_declaration" {
1851 process_field_declaration(
1852 anon_child,
1853 content,
1854 class_qualified_name,
1855 current_visibility,
1856 helper,
1857 )?;
1858 }
1859 }
1860 handled_nested = true;
1861 }
1862 }
1863
1864 let _ = handled_nested;
1877 process_field_declaration(
1878 child,
1879 content,
1880 class_qualified_name,
1881 current_visibility,
1882 helper,
1883 )?;
1884 }
1885 "function_definition" => {
1886 if let Some(context) = ast_graph.context_for_start(child.start_byte()) {
1889 let span = span_from_node(child);
1890 helper.add_method_with_signature(
1891 &context.qualified_name,
1892 Some(span),
1893 false, context.is_static,
1895 Some(current_visibility),
1896 context.return_type.as_deref(),
1897 );
1898 }
1899 walk_tree_for_graph(
1901 child,
1902 content,
1903 ast_graph,
1904 helper,
1905 seen_includes,
1906 namespace_stack,
1907 class_stack,
1908 ffi_registry,
1909 pure_virtual_registry,
1910 budget,
1911 )?;
1912 }
1913 _ => {
1914 walk_tree_for_graph(
1916 child,
1917 content,
1918 ast_graph,
1919 helper,
1920 seen_includes,
1921 namespace_stack,
1922 class_stack,
1923 ffi_registry,
1924 pure_virtual_registry,
1925 budget,
1926 )?;
1927 }
1928 }
1929 }
1930
1931 Ok(())
1932}
1933
1934#[allow(clippy::too_many_arguments)]
1936#[allow(clippy::too_many_lines)] fn walk_tree_for_graph(
1938 node: Node,
1939 content: &[u8],
1940 ast_graph: &ASTGraph,
1941 helper: &mut GraphBuildHelper,
1942 seen_includes: &mut HashSet<String>,
1943 namespace_stack: &mut Vec<String>,
1944 class_stack: &mut Vec<String>,
1945 ffi_registry: &FfiRegistry,
1946 pure_virtual_registry: &PureVirtualRegistry,
1947 budget: &mut BuildBudget,
1948) -> GraphResult<()> {
1949 budget.checkpoint("cpp:walk_tree_for_graph")?;
1950 match node.kind() {
1951 "preproc_include" => {
1952 build_import_edge(node, content, helper, seen_includes)?;
1954 }
1955 "linkage_specification" => {
1956 build_ffi_block_for_staging(node, content, helper, namespace_stack);
1958 }
1959 "namespace_definition" => {
1960 if let Some(name_node) = node.child_by_field_name("name")
1962 && let Ok(ns_name) = name_node.utf8_text(content)
1963 {
1964 namespace_stack.push(ns_name.trim().to_string());
1965
1966 let mut cursor = node.walk();
1968 for child in node.children(&mut cursor) {
1969 walk_tree_for_graph(
1970 child,
1971 content,
1972 ast_graph,
1973 helper,
1974 seen_includes,
1975 namespace_stack,
1976 class_stack,
1977 ffi_registry,
1978 pure_virtual_registry,
1979 budget,
1980 )?;
1981 }
1982
1983 namespace_stack.pop();
1984 return Ok(());
1985 }
1986 }
1987 "class_specifier" | "struct_specifier" | "union_specifier" => {
1988 if let Some(name_node) = node.child_by_field_name("name")
1990 && let Ok(class_name) = name_node.utf8_text(content)
1991 {
1992 let class_name = class_name.trim();
1993 let span = span_from_node(node);
1994 let is_struct = matches!(node.kind(), "struct_specifier" | "union_specifier");
1997
1998 let qualified_class =
2000 build_qualified_name(namespace_stack, class_stack, class_name);
2001
2002 let visibility = "public";
2004 let class_id = if is_struct {
2005 helper.add_struct_with_visibility(
2006 &qualified_class,
2007 Some(span),
2008 Some(visibility),
2009 )
2010 } else {
2011 helper.add_class_with_visibility(&qualified_class, Some(span), Some(visibility))
2012 };
2013
2014 build_inheritance_and_implements_edges(
2017 node,
2018 content,
2019 &qualified_class,
2020 class_id,
2021 helper,
2022 namespace_stack,
2023 pure_virtual_registry,
2024 )?;
2025
2026 if class_stack.is_empty() {
2029 let module_id = helper.add_module(FILE_MODULE_NAME, None);
2030 helper.add_export_edge(module_id, class_id);
2031 }
2032
2033 class_stack.push(class_name.to_string());
2035
2036 if let Some(body) = node.child_by_field_name("body") {
2039 walk_class_body(
2040 body,
2041 content,
2042 &qualified_class,
2043 is_struct,
2044 ast_graph,
2045 helper,
2046 seen_includes,
2047 namespace_stack,
2048 class_stack,
2049 ffi_registry,
2050 pure_virtual_registry,
2051 budget,
2052 )?;
2053 }
2054
2055 class_stack.pop();
2056 return Ok(());
2057 }
2058 }
2059 "enum_specifier" => {
2060 if let Some(name_node) = node.child_by_field_name("name")
2061 && let Ok(enum_name) = name_node.utf8_text(content)
2062 {
2063 let enum_name = enum_name.trim();
2064 let span = span_from_node(node);
2065 let qualified_enum = build_qualified_name(namespace_stack, class_stack, enum_name);
2066 let enum_id = helper.add_enum(&qualified_enum, Some(span));
2067
2068 if class_stack.is_empty() {
2069 let module_id = helper.add_module(FILE_MODULE_NAME, None);
2070 helper.add_export_edge(module_id, enum_id);
2071 }
2072 }
2073 }
2074 "function_definition" => {
2075 if !class_stack.is_empty() {
2079 let mut cursor = node.walk();
2082 for child in node.children(&mut cursor) {
2083 walk_tree_for_graph(
2084 child,
2085 content,
2086 ast_graph,
2087 helper,
2088 seen_includes,
2089 namespace_stack,
2090 class_stack,
2091 ffi_registry,
2092 pure_virtual_registry,
2093 budget,
2094 )?;
2095 }
2096 return Ok(());
2097 }
2098
2099 if let Some(context) = ast_graph.context_for_start(node.start_byte()) {
2101 let span = span_from_node(node);
2102
2103 if context.class_stack.is_empty() {
2105 let visibility = if context.is_static {
2108 "private"
2109 } else {
2110 "public"
2111 };
2112 let fn_id = helper.add_function_with_signature(
2113 &context.qualified_name,
2114 Some(span),
2115 false, false, Some(visibility),
2118 context.return_type.as_deref(),
2119 );
2120
2121 if !context.is_static {
2123 let module_id = helper.add_module(FILE_MODULE_NAME, None);
2124 helper.add_export_edge(module_id, fn_id);
2125 }
2126 } else {
2127 helper.add_method_with_signature(
2132 &context.qualified_name,
2133 Some(span),
2134 false, context.is_static,
2136 Some("public"), context.return_type.as_deref(),
2138 );
2139 }
2140 }
2141 }
2142 "call_expression" => {
2143 if let Ok(Some((caller_qname, callee_qname, argument_count, span))) =
2145 build_call_for_staging(ast_graph, node, content)
2146 {
2147 let caller_function_id =
2149 helper.ensure_callee(&caller_qname, span, CalleeKindHint::Function);
2150 let argument_count = u8::try_from(argument_count).unwrap_or(u8::MAX);
2151
2152 let is_unqualified = !callee_qname.contains("::");
2155 if is_unqualified {
2156 if let Some((ffi_qualified, ffi_convention)) = ffi_registry.get(&callee_qname) {
2157 let ffi_target_id =
2159 helper.ensure_callee(ffi_qualified, span, CalleeKindHint::Function);
2160 helper.add_ffi_edge(caller_function_id, ffi_target_id, *ffi_convention);
2161 } else {
2162 let target_function_id =
2164 helper.ensure_callee(&callee_qname, span, CalleeKindHint::Function);
2165 helper.add_call_edge_full_with_span(
2166 caller_function_id,
2167 target_function_id,
2168 argument_count,
2169 false,
2170 vec![span],
2171 );
2172 }
2173 } else {
2174 let target_function_id =
2176 helper.ensure_callee(&callee_qname, span, CalleeKindHint::Function);
2177 helper.add_call_edge_full_with_span(
2178 caller_function_id,
2179 target_function_id,
2180 argument_count,
2181 false,
2182 vec![span],
2183 );
2184 }
2185 }
2186 }
2187 "declaration" => {
2188 if class_stack.is_empty() {
2191 process_global_variable_declaration(node, content, namespace_stack, helper)?;
2192 }
2193 }
2194 _ => {}
2195 }
2196
2197 let mut cursor = node.walk();
2199 for child in node.children(&mut cursor) {
2200 walk_tree_for_graph(
2201 child,
2202 content,
2203 ast_graph,
2204 helper,
2205 seen_includes,
2206 namespace_stack,
2207 class_stack,
2208 ffi_registry,
2209 pure_virtual_registry,
2210 budget,
2211 )?;
2212 }
2213
2214 Ok(())
2215}
2216
2217fn build_call_for_staging(
2219 ast_graph: &ASTGraph,
2220 call_node: Node<'_>,
2221 content: &[u8],
2222) -> GraphResult<Option<(String, String, usize, Span)>> {
2223 let call_context = ast_graph.find_enclosing(call_node.start_byte());
2225 let caller_qualified_name = if let Some(ctx) = call_context {
2226 ctx.qualified_name.clone()
2227 } else {
2228 return Ok(None);
2230 };
2231
2232 let Some(function_node) = call_node.child_by_field_name("function") else {
2233 return Ok(None);
2234 };
2235
2236 let callee_text = function_node
2237 .utf8_text(content)
2238 .map_err(|_| GraphBuilderError::ParseError {
2239 span: span_from_node(call_node),
2240 reason: "failed to read call expression".to_string(),
2241 })?
2242 .trim();
2243
2244 if callee_text.is_empty() {
2245 return Ok(None);
2246 }
2247
2248 let target_qualified_name = if let Some(ctx) = call_context {
2250 resolve_callee_name(callee_text, ctx, ast_graph)
2251 } else {
2252 callee_text.to_string()
2253 };
2254
2255 let span = span_from_node(call_node);
2256 let argument_count = count_arguments(call_node);
2257
2258 Ok(Some((
2259 caller_qualified_name,
2260 target_qualified_name,
2261 argument_count,
2262 span,
2263 )))
2264}
2265
2266fn build_import_edge(
2273 include_node: Node<'_>,
2274 content: &[u8],
2275 helper: &mut GraphBuildHelper,
2276 seen_includes: &mut HashSet<String>,
2277) -> GraphResult<()> {
2278 let path_node = include_node.child_by_field_name("path").or_else(|| {
2280 let mut cursor = include_node.walk();
2282 include_node.children(&mut cursor).find(|child| {
2283 matches!(
2284 child.kind(),
2285 "system_lib_string" | "string_literal" | "string_content"
2286 )
2287 })
2288 });
2289
2290 let Some(path_node) = path_node else {
2291 return Ok(());
2292 };
2293
2294 let include_path = path_node
2295 .utf8_text(content)
2296 .map_err(|_| GraphBuilderError::ParseError {
2297 span: span_from_node(include_node),
2298 reason: "failed to read include path".to_string(),
2299 })?
2300 .trim();
2301
2302 if include_path.is_empty() {
2303 return Ok(());
2304 }
2305
2306 let is_system_include = include_path.starts_with('<') && include_path.ends_with('>');
2308 let cleaned_path = if is_system_include {
2309 include_path.trim_start_matches('<').trim_end_matches('>')
2311 } else {
2312 include_path.trim_start_matches('"').trim_end_matches('"')
2314 };
2315
2316 if cleaned_path.is_empty() {
2317 return Ok(());
2318 }
2319
2320 if !seen_includes.insert(cleaned_path.to_string()) {
2322 return Ok(()); }
2324
2325 let file_module_id = helper.add_module("<file>", None);
2327
2328 let span = span_from_node(include_node);
2330 let import_id = helper.add_import(cleaned_path, Some(span));
2331
2332 helper.add_import_edge(file_module_id, import_id);
2335
2336 Ok(())
2337}
2338
2339fn collect_ffi_declarations(
2350 node: Node<'_>,
2351 content: &[u8],
2352 ffi_registry: &mut FfiRegistry,
2353 budget: &mut BuildBudget,
2354) -> GraphResult<()> {
2355 budget.checkpoint("cpp:collect_ffi_declarations")?;
2356 if node.kind() == "linkage_specification" {
2357 let abi = extract_ffi_abi(node, content);
2359 let convention = abi_to_convention(&abi);
2360
2361 if let Some(body_node) = node.child_by_field_name("body") {
2363 collect_ffi_from_body(body_node, content, &abi, convention, ffi_registry);
2364 }
2365 }
2366
2367 let mut cursor = node.walk();
2369 for child in node.children(&mut cursor) {
2370 collect_ffi_declarations(child, content, ffi_registry, budget)?;
2371 }
2372
2373 Ok(())
2374}
2375
2376fn collect_ffi_from_body(
2378 body_node: Node<'_>,
2379 content: &[u8],
2380 abi: &str,
2381 convention: FfiConvention,
2382 ffi_registry: &mut FfiRegistry,
2383) {
2384 match body_node.kind() {
2385 "declaration_list" => {
2386 let mut cursor = body_node.walk();
2388 for decl in body_node.children(&mut cursor) {
2389 if decl.kind() == "declaration"
2390 && let Some(fn_name) = extract_ffi_function_name(decl, content)
2391 {
2392 let qualified = format!("extern::{abi}::{fn_name}");
2393 ffi_registry.insert(fn_name, (qualified, convention));
2394 }
2395 }
2396 }
2397 "declaration" => {
2398 if let Some(fn_name) = extract_ffi_function_name(body_node, content) {
2400 let qualified = format!("extern::{abi}::{fn_name}");
2401 ffi_registry.insert(fn_name, (qualified, convention));
2402 }
2403 }
2404 _ => {}
2405 }
2406}
2407
2408fn extract_ffi_function_name(decl_node: Node<'_>, content: &[u8]) -> Option<String> {
2410 if let Some(declarator_node) = decl_node.child_by_field_name("declarator") {
2412 return extract_function_name_from_declarator(declarator_node, content);
2413 }
2414 None
2415}
2416
2417fn extract_function_name_from_declarator(node: Node<'_>, content: &[u8]) -> Option<String> {
2419 match node.kind() {
2420 "function_declarator" => {
2421 if let Some(inner) = node.child_by_field_name("declarator") {
2423 return extract_function_name_from_declarator(inner, content);
2424 }
2425 }
2426 "identifier" => {
2427 if let Ok(name) = node.utf8_text(content) {
2429 let name = name.trim();
2430 if !name.is_empty() {
2431 return Some(name.to_string());
2432 }
2433 }
2434 }
2435 "pointer_declarator" | "reference_declarator" => {
2436 if let Some(inner) = node.child_by_field_name("declarator") {
2438 return extract_function_name_from_declarator(inner, content);
2439 }
2440 }
2441 "parenthesized_declarator" => {
2442 let mut cursor = node.walk();
2444 for child in node.children(&mut cursor) {
2445 if let Some(name) = extract_function_name_from_declarator(child, content) {
2446 return Some(name);
2447 }
2448 }
2449 }
2450 _ => {}
2451 }
2452 None
2453}
2454
2455fn extract_ffi_abi(node: Node<'_>, content: &[u8]) -> String {
2459 if let Some(value_node) = node.child_by_field_name("value")
2461 && value_node.kind() == "string_literal"
2462 {
2463 let mut cursor = value_node.walk();
2465 for child in value_node.children(&mut cursor) {
2466 if child.kind() == "string_content"
2467 && let Ok(text) = child.utf8_text(content)
2468 {
2469 let trimmed = text.trim();
2470 if !trimmed.is_empty() {
2471 return trimmed.to_string();
2472 }
2473 }
2474 }
2475 }
2476 "C".to_string()
2478}
2479
2480fn abi_to_convention(abi: &str) -> FfiConvention {
2482 match abi.to_lowercase().as_str() {
2483 "system" => FfiConvention::System,
2484 "stdcall" => FfiConvention::Stdcall,
2485 "fastcall" => FfiConvention::Fastcall,
2486 "cdecl" => FfiConvention::Cdecl,
2487 _ => FfiConvention::C, }
2489}
2490
2491fn build_ffi_block_for_staging(
2495 node: Node<'_>,
2496 content: &[u8],
2497 helper: &mut GraphBuildHelper,
2498 namespace_stack: &[String],
2499) {
2500 let abi = extract_ffi_abi(node, content);
2502
2503 if let Some(body_node) = node.child_by_field_name("body") {
2505 build_ffi_from_body(body_node, content, &abi, helper, namespace_stack);
2506 }
2507}
2508
2509fn build_ffi_from_body(
2511 body_node: Node<'_>,
2512 content: &[u8],
2513 abi: &str,
2514 helper: &mut GraphBuildHelper,
2515 namespace_stack: &[String],
2516) {
2517 match body_node.kind() {
2518 "declaration_list" => {
2519 let mut cursor = body_node.walk();
2521 for decl in body_node.children(&mut cursor) {
2522 if decl.kind() == "declaration"
2523 && let Some(fn_name) = extract_ffi_function_name(decl, content)
2524 {
2525 let span = span_from_node(decl);
2526 let qualified = if namespace_stack.is_empty() {
2528 format!("extern::{abi}::{fn_name}")
2529 } else {
2530 format!("{}::extern::{abi}::{fn_name}", namespace_stack.join("::"))
2531 };
2532 helper.add_function(
2534 &qualified,
2535 Some(span),
2536 false, true, );
2539 }
2540 }
2541 }
2542 "declaration" => {
2543 if let Some(fn_name) = extract_ffi_function_name(body_node, content) {
2545 let span = span_from_node(body_node);
2546 let qualified = if namespace_stack.is_empty() {
2547 format!("extern::{abi}::{fn_name}")
2548 } else {
2549 format!("{}::extern::{abi}::{fn_name}", namespace_stack.join("::"))
2550 };
2551 helper.add_function(&qualified, Some(span), false, true);
2552 }
2553 }
2554 _ => {}
2555 }
2556}
2557
2558fn collect_pure_virtual_interfaces(
2568 node: Node<'_>,
2569 content: &[u8],
2570 registry: &mut PureVirtualRegistry,
2571 budget: &mut BuildBudget,
2572) -> GraphResult<()> {
2573 budget.checkpoint("cpp:collect_pure_virtual_interfaces")?;
2574 if matches!(node.kind(), "class_specifier" | "struct_specifier")
2575 && let Some(name_node) = node.child_by_field_name("name")
2576 && let Ok(class_name) = name_node.utf8_text(content)
2577 {
2578 let class_name = class_name.trim();
2579 if !class_name.is_empty() && has_pure_virtual_methods(node, content) {
2580 registry.insert(class_name.to_string());
2581 }
2582 }
2583
2584 let mut cursor = node.walk();
2586 for child in node.children(&mut cursor) {
2587 collect_pure_virtual_interfaces(child, content, registry, budget)?;
2588 }
2589
2590 Ok(())
2591}
2592
2593fn has_pure_virtual_methods(class_node: Node<'_>, content: &[u8]) -> bool {
2597 if let Some(body) = class_node.child_by_field_name("body") {
2598 let mut cursor = body.walk();
2599 for child in body.children(&mut cursor) {
2600 if child.kind() == "field_declaration" && is_pure_virtual_declaration(child, content) {
2602 return true;
2603 }
2604 }
2605 }
2606 false
2607}
2608
2609fn is_pure_virtual_declaration(decl_node: Node<'_>, content: &[u8]) -> bool {
2611 let mut has_virtual = false;
2612 let mut has_pure_specifier = false;
2613
2614 let mut cursor = decl_node.walk();
2616 for child in decl_node.children(&mut cursor) {
2617 match child.kind() {
2618 "virtual" => {
2619 has_virtual = true;
2620 }
2621 "number_literal" => {
2622 if let Ok(text) = child.utf8_text(content)
2625 && text.trim() == "0"
2626 {
2627 has_pure_specifier = true;
2628 }
2629 }
2630 _ => {}
2631 }
2632 }
2633
2634 has_virtual && has_pure_specifier
2635}
2636
2637fn build_inheritance_and_implements_edges(
2643 class_node: Node<'_>,
2644 content: &[u8],
2645 _qualified_class_name: &str,
2646 child_id: sqry_core::graph::unified::node::NodeId,
2647 helper: &mut GraphBuildHelper,
2648 namespace_stack: &[String],
2649 pure_virtual_registry: &PureVirtualRegistry,
2650) -> GraphResult<()> {
2651 let mut cursor = class_node.walk();
2653 let base_clause = class_node
2654 .children(&mut cursor)
2655 .find(|child| child.kind() == "base_class_clause");
2656
2657 let Some(base_clause) = base_clause else {
2658 return Ok(()); };
2660
2661 let mut clause_cursor = base_clause.walk();
2663 for child in base_clause.children(&mut clause_cursor) {
2664 match child.kind() {
2665 "type_identifier" => {
2666 let base_name = child
2667 .utf8_text(content)
2668 .map_err(|_| GraphBuilderError::ParseError {
2669 span: span_from_node(child),
2670 reason: "failed to read base class name".to_string(),
2671 })?
2672 .trim();
2673
2674 if !base_name.is_empty() {
2675 let qualified_base = if namespace_stack.is_empty() {
2677 base_name.to_string()
2678 } else {
2679 format!("{}::{}", namespace_stack.join("::"), base_name)
2680 };
2681
2682 if pure_virtual_registry.contains(base_name) {
2684 let interface_id = helper.add_interface(&qualified_base, None);
2686 helper.add_implements_edge(child_id, interface_id);
2687 } else {
2688 let parent_id = helper.add_class(&qualified_base, None);
2690 helper.add_inherits_edge(child_id, parent_id);
2691 }
2692 }
2693 }
2694 "qualified_identifier" => {
2695 let base_name = child
2697 .utf8_text(content)
2698 .map_err(|_| GraphBuilderError::ParseError {
2699 span: span_from_node(child),
2700 reason: "failed to read base class name".to_string(),
2701 })?
2702 .trim();
2703
2704 if !base_name.is_empty() {
2705 let simple_name = base_name.rsplit("::").next().unwrap_or(base_name);
2707
2708 if pure_virtual_registry.contains(simple_name) {
2709 let interface_id = helper.add_interface(base_name, None);
2710 helper.add_implements_edge(child_id, interface_id);
2711 } else {
2712 let parent_id = helper.add_class(base_name, None);
2713 helper.add_inherits_edge(child_id, parent_id);
2714 }
2715 }
2716 }
2717 "template_type" => {
2718 if let Some(template_name_node) = child.child_by_field_name("name")
2720 && let Ok(base_name) = template_name_node.utf8_text(content)
2721 {
2722 let base_name = base_name.trim();
2723 if !base_name.is_empty() {
2724 let qualified_base =
2725 if base_name.contains("::") || namespace_stack.is_empty() {
2726 base_name.to_string()
2727 } else {
2728 format!("{}::{}", namespace_stack.join("::"), base_name)
2729 };
2730
2731 if pure_virtual_registry.contains(base_name) {
2734 let interface_id = helper.add_interface(&qualified_base, None);
2735 helper.add_implements_edge(child_id, interface_id);
2736 } else {
2737 let parent_id = helper.add_class(&qualified_base, None);
2738 helper.add_inherits_edge(child_id, parent_id);
2739 }
2740 }
2741 }
2742 }
2743 _ => {
2744 }
2746 }
2747 }
2748
2749 Ok(())
2750}
2751
2752fn span_from_node(node: Node<'_>) -> Span {
2753 let start = node.start_position();
2754 let end = node.end_position();
2755 Span::new(
2756 sqry_core::graph::node::Position::new(start.row, start.column),
2757 sqry_core::graph::node::Position::new(end.row, end.column),
2758 )
2759}
2760
2761fn count_arguments(node: Node<'_>) -> usize {
2762 node.child_by_field_name("arguments").map_or(0, |args| {
2763 let mut count = 0;
2764 let mut cursor = args.walk();
2765 for child in args.children(&mut cursor) {
2766 if !matches!(child.kind(), "(" | ")" | ",") {
2767 count += 1;
2768 }
2769 }
2770 count
2771 })
2772}
2773
2774#[cfg(test)]
2775mod tests {
2776 use super::*;
2777 use sqry_core::graph::unified::build::test_helpers::{
2778 assert_has_node, assert_has_node_with_kind, assert_has_node_with_kind_exact,
2779 collect_call_edges,
2780 };
2781 use sqry_core::graph::unified::node::NodeKind;
2782 use tree_sitter::Parser;
2783
2784 fn parse_cpp(source: &str) -> Tree {
2785 let mut parser = Parser::new();
2786 parser
2787 .set_language(&tree_sitter_cpp::LANGUAGE.into())
2788 .expect("Failed to set Cpp language");
2789 parser
2790 .parse(source.as_bytes(), None)
2791 .expect("Failed to parse Cpp source")
2792 }
2793
2794 fn test_budget() -> BuildBudget {
2795 BuildBudget::new(Path::new("test.cpp"))
2796 }
2797
2798 fn extract_namespace_map_for_test(
2799 tree: &Tree,
2800 source: &str,
2801 ) -> HashMap<std::ops::Range<usize>, String> {
2802 let mut budget = test_budget();
2803 extract_namespace_map(tree.root_node(), source.as_bytes(), &mut budget)
2804 .expect("namespace extraction should succeed in tests")
2805 }
2806
2807 fn extract_cpp_contexts_for_test(
2808 tree: &Tree,
2809 source: &str,
2810 namespace_map: &HashMap<std::ops::Range<usize>, String>,
2811 ) -> Vec<FunctionContext> {
2812 let mut budget = test_budget();
2813 extract_cpp_contexts(
2814 tree.root_node(),
2815 source.as_bytes(),
2816 namespace_map,
2817 &mut budget,
2818 )
2819 .expect("context extraction should succeed in tests")
2820 }
2821
2822 fn extract_field_and_type_info_for_test(
2823 tree: &Tree,
2824 source: &str,
2825 namespace_map: &HashMap<std::ops::Range<usize>, String>,
2826 ) -> (QualifiedNameMap, QualifiedNameMap) {
2827 let mut budget = test_budget();
2828 extract_field_and_type_info(
2829 tree.root_node(),
2830 source.as_bytes(),
2831 namespace_map,
2832 &mut budget,
2833 )
2834 .expect("field/type extraction should succeed in tests")
2835 }
2836
2837 #[test]
2838 fn test_build_graph_times_out_with_expired_budget() {
2839 let source = r"
2840 namespace demo {
2841 class Service {
2842 public:
2843 void process() {}
2844 };
2845 }
2846 ";
2847 let tree = parse_cpp(source);
2848 let builder = CppGraphBuilder::new();
2849 let mut staging = StagingGraph::new();
2850 let mut budget = BuildBudget::already_expired(Path::new("timeout.cpp"));
2851
2852 let err = builder
2853 .build_graph_with_budget(
2854 &tree,
2855 source.as_bytes(),
2856 Path::new("timeout.cpp"),
2857 &mut staging,
2858 &mut budget,
2859 )
2860 .expect_err("expired budget should force timeout");
2861
2862 match err {
2863 GraphBuilderError::BuildTimedOut {
2864 file,
2865 phase,
2866 timeout_ms,
2867 } => {
2868 assert_eq!(file, PathBuf::from("timeout.cpp"));
2869 assert_eq!(phase, "cpp:extract_namespace_map");
2870 assert_eq!(timeout_ms, 1_000);
2871 }
2872 other => panic!("expected BuildTimedOut, got {other:?}"),
2873 }
2874 }
2875
2876 #[test]
2877 fn test_extract_class() {
2878 let source = "class User { }";
2879 let tree = parse_cpp(source);
2880 let mut staging = StagingGraph::new();
2881 let builder = CppGraphBuilder::new();
2882
2883 let result = builder.build_graph(
2884 &tree,
2885 source.as_bytes(),
2886 Path::new("test.cpp"),
2887 &mut staging,
2888 );
2889
2890 assert!(result.is_ok());
2891 assert_has_node_with_kind(&staging, "User", NodeKind::Class);
2892 }
2893
2894 #[test]
2895 fn test_extract_template_class() {
2896 let source = r"
2897 template <typename T>
2898 class Person {
2899 public:
2900 T name;
2901 T age;
2902 };
2903 ";
2904 let tree = parse_cpp(source);
2905 let mut staging = StagingGraph::new();
2906 let builder = CppGraphBuilder::new();
2907
2908 let result = builder.build_graph(
2909 &tree,
2910 source.as_bytes(),
2911 Path::new("test.cpp"),
2912 &mut staging,
2913 );
2914
2915 assert!(result.is_ok());
2916 assert_has_node_with_kind(&staging, "Person", NodeKind::Class);
2917 }
2918
2919 #[test]
2920 fn test_nested_named_types_emit_nodes() {
2921 let source = r"
2926 class Outer {
2927 public:
2928 class Inner { int z; };
2929 struct InnerS { int w; };
2930 union InnerU { int i; float f; };
2931 enum class InnerE { A, B };
2932 class L1 { public: class L2 { int q; }; };
2933 };
2934 namespace ns {
2935 class NsOuter { public: class NsInner { int n; }; };
2936 }
2937 ";
2938 let staging = build_cpp(source);
2939
2940 assert_has_node_with_kind_exact(&staging, "Outer::Inner", NodeKind::Class);
2942 assert_has_node_with_kind_exact(&staging, "Outer::InnerS", NodeKind::Struct);
2943 assert_has_node_with_kind_exact(&staging, "Outer::InnerU", NodeKind::Struct);
2945 assert_has_node_with_kind_exact(&staging, "Outer::InnerE", NodeKind::Enum);
2946 assert_has_node_with_kind_exact(&staging, "Outer::L1", NodeKind::Class);
2948 assert_has_node_with_kind_exact(&staging, "Outer::L1::L2", NodeKind::Class);
2949 assert_has_node_with_kind_exact(&staging, "ns::NsOuter", NodeKind::Class);
2951 assert_has_node_with_kind_exact(&staging, "ns::NsOuter::NsInner", NodeKind::Class);
2952
2953 assert_has_node_with_kind_exact(&staging, "Outer::Inner.z", NodeKind::Property);
2956 assert_has_node_with_kind_exact(&staging, "Outer::L1::L2.q", NodeKind::Property);
2957 assert_has_node_with_kind_exact(&staging, "ns::NsOuter::NsInner.n", NodeKind::Property);
2958 }
2959
2960 #[test]
2961 fn test_nested_enum_carries_enclosing_visibility() {
2962 let source = r"
2966 class Outer {
2967 private:
2968 enum class Secret { A, B };
2969 public:
2970 enum class Pub { X, Y };
2971 };
2972 ";
2973 let staging = build_cpp(source);
2974
2975 let secret = cpp_find_added_node(&staging, "Outer::Secret")
2976 .expect("nested enum Outer::Secret must be staged");
2977 assert_eq!(secret.kind, NodeKind::Enum, "Secret must be an Enum node");
2978 let secret_vis = staging.resolve_local_string(
2979 secret
2980 .visibility
2981 .expect("nested enum must carry a visibility id"),
2982 );
2983 assert_eq!(
2984 secret_vis,
2985 Some("private"),
2986 "nested enum under `private:` must be private"
2987 );
2988
2989 let pub_enum = cpp_find_added_node(&staging, "Outer::Pub")
2990 .expect("nested enum Outer::Pub must be staged");
2991 let pub_vis = staging.resolve_local_string(
2992 pub_enum
2993 .visibility
2994 .expect("nested enum must carry a visibility id"),
2995 );
2996 assert_eq!(
2997 pub_vis,
2998 Some("public"),
2999 "nested enum under `public:` must be public"
3000 );
3001 }
3002
3003 #[test]
3004 fn test_nested_class_emits_inheritance_edge() {
3005 let source = r"
3009 struct Base { virtual ~Base(); };
3010 class Outer {
3011 public:
3012 class Derived : public Base {};
3013 };
3014 ";
3015 let staging = build_cpp(source);
3016
3017 let derived_id = cpp_find_added_node_id(&staging, "Outer::Derived", NodeKind::Class)
3018 .expect("nested Derived class node must be staged");
3019
3020 let has_inherits = staging.operations().iter().any(|op| {
3021 matches!(
3022 op,
3023 StagingOp::AddEdge {
3024 source: src,
3025 kind: EdgeKind::Inherits,
3026 ..
3027 } if *src == derived_id
3028 )
3029 });
3030 assert!(
3031 has_inherits,
3032 "nested Derived must emit an Inherits edge to its base"
3033 );
3034 }
3035
3036 #[test]
3037 fn test_top_level_union_emits_struct_node() {
3038 let source = "union Value { int i; float f; };";
3042 let staging = build_cpp(source);
3043 assert_has_node_with_kind_exact(&staging, "Value", NodeKind::Struct);
3044 }
3045
3046 #[test]
3047 fn test_extract_function() {
3048 let source = r#"
3049 #include <cstdio>
3050 void hello() {
3051 std::printf("Hello");
3052 }
3053 "#;
3054 let tree = parse_cpp(source);
3055 let mut staging = StagingGraph::new();
3056 let builder = CppGraphBuilder::new();
3057
3058 let result = builder.build_graph(
3059 &tree,
3060 source.as_bytes(),
3061 Path::new("test.cpp"),
3062 &mut staging,
3063 );
3064
3065 assert!(result.is_ok());
3066 assert_has_node_with_kind(&staging, "hello", NodeKind::Function);
3067 }
3068
3069 #[test]
3070 fn test_extract_virtual_function() {
3071 let source = r"
3072 class Service {
3073 public:
3074 virtual void fetchData() {}
3075 };
3076 ";
3077 let tree = parse_cpp(source);
3078 let mut staging = StagingGraph::new();
3079 let builder = CppGraphBuilder::new();
3080
3081 let result = builder.build_graph(
3082 &tree,
3083 source.as_bytes(),
3084 Path::new("test.cpp"),
3085 &mut staging,
3086 );
3087
3088 assert!(result.is_ok());
3089 assert_has_node(&staging, "fetchData");
3090 }
3091
3092 #[test]
3093 fn test_extract_call_edge() {
3094 let source = r"
3095 void greet() {}
3096
3097 int main() {
3098 greet();
3099 return 0;
3100 }
3101 ";
3102 let tree = parse_cpp(source);
3103 let mut staging = StagingGraph::new();
3104 let builder = CppGraphBuilder::new();
3105
3106 let result = builder.build_graph(
3107 &tree,
3108 source.as_bytes(),
3109 Path::new("test.cpp"),
3110 &mut staging,
3111 );
3112
3113 assert!(result.is_ok());
3114 assert_has_node(&staging, "main");
3115 assert_has_node(&staging, "greet");
3116 let calls = collect_call_edges(&staging);
3117 assert!(!calls.is_empty());
3118 }
3119
3120 #[test]
3121 fn test_extract_member_call_edge() {
3122 let source = r"
3123 class Service {
3124 public:
3125 void helper() {}
3126 };
3127
3128 int main() {
3129 Service svc;
3130 svc.helper();
3131 return 0;
3132 }
3133 ";
3134 let tree = parse_cpp(source);
3135 let mut staging = StagingGraph::new();
3136 let builder = CppGraphBuilder::new();
3137
3138 let result = builder.build_graph(
3139 &tree,
3140 source.as_bytes(),
3141 Path::new("member.cpp"),
3142 &mut staging,
3143 );
3144
3145 assert!(result.is_ok());
3146 assert_has_node(&staging, "main");
3147 assert_has_node(&staging, "helper");
3148 let calls = collect_call_edges(&staging);
3149 assert!(!calls.is_empty());
3150 }
3151
3152 #[test]
3153 fn test_extract_namespace_map_simple() {
3154 let source = r"
3155 namespace demo {
3156 void func() {}
3157 }
3158 ";
3159 let tree = parse_cpp(source);
3160 let namespace_map = extract_namespace_map_for_test(&tree, source);
3161
3162 assert_eq!(namespace_map.len(), 1);
3164
3165 let (_, ns_prefix) = namespace_map.iter().next().unwrap();
3167 assert_eq!(ns_prefix, "demo::");
3168 }
3169
3170 #[test]
3171 fn test_extract_namespace_map_nested() {
3172 let source = r"
3173 namespace outer {
3174 namespace inner {
3175 void func() {}
3176 }
3177 }
3178 ";
3179 let tree = parse_cpp(source);
3180 let namespace_map = extract_namespace_map_for_test(&tree, source);
3181
3182 assert!(namespace_map.len() >= 2);
3184
3185 let ns_values: Vec<&String> = namespace_map.values().collect();
3187 assert!(ns_values.iter().any(|v| v.as_str() == "outer::"));
3188 assert!(ns_values.iter().any(|v| v.as_str() == "outer::inner::"));
3189 }
3190
3191 #[test]
3192 fn test_extract_namespace_map_multiple() {
3193 let source = r"
3194 namespace first {
3195 void func1() {}
3196 }
3197 namespace second {
3198 void func2() {}
3199 }
3200 ";
3201 let tree = parse_cpp(source);
3202 let namespace_map = extract_namespace_map_for_test(&tree, source);
3203
3204 assert_eq!(namespace_map.len(), 2);
3206
3207 let ns_values: Vec<&String> = namespace_map.values().collect();
3208 assert!(ns_values.iter().any(|v| v.as_str() == "first::"));
3209 assert!(ns_values.iter().any(|v| v.as_str() == "second::"));
3210 }
3211
3212 #[test]
3213 fn test_find_namespace_for_offset() {
3214 let source = r"
3215 namespace demo {
3216 void func() {}
3217 }
3218 ";
3219 let tree = parse_cpp(source);
3220 let namespace_map = extract_namespace_map_for_test(&tree, source);
3221
3222 let func_offset = source.find("func").unwrap();
3224 let ns = find_namespace_for_offset(func_offset, &namespace_map);
3225 assert_eq!(ns, "demo::");
3226
3227 let ns = find_namespace_for_offset(0, &namespace_map);
3229 assert_eq!(ns, "");
3230 }
3231
3232 #[test]
3233 fn test_extract_cpp_contexts_free_function() {
3234 let source = r"
3235 void helper() {}
3236 ";
3237 let tree = parse_cpp(source);
3238 let namespace_map = extract_namespace_map_for_test(&tree, source);
3239 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3240
3241 assert_eq!(contexts.len(), 1);
3242 assert_eq!(contexts[0].qualified_name, "helper");
3243 assert!(!contexts[0].is_static);
3244 assert!(!contexts[0].is_virtual);
3245 }
3246
3247 #[test]
3248 fn test_extract_cpp_contexts_namespace_function() {
3249 let source = r"
3250 namespace demo {
3251 void helper() {}
3252 }
3253 ";
3254 let tree = parse_cpp(source);
3255 let namespace_map = extract_namespace_map_for_test(&tree, source);
3256 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3257
3258 assert_eq!(contexts.len(), 1);
3259 assert_eq!(contexts[0].qualified_name, "demo::helper");
3260 assert_eq!(contexts[0].namespace_stack, vec!["demo"]);
3261 }
3262
3263 #[test]
3264 fn test_extract_cpp_contexts_class_method() {
3265 let source = r"
3266 class Service {
3267 public:
3268 void process() {}
3269 };
3270 ";
3271 let tree = parse_cpp(source);
3272 let namespace_map = extract_namespace_map_for_test(&tree, source);
3273 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3274
3275 assert_eq!(contexts.len(), 1);
3276 assert_eq!(contexts[0].qualified_name, "Service::process");
3277 assert_eq!(contexts[0].class_stack, vec!["Service"]);
3278 }
3279
3280 #[test]
3281 fn test_extract_cpp_contexts_namespace_and_class() {
3282 let source = r"
3283 namespace demo {
3284 class Service {
3285 public:
3286 void process() {}
3287 };
3288 }
3289 ";
3290 let tree = parse_cpp(source);
3291 let namespace_map = extract_namespace_map_for_test(&tree, source);
3292 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3293
3294 assert_eq!(contexts.len(), 1);
3295 assert_eq!(contexts[0].qualified_name, "demo::Service::process");
3296 assert_eq!(contexts[0].namespace_stack, vec!["demo"]);
3297 assert_eq!(contexts[0].class_stack, vec!["Service"]);
3298 }
3299
3300 #[test]
3301 fn test_extract_cpp_contexts_static_method() {
3302 let source = r"
3303 class Repository {
3304 public:
3305 static void save() {}
3306 };
3307 ";
3308 let tree = parse_cpp(source);
3309 let namespace_map = extract_namespace_map_for_test(&tree, source);
3310 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3311
3312 assert_eq!(contexts.len(), 1);
3313 assert_eq!(contexts[0].qualified_name, "Repository::save");
3314 assert!(contexts[0].is_static);
3315 }
3316
3317 #[test]
3318 fn test_extract_cpp_contexts_virtual_method() {
3319 let source = r"
3320 class Base {
3321 public:
3322 virtual void render() {}
3323 };
3324 ";
3325 let tree = parse_cpp(source);
3326 let namespace_map = extract_namespace_map_for_test(&tree, source);
3327 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3328
3329 assert_eq!(contexts.len(), 1);
3330 assert_eq!(contexts[0].qualified_name, "Base::render");
3331 assert!(contexts[0].is_virtual);
3332 }
3333
3334 #[test]
3335 fn test_extract_cpp_contexts_inline_function() {
3336 let source = r"
3337 inline void helper() {}
3338 ";
3339 let tree = parse_cpp(source);
3340 let namespace_map = extract_namespace_map_for_test(&tree, source);
3341 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3342
3343 assert_eq!(contexts.len(), 1);
3344 assert_eq!(contexts[0].qualified_name, "helper");
3345 assert!(contexts[0].is_inline);
3346 }
3347
3348 #[test]
3349 fn test_extract_cpp_contexts_out_of_line_definition() {
3350 let source = r"
3351 namespace demo {
3352 class Service {
3353 public:
3354 int process(int v);
3355 };
3356
3357 inline int Service::process(int v) {
3358 return v;
3359 }
3360 }
3361 ";
3362 let tree = parse_cpp(source);
3363 let namespace_map = extract_namespace_map_for_test(&tree, source);
3364 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3365
3366 assert_eq!(contexts.len(), 1);
3368 assert_eq!(contexts[0].qualified_name, "demo::Service::process");
3369 assert!(contexts[0].is_inline);
3370 }
3371
3372 #[test]
3373 fn test_extract_field_types_simple() {
3374 let source = r"
3375 class Service {
3376 public:
3377 Repository repo;
3378 };
3379 ";
3380 let tree = parse_cpp(source);
3381 let namespace_map = extract_namespace_map_for_test(&tree, source);
3382 let (field_types, _type_map) =
3383 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3384
3385 assert_eq!(field_types.len(), 1);
3387 assert_eq!(
3388 field_types.get(&("Service".to_string(), "repo".to_string())),
3389 Some(&"Repository".to_string())
3390 );
3391 }
3392
3393 #[test]
3394 fn test_extract_field_types_namespace() {
3395 let source = r"
3396 namespace demo {
3397 class Service {
3398 public:
3399 Repository repo;
3400 };
3401 }
3402 ";
3403 let tree = parse_cpp(source);
3404 let namespace_map = extract_namespace_map_for_test(&tree, source);
3405 let (field_types, _type_map) =
3406 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3407
3408 assert_eq!(field_types.len(), 1);
3410 assert_eq!(
3411 field_types.get(&("demo::Service".to_string(), "repo".to_string())),
3412 Some(&"Repository".to_string())
3413 );
3414 }
3415
3416 #[test]
3417 fn test_extract_field_types_no_collision() {
3418 let source = r"
3419 class ServiceA {
3420 public:
3421 Repository repo;
3422 };
3423
3424 class ServiceB {
3425 public:
3426 Repository repo;
3427 };
3428 ";
3429 let tree = parse_cpp(source);
3430 let namespace_map = extract_namespace_map_for_test(&tree, source);
3431 let (field_types, _type_map) =
3432 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3433
3434 assert_eq!(field_types.len(), 2);
3436 assert_eq!(
3437 field_types.get(&("ServiceA".to_string(), "repo".to_string())),
3438 Some(&"Repository".to_string())
3439 );
3440 assert_eq!(
3441 field_types.get(&("ServiceB".to_string(), "repo".to_string())),
3442 Some(&"Repository".to_string())
3443 );
3444 }
3445
3446 #[test]
3447 fn test_extract_using_declaration() {
3448 let source = r"
3449 using std::vector;
3450
3451 class Service {
3452 public:
3453 vector data;
3454 };
3455 ";
3456 let tree = parse_cpp(source);
3457 let namespace_map = extract_namespace_map_for_test(&tree, source);
3458 let (field_types, type_map) =
3459 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3460
3461 assert_eq!(field_types.len(), 1);
3463 assert_eq!(
3464 field_types.get(&("Service".to_string(), "data".to_string())),
3465 Some(&"std::vector".to_string()),
3466 "Field type should resolve 'vector' to 'std::vector' via using declaration"
3467 );
3468
3469 assert_eq!(
3471 type_map.get(&(String::new(), "vector".to_string())),
3472 Some(&"std::vector".to_string()),
3473 "Using declaration should map 'vector' to 'std::vector' in type_map"
3474 );
3475 }
3476
3477 #[test]
3478 fn test_extract_field_types_pointer() {
3479 let source = r"
3480 class Service {
3481 public:
3482 Repository* repo;
3483 };
3484 ";
3485 let tree = parse_cpp(source);
3486 let namespace_map = extract_namespace_map_for_test(&tree, source);
3487 let (field_types, _type_map) =
3488 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3489
3490 assert_eq!(field_types.len(), 1);
3492 assert_eq!(
3493 field_types.get(&("Service".to_string(), "repo".to_string())),
3494 Some(&"Repository".to_string())
3495 );
3496 }
3497
3498 #[test]
3499 fn test_extract_field_types_multiple_declarators() {
3500 let source = r"
3501 class Service {
3502 public:
3503 Repository repo_a, repo_b, repo_c;
3504 };
3505 ";
3506 let tree = parse_cpp(source);
3507 let namespace_map = extract_namespace_map_for_test(&tree, source);
3508 let (field_types, _type_map) =
3509 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3510
3511 assert_eq!(field_types.len(), 3);
3513 assert_eq!(
3514 field_types.get(&("Service".to_string(), "repo_a".to_string())),
3515 Some(&"Repository".to_string())
3516 );
3517 assert_eq!(
3518 field_types.get(&("Service".to_string(), "repo_b".to_string())),
3519 Some(&"Repository".to_string())
3520 );
3521 assert_eq!(
3522 field_types.get(&("Service".to_string(), "repo_c".to_string())),
3523 Some(&"Repository".to_string())
3524 );
3525 }
3526
3527 #[test]
3528 fn test_extract_field_types_nested_struct_with_parent_field() {
3529 let source = r"
3532 namespace demo {
3533 struct Outer {
3534 int outer_field;
3535 struct Inner {
3536 int inner_field;
3537 };
3538 Inner nested_instance;
3539 };
3540 }
3541 ";
3542 let tree = parse_cpp(source);
3543 let namespace_map = extract_namespace_map_for_test(&tree, source);
3544 let (field_types, _type_map) =
3545 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3546
3547 assert!(
3550 field_types.len() >= 2,
3551 "Expected at least outer_field and nested_instance"
3552 );
3553
3554 assert_eq!(
3556 field_types.get(&("demo::Outer".to_string(), "outer_field".to_string())),
3557 Some(&"int".to_string())
3558 );
3559
3560 assert_eq!(
3562 field_types.get(&("demo::Outer".to_string(), "nested_instance".to_string())),
3563 Some(&"Inner".to_string())
3564 );
3565
3566 if field_types.contains_key(&("demo::Outer::Inner".to_string(), "inner_field".to_string()))
3568 {
3569 assert_eq!(
3571 field_types.get(&("demo::Outer::Inner".to_string(), "inner_field".to_string())),
3572 Some(&"int".to_string()),
3573 "Inner class fields must use parent-qualified FQN 'demo::Outer::Inner'"
3574 );
3575 }
3576 }
3577
3578 use sqry_core::graph::unified::build::staging::StagingOp;
3595 use sqry_core::graph::unified::edge::kind::{EdgeKind, TypeOfContext};
3596
3597 fn cpp_find_added_node<'a>(
3599 staging: &'a StagingGraph,
3600 canonical_name: &str,
3601 ) -> Option<&'a sqry_core::graph::unified::storage::arena::NodeEntry> {
3602 staging.operations().iter().find_map(|op| {
3603 if let StagingOp::AddNode { entry, .. } = op
3604 && staging.resolve_node_canonical_name(entry) == Some(canonical_name)
3605 {
3606 Some(entry)
3607 } else {
3608 None
3609 }
3610 })
3611 }
3612
3613 fn cpp_find_added_node_id(
3615 staging: &StagingGraph,
3616 canonical_name: &str,
3617 kind: NodeKind,
3618 ) -> Option<sqry_core::graph::unified::NodeId> {
3619 staging.operations().iter().find_map(|op| match op {
3620 StagingOp::AddNode {
3621 entry,
3622 expected_id: Some(id),
3623 } if entry.kind == kind
3624 && staging.resolve_node_canonical_name(entry) == Some(canonical_name) =>
3625 {
3626 Some(*id)
3627 }
3628 _ => None,
3629 })
3630 }
3631
3632 fn build_cpp(source: &str) -> StagingGraph {
3634 let tree = parse_cpp(source);
3635 let mut staging = StagingGraph::new();
3636 let builder = CppGraphBuilder::new();
3637 builder
3638 .build_graph(
3639 &tree,
3640 source.as_bytes(),
3641 Path::new("test.cpp"),
3642 &mut staging,
3643 )
3644 .expect("build_graph must succeed for the test fixture");
3645 staging
3646 }
3647
3648 #[test]
3654 fn test_struct_field_emits_property_with_field_context() {
3655 let source = "struct Point { int x; int y; };";
3656 let staging = build_cpp(source);
3657
3658 assert_has_node_with_kind_exact(&staging, "Point.x", NodeKind::Property);
3660 assert_has_node_with_kind_exact(&staging, "Point.y", NodeKind::Property);
3661
3662 let entry =
3663 cpp_find_added_node(&staging, "Point.x").expect("Point.x should be staged as a node");
3664 assert_eq!(entry.kind, NodeKind::Property, "x must be Property");
3665 assert!(!entry.is_static, "instance field is_static must be false");
3666 let vis = staging.resolve_local_string(entry.visibility.expect("visibility id"));
3667 assert_eq!(
3668 vis,
3669 Some("public"),
3670 "struct default visibility must be 'public'"
3671 );
3672 assert!(entry.end_line > 0, "field end_line must be set (got 0)");
3678 assert!(
3679 entry.end_line > entry.start_line
3680 || (entry.end_line == entry.start_line && entry.end_column > entry.start_column),
3681 "field span must be non-empty: [{}:{}..{}:{}]",
3682 entry.start_line,
3683 entry.start_column,
3684 entry.end_line,
3685 entry.end_column,
3686 );
3687
3688 let x_id = cpp_find_added_node_id(&staging, "Point.x", NodeKind::Property)
3690 .expect("Point.x Property NodeId");
3691 let edge = staging.operations().iter().find_map(|op| {
3692 if let StagingOp::AddEdge {
3693 source: src,
3694 kind: EdgeKind::TypeOf { context, name, .. },
3695 ..
3696 } = op
3697 && *src == x_id
3698 {
3699 Some((*context, *name))
3700 } else {
3701 None
3702 }
3703 });
3704 let (ctx, name) = edge.expect("TypeOf edge from Point.x should be staged");
3705 assert_eq!(
3706 ctx,
3707 Some(TypeOfContext::Field),
3708 "TypeOf edge context must be Field"
3709 );
3710 let resolved_name = name.and_then(|sid| staging.resolve_local_string(sid));
3711 assert_eq!(
3712 resolved_name,
3713 Some("x"),
3714 "TypeOf edge name must be the bare field name 'x'"
3715 );
3716
3717 let stale_variable = staging.nodes().any(|n| {
3719 n.entry.kind == NodeKind::Variable
3720 && matches!(
3721 staging.resolve_node_name(n.entry),
3722 Some("Point.x" | "Point.y" | "Point::x" | "Point::y")
3723 )
3724 });
3725 assert!(
3726 !stale_variable,
3727 "Point fields must not be emitted as NodeKind::Variable"
3728 );
3729 }
3730
3731 #[test]
3733 fn test_class_field_default_visibility_is_private() {
3734 let source = "class Foo { int hidden; };";
3735 let staging = build_cpp(source);
3736
3737 let entry = cpp_find_added_node(&staging, "Foo.hidden")
3738 .expect("Foo.hidden should be staged as a node");
3739 assert_eq!(entry.kind, NodeKind::Property);
3740 let vis = staging.resolve_local_string(entry.visibility.expect("visibility id"));
3741 assert_eq!(
3742 vis,
3743 Some("private"),
3744 "class default visibility must be 'private'"
3745 );
3746 }
3747
3748 #[test]
3750 fn test_class_field_respects_explicit_access_specifier() {
3751 let source = "class Foo { public: int public_field; protected: int prot_field; };";
3752 let staging = build_cpp(source);
3753
3754 let pub_entry = cpp_find_added_node(&staging, "Foo.public_field")
3755 .expect("Foo.public_field should be staged");
3756 assert_eq!(
3757 staging.resolve_local_string(pub_entry.visibility.expect("vis")),
3758 Some("public")
3759 );
3760
3761 let prot_entry = cpp_find_added_node(&staging, "Foo.prot_field")
3762 .expect("Foo.prot_field should be staged");
3763 assert_eq!(
3764 staging.resolve_local_string(prot_entry.visibility.expect("vis")),
3765 Some("protected")
3766 );
3767 }
3768
3769 #[test]
3772 fn test_const_field_emits_constant() {
3773 let source = "class Foo { const int kMax = 0; };";
3774 let staging = build_cpp(source);
3775
3776 assert_has_node_with_kind_exact(&staging, "Foo.kMax", NodeKind::Constant);
3777 let entry = cpp_find_added_node(&staging, "Foo.kMax").expect("Foo.kMax");
3778 assert_eq!(entry.kind, NodeKind::Constant);
3779 assert!(
3780 !entry.is_static,
3781 "const (non-static) field is_static must be false; only `static` keyword sets is_static"
3782 );
3783 }
3784
3785 #[test]
3789 fn test_constexpr_field_emits_constant() {
3790 let source = "class Foo { constexpr static int kAnswer = 42; };";
3791 let staging = build_cpp(source);
3792
3793 assert_has_node_with_kind_exact(&staging, "Foo.kAnswer", NodeKind::Constant);
3794 let entry = cpp_find_added_node(&staging, "Foo.kAnswer").expect("Foo.kAnswer");
3795 assert_eq!(entry.kind, NodeKind::Constant);
3796 assert!(
3797 entry.is_static,
3798 "static constexpr member must have is_static = true"
3799 );
3800 }
3801
3802 #[test]
3805 fn test_static_field_sets_is_static_true() {
3806 let source = "class Foo { static int counter; };";
3807 let staging = build_cpp(source);
3808
3809 let entry = cpp_find_added_node(&staging, "Foo.counter").expect("Foo.counter");
3810 assert_eq!(entry.kind, NodeKind::Property);
3811 assert!(entry.is_static, "static keyword must set is_static = true");
3812 }
3813
3814 #[test]
3817 fn test_bitfield_emits_property() {
3818 let source = "struct Flags { unsigned int low : 4; unsigned int high : 4; };";
3819 let staging = build_cpp(source);
3820
3821 assert_has_node_with_kind_exact(&staging, "Flags.low", NodeKind::Property);
3822 assert_has_node_with_kind_exact(&staging, "Flags.high", NodeKind::Property);
3823 }
3824
3825 #[test]
3831 fn test_anonymous_union_member_fields_emit_property() {
3832 let source = r"
3833class Variant {
3834public:
3835 int tag;
3836 union {
3837 int as_int;
3838 float as_float;
3839 };
3840};
3841";
3842 let staging = build_cpp(source);
3843
3844 assert_has_node_with_kind_exact(&staging, "Variant.tag", NodeKind::Property);
3846
3847 assert_has_node_with_kind_exact(&staging, "Variant.as_int", NodeKind::Property);
3850 assert_has_node_with_kind_exact(&staging, "Variant.as_float", NodeKind::Property);
3851
3852 let as_int = cpp_find_added_node(&staging, "Variant.as_int")
3855 .expect("Variant.as_int should be staged");
3856 let vis = staging.resolve_local_string(as_int.visibility.expect("visibility id"));
3857 assert_eq!(
3858 vis,
3859 Some("public"),
3860 "anonymous-union members must inherit OUTER access (`public:` here)"
3861 );
3862
3863 let bogus = staging.nodes().any(|n| {
3866 staging
3867 .resolve_node_name(n.entry)
3868 .is_some_and(|name| name.contains("::.") || name.starts_with("Variant::."))
3869 });
3870 assert!(
3871 !bogus,
3872 "anonymous union must not produce a synthetic qualifier"
3873 );
3874
3875 let stale_variable = staging.nodes().any(|n| {
3877 n.entry.kind == NodeKind::Variable
3878 && matches!(
3879 staging.resolve_node_name(n.entry),
3880 Some("Variant.tag" | "Variant.as_int" | "Variant.as_float")
3881 )
3882 });
3883 assert!(
3884 !stale_variable,
3885 "anonymous-union members + outer fields must not stay as Variable"
3886 );
3887 }
3888
3889 #[test]
3893 fn test_templated_class_field_emits_property() {
3894 let source = r"
3895template<class T>
3896struct Box {
3897 T value;
3898};
3899";
3900 let staging = build_cpp(source);
3901
3902 assert_has_node_with_kind_exact(&staging, "Box.value", NodeKind::Property);
3903 let entry = cpp_find_added_node(&staging, "Box.value").expect("Box.value");
3904 assert_eq!(entry.kind, NodeKind::Property);
3905 assert!(!entry.is_static);
3906 }
3907
3908 #[test]
3914 fn test_outer_class_field_with_nested_class_present() {
3915 let source = r"
3916class Outer {
3917public:
3918 int outer_value;
3919 class Inner {
3920 public:
3921 int x;
3922 };
3923};
3924";
3925 let staging = build_cpp(source);
3926
3927 assert_has_node_with_kind_exact(&staging, "Outer.outer_value", NodeKind::Property);
3929
3930 assert_has_node_with_kind_exact(&staging, "Outer::Inner.x", NodeKind::Property);
3934
3935 let legacy_hits: Vec<_> = staging
3938 .nodes()
3939 .filter(|n| staging.resolve_node_name(n.entry) == Some("Outer::outer_value"))
3940 .collect();
3941 assert!(
3942 legacy_hits.is_empty(),
3943 "legacy `Outer::outer_value` lookup must return 0 hits"
3944 );
3945
3946 for legacy in ["Inner.x", "Outer::Inner::x", "Outer.Inner.x"] {
3950 let hits: Vec<_> = staging
3951 .nodes()
3952 .filter(|n| staging.resolve_node_name(n.entry) == Some(legacy))
3953 .collect();
3954 assert!(
3955 hits.is_empty(),
3956 "nested-class field `{legacy}` must not appear; expected only `Outer::Inner.x`"
3957 );
3958 }
3959 }
3960
3961 #[test]
3965 fn test_outer_class_with_nested_struct_emits_inner_field() {
3966 let source = r"
3967class Outer {
3968private:
3969 struct Inner {
3970 int y;
3971 };
3972};
3973";
3974 let staging = build_cpp(source);
3975
3976 assert_has_node_with_kind_exact(&staging, "Outer::Inner.y", NodeKind::Property);
3977
3978 let entry = cpp_find_added_node(&staging, "Outer::Inner.y")
3979 .expect("Outer::Inner.y should be staged");
3980 let vis = staging.resolve_local_string(entry.visibility.expect("visibility id"));
3981 assert_eq!(
3982 vis,
3983 Some("public"),
3984 "nested struct field default visibility must be 'public' \
3985 regardless of OUTER access state"
3986 );
3987 }
3988
3989 #[test]
3997 fn test_legacy_double_colon_field_lookup_returns_zero() {
3998 let source = r"
3999class Foo {
4000public:
4001 int bar;
4002 static int baz;
4003 const int qux = 0;
4004};
4005struct Quux {
4006 int corge;
4007};
4008";
4009 let staging = build_cpp(source);
4010
4011 assert_has_node_with_kind_exact(&staging, "Foo.bar", NodeKind::Property);
4013 assert_has_node_with_kind_exact(&staging, "Foo.baz", NodeKind::Property);
4014 assert_has_node_with_kind_exact(&staging, "Foo.qux", NodeKind::Constant);
4015 assert_has_node_with_kind_exact(&staging, "Quux.corge", NodeKind::Property);
4016
4017 for legacy in ["Foo::bar", "Foo::baz", "Foo::qux", "Quux::corge"] {
4020 let hits: Vec<_> = staging
4021 .nodes()
4022 .filter(|n| staging.resolve_node_name(n.entry) == Some(legacy))
4023 .collect();
4024 assert!(
4025 hits.is_empty(),
4026 "legacy lookup for {legacy:?} must return 0 hits, got {} node(s) ({:?})",
4027 hits.len(),
4028 hits.iter()
4029 .map(|n| (n.entry.kind, staging.resolve_node_name(n.entry)))
4030 .collect::<Vec<_>>()
4031 );
4032 }
4033 }
4034
4035 #[test]
4038 fn test_namespaced_class_field_qualified_name() {
4039 let source = r"
4040namespace demo {
4041 class Service {
4042 public:
4043 int counter;
4044 };
4045}
4046";
4047 let staging = build_cpp(source);
4048
4049 assert_has_node_with_kind_exact(&staging, "demo::Service.counter", NodeKind::Property);
4050 }
4051}
4052
4053#[cfg(test)]
4054mod shape_tests {
4055 use super::{cf_bucket_for_cpp_kind, cpp_shape_mapping};
4056 use sqry_core::graph::unified::build::shape::{
4057 CfBucket, ShapeBudget, ShapeMapping, compute_shape_descriptor,
4058 };
4059
4060 const SAMPLE: &str = include_str!(concat!(
4061 env!("CARGO_MANIFEST_DIR"),
4062 "/../test-fixtures/shape/reference/sample.cpp"
4063 ));
4064
4065 fn parse(src: &str) -> tree_sitter::Tree {
4066 let lang: tree_sitter::Language = tree_sitter_cpp::LANGUAGE.into();
4067 let mut p = tree_sitter::Parser::new();
4068 p.set_language(&lang).expect("load cpp grammar");
4069 p.parse(src, None).expect("parse")
4070 }
4071
4072 fn function_named<'t>(tree: &'t tree_sitter::Tree, name: &str) -> tree_sitter::Node<'t> {
4074 let root = tree.root_node();
4075 let mut stack = vec![root];
4076 while let Some(node) = stack.pop() {
4077 if node.kind() == "function_definition"
4078 && function_def_name(node).as_deref() == Some(name)
4079 {
4080 return node;
4081 }
4082 let mut c = node.walk();
4083 for ch in node.children(&mut c) {
4084 stack.push(ch);
4085 }
4086 }
4087 panic!("no function_definition named {name}");
4088 }
4089
4090 fn function_def_name(node: tree_sitter::Node) -> Option<String> {
4092 let mut decl = node.child_by_field_name("declarator")?;
4093 for _ in 0..8 {
4094 if decl.kind() == "function_declarator" {
4095 let inner = decl.child_by_field_name("declarator")?;
4096 return inner.utf8_text(SAMPLE.as_bytes()).ok().map(str::to_owned);
4097 }
4098 decl = decl.child_by_field_name("declarator")?;
4099 }
4100 None
4101 }
4102
4103 #[test]
4104 fn cf_table_is_non_empty() {
4105 let mapping = cpp_shape_mapping();
4106 let lang: tree_sitter::Language = tree_sitter_cpp::LANGUAGE.into();
4107 let mut covered = 0;
4108 for id in 0..lang.node_kind_count() {
4109 if mapping.cf_bucket(id as u16).is_some() {
4110 covered += 1;
4111 }
4112 }
4113 assert!(
4114 covered >= 10,
4115 "expected many C++ CF kinds mapped, got {covered}"
4116 );
4117 }
4118
4119 #[test]
4120 fn histogram_covers_real_control_flow() {
4121 let tree = parse(SAMPLE);
4122 let func = function_named(&tree, "classify");
4123 let d = compute_shape_descriptor(
4124 func,
4125 SAMPLE.as_bytes(),
4126 cpp_shape_mapping(),
4127 &ShapeBudget::default(),
4128 );
4129 assert!(!d.is_unhashable());
4130 for bucket in [
4131 CfBucket::Branch,
4132 CfBucket::Loop,
4133 CfBucket::Match,
4134 CfBucket::Try,
4135 CfBucket::Catch,
4136 CfBucket::Throw,
4137 CfBucket::Return,
4138 CfBucket::BreakContinue,
4139 CfBucket::Call,
4140 CfBucket::Assign,
4141 ] {
4142 assert!(
4143 d.cf_histogram[bucket.index()] >= 1,
4144 "classify must exercise {bucket:?}"
4145 );
4146 }
4147 }
4148
4149 #[test]
4150 fn lambda_body_covers_closure() {
4151 let tree = parse(SAMPLE);
4152 let func = function_named(&tree, "adder");
4153 let d = compute_shape_descriptor(
4154 func,
4155 SAMPLE.as_bytes(),
4156 cpp_shape_mapping(),
4157 &ShapeBudget::default(),
4158 );
4159 assert!(
4160 d.cf_histogram[CfBucket::Closure.index()] >= 1,
4161 "lambda closure"
4162 );
4163 }
4164
4165 #[test]
4166 fn signature_shape_reads_arity_defaults_return() {
4167 let tree = parse(SAMPLE);
4168 let func = function_named(&tree, "classify");
4169 let mapping = cpp_shape_mapping();
4170 let shape = mapping.signature_shape(func, SAMPLE.as_bytes());
4171 assert_eq!(shape.arity_positional, 2);
4173 assert!(shape.has_defaults, "threshold = 0");
4174 assert!(shape.has_return_annotation, "int return type");
4175 }
4176
4177 #[test]
4180 fn ac6_cpp_classify_histogram_well_formed() {
4181 let tree = parse(SAMPLE);
4182 let func = function_named(&tree, "classify");
4183 let d = compute_shape_descriptor(
4184 func,
4185 SAMPLE.as_bytes(),
4186 cpp_shape_mapping(),
4187 &ShapeBudget::default(),
4188 );
4189 assert_eq!(d.cf_histogram[CfBucket::Branch.index()], 2, "two if levels");
4191 assert!(d.cf_histogram[CfBucket::Loop.index()] >= 2, "for + while");
4192 assert!(d.cf_histogram[CfBucket::Return.index()] >= 2, "two returns");
4193 }
4194
4195 #[test]
4196 fn unknown_kind_maps_to_none() {
4197 assert!(cf_bucket_for_cpp_kind("translation_unit").is_none());
4198 assert!(cf_bucket_for_cpp_kind("identifier").is_none());
4199 }
4200}