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 helper.mark_definition(var_id);
1638
1639 let type_id = helper.add_type(&base_type, None);
1641
1642 helper.add_typeof_edge(var_id, type_id);
1644 helper.add_reference_edge(var_id, type_id);
1645 }
1646 }
1647
1648 Ok(())
1649}
1650
1651fn extract_declarator_name(node: Node, content: &[u8]) -> Option<String> {
1653 match node.kind() {
1654 "identifier" => {
1655 if let Ok(name) = node.utf8_text(content) {
1656 Some(name.to_string())
1657 } else {
1658 None
1659 }
1660 }
1661 "pointer_declarator" | "reference_declarator" | "array_declarator" => {
1662 if let Some(inner) = node.child_by_field_name("declarator") {
1664 extract_declarator_name(inner, content)
1665 } else {
1666 let mut cursor = node.walk();
1668 for child in node.children(&mut cursor) {
1669 if child.kind() == "identifier"
1670 && let Ok(name) = child.utf8_text(content)
1671 {
1672 return Some(name.to_string());
1673 }
1674 }
1675 None
1676 }
1677 }
1678 "init_declarator" => {
1679 if let Some(inner) = node.child_by_field_name("declarator") {
1681 extract_declarator_name(inner, content)
1682 } else {
1683 None
1684 }
1685 }
1686 "field_declarator" => {
1687 if let Some(inner) = node.child_by_field_name("declarator") {
1689 extract_declarator_name(inner, content)
1690 } else {
1691 if let Ok(name) = node.utf8_text(content) {
1693 Some(name.to_string())
1694 } else {
1695 None
1696 }
1697 }
1698 }
1699 _ => None,
1700 }
1701}
1702
1703#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
1705fn walk_class_body(
1706 body_node: Node,
1707 content: &[u8],
1708 class_qualified_name: &str,
1709 is_struct: bool,
1710 ast_graph: &ASTGraph,
1711 helper: &mut GraphBuildHelper,
1712 seen_includes: &mut HashSet<String>,
1713 namespace_stack: &mut Vec<String>,
1714 class_stack: &mut Vec<String>,
1715 ffi_registry: &FfiRegistry,
1716 pure_virtual_registry: &PureVirtualRegistry,
1717 budget: &mut BuildBudget,
1718) -> GraphResult<()> {
1719 let mut current_visibility = if is_struct { "public" } else { "private" };
1721
1722 let mut cursor = body_node.walk();
1723 for child in body_node.children(&mut cursor) {
1724 budget.checkpoint("cpp:walk_class_body")?;
1725 match child.kind() {
1726 "access_specifier" => {
1727 if let Ok(text) = child.utf8_text(content) {
1729 let spec = text.trim().trim_end_matches(':').trim();
1730 current_visibility = spec;
1731 }
1732 }
1733 "field_declaration" => {
1734 let mut handled_nested = false;
1750 let mut inner_cursor = child.walk();
1751 for inner in child.children(&mut inner_cursor) {
1752 let kind = inner.kind();
1753 if !matches!(
1754 kind,
1755 "class_specifier"
1756 | "struct_specifier"
1757 | "union_specifier"
1758 | "enum_specifier"
1759 ) {
1760 continue;
1761 }
1762
1763 let is_struct_or_union = matches!(kind, "struct_specifier" | "union_specifier");
1764
1765 if let Some(name_node) = inner.child_by_field_name("name") {
1766 if let Ok(inner_name) = name_node.utf8_text(content) {
1778 let inner_name = inner_name.trim();
1779 let nested_qualified = format!("{class_qualified_name}::{inner_name}");
1780 let nested_span = span_from_node(inner);
1781
1782 if kind == "enum_specifier" {
1786 helper.add_enum_with_visibility(
1790 &nested_qualified,
1791 Some(nested_span),
1792 Some(current_visibility),
1793 );
1794 } else {
1795 let nested_id = if is_struct_or_union {
1796 helper.add_struct_with_visibility(
1797 &nested_qualified,
1798 Some(nested_span),
1799 Some(current_visibility),
1800 )
1801 } else {
1802 helper.add_class_with_visibility(
1803 &nested_qualified,
1804 Some(nested_span),
1805 Some(current_visibility),
1806 )
1807 };
1808 build_inheritance_and_implements_edges(
1809 inner,
1810 content,
1811 &nested_qualified,
1812 nested_id,
1813 helper,
1814 namespace_stack,
1815 pure_virtual_registry,
1816 )?;
1817 }
1818
1819 if matches!(
1824 kind,
1825 "class_specifier" | "struct_specifier" | "union_specifier"
1826 ) && let Some(body) = inner.child_by_field_name("body")
1827 {
1828 walk_class_body(
1829 body,
1830 content,
1831 &nested_qualified,
1832 is_struct_or_union,
1833 ast_graph,
1834 helper,
1835 seen_includes,
1836 namespace_stack,
1837 class_stack,
1838 ffi_registry,
1839 pure_virtual_registry,
1840 budget,
1841 )?;
1842 }
1843 handled_nested = true;
1844 }
1845 } else if let Some(body) = inner.child_by_field_name("body") {
1846 let mut anon_cursor = body.walk();
1851 for anon_child in body.children(&mut anon_cursor) {
1852 if anon_child.kind() == "field_declaration" {
1853 process_field_declaration(
1854 anon_child,
1855 content,
1856 class_qualified_name,
1857 current_visibility,
1858 helper,
1859 )?;
1860 }
1861 }
1862 handled_nested = true;
1863 }
1864 }
1865
1866 let _ = handled_nested;
1879 process_field_declaration(
1880 child,
1881 content,
1882 class_qualified_name,
1883 current_visibility,
1884 helper,
1885 )?;
1886 }
1887 "function_definition" => {
1888 if let Some(context) = ast_graph.context_for_start(child.start_byte()) {
1891 let span = span_from_node(child);
1892 helper.add_method_with_signature(
1893 &context.qualified_name,
1894 Some(span),
1895 false, context.is_static,
1897 Some(current_visibility),
1898 context.return_type.as_deref(),
1899 );
1900 }
1901 walk_tree_for_graph(
1903 child,
1904 content,
1905 ast_graph,
1906 helper,
1907 seen_includes,
1908 namespace_stack,
1909 class_stack,
1910 ffi_registry,
1911 pure_virtual_registry,
1912 budget,
1913 )?;
1914 }
1915 _ => {
1916 walk_tree_for_graph(
1918 child,
1919 content,
1920 ast_graph,
1921 helper,
1922 seen_includes,
1923 namespace_stack,
1924 class_stack,
1925 ffi_registry,
1926 pure_virtual_registry,
1927 budget,
1928 )?;
1929 }
1930 }
1931 }
1932
1933 Ok(())
1934}
1935
1936#[allow(clippy::too_many_arguments)]
1938#[allow(clippy::too_many_lines)] fn walk_tree_for_graph(
1940 node: Node,
1941 content: &[u8],
1942 ast_graph: &ASTGraph,
1943 helper: &mut GraphBuildHelper,
1944 seen_includes: &mut HashSet<String>,
1945 namespace_stack: &mut Vec<String>,
1946 class_stack: &mut Vec<String>,
1947 ffi_registry: &FfiRegistry,
1948 pure_virtual_registry: &PureVirtualRegistry,
1949 budget: &mut BuildBudget,
1950) -> GraphResult<()> {
1951 budget.checkpoint("cpp:walk_tree_for_graph")?;
1952 match node.kind() {
1953 "preproc_include" => {
1954 build_import_edge(node, content, helper, seen_includes)?;
1956 }
1957 "linkage_specification" => {
1958 build_ffi_block_for_staging(node, content, helper, namespace_stack);
1960 }
1961 "namespace_definition" => {
1962 if let Some(name_node) = node.child_by_field_name("name")
1964 && let Ok(ns_name) = name_node.utf8_text(content)
1965 {
1966 namespace_stack.push(ns_name.trim().to_string());
1967
1968 let mut cursor = node.walk();
1970 for child in node.children(&mut cursor) {
1971 walk_tree_for_graph(
1972 child,
1973 content,
1974 ast_graph,
1975 helper,
1976 seen_includes,
1977 namespace_stack,
1978 class_stack,
1979 ffi_registry,
1980 pure_virtual_registry,
1981 budget,
1982 )?;
1983 }
1984
1985 namespace_stack.pop();
1986 return Ok(());
1987 }
1988 }
1989 "class_specifier" | "struct_specifier" | "union_specifier" => {
1990 if let Some(name_node) = node.child_by_field_name("name")
1992 && let Ok(class_name) = name_node.utf8_text(content)
1993 {
1994 let class_name = class_name.trim();
1995 let span = span_from_node(node);
1996 let is_struct = matches!(node.kind(), "struct_specifier" | "union_specifier");
1999
2000 let qualified_class =
2002 build_qualified_name(namespace_stack, class_stack, class_name);
2003
2004 let visibility = "public";
2006 let class_id = if is_struct {
2007 helper.add_struct_with_visibility(
2008 &qualified_class,
2009 Some(span),
2010 Some(visibility),
2011 )
2012 } else {
2013 helper.add_class_with_visibility(&qualified_class, Some(span), Some(visibility))
2014 };
2015
2016 build_inheritance_and_implements_edges(
2019 node,
2020 content,
2021 &qualified_class,
2022 class_id,
2023 helper,
2024 namespace_stack,
2025 pure_virtual_registry,
2026 )?;
2027
2028 if class_stack.is_empty() {
2031 let module_id = helper.add_module(FILE_MODULE_NAME, None);
2032 helper.add_export_edge(module_id, class_id);
2033 }
2034
2035 class_stack.push(class_name.to_string());
2037
2038 if let Some(body) = node.child_by_field_name("body") {
2041 walk_class_body(
2042 body,
2043 content,
2044 &qualified_class,
2045 is_struct,
2046 ast_graph,
2047 helper,
2048 seen_includes,
2049 namespace_stack,
2050 class_stack,
2051 ffi_registry,
2052 pure_virtual_registry,
2053 budget,
2054 )?;
2055 }
2056
2057 class_stack.pop();
2058 return Ok(());
2059 }
2060 }
2061 "enum_specifier" => {
2062 if let Some(name_node) = node.child_by_field_name("name")
2063 && let Ok(enum_name) = name_node.utf8_text(content)
2064 {
2065 let enum_name = enum_name.trim();
2066 let span = span_from_node(node);
2067 let qualified_enum = build_qualified_name(namespace_stack, class_stack, enum_name);
2068 let enum_id = helper.add_enum(&qualified_enum, Some(span));
2069
2070 if class_stack.is_empty() {
2071 let module_id = helper.add_module(FILE_MODULE_NAME, None);
2072 helper.add_export_edge(module_id, enum_id);
2073 }
2074 }
2075 }
2076 "function_definition" => {
2077 if !class_stack.is_empty() {
2081 let mut cursor = node.walk();
2084 for child in node.children(&mut cursor) {
2085 walk_tree_for_graph(
2086 child,
2087 content,
2088 ast_graph,
2089 helper,
2090 seen_includes,
2091 namespace_stack,
2092 class_stack,
2093 ffi_registry,
2094 pure_virtual_registry,
2095 budget,
2096 )?;
2097 }
2098 return Ok(());
2099 }
2100
2101 if let Some(context) = ast_graph.context_for_start(node.start_byte()) {
2103 let span = span_from_node(node);
2104
2105 if context.class_stack.is_empty() {
2107 let visibility = if context.is_static {
2110 "private"
2111 } else {
2112 "public"
2113 };
2114 let fn_id = helper.add_function_with_signature(
2115 &context.qualified_name,
2116 Some(span),
2117 false, false, Some(visibility),
2120 context.return_type.as_deref(),
2121 );
2122
2123 if !context.is_static {
2125 let module_id = helper.add_module(FILE_MODULE_NAME, None);
2126 helper.add_export_edge(module_id, fn_id);
2127 }
2128 } else {
2129 helper.add_method_with_signature(
2134 &context.qualified_name,
2135 Some(span),
2136 false, context.is_static,
2138 Some("public"), context.return_type.as_deref(),
2140 );
2141 }
2142 }
2143 }
2144 "call_expression" => {
2145 if let Ok(Some((caller_qname, callee_qname, argument_count, span))) =
2147 build_call_for_staging(ast_graph, node, content)
2148 {
2149 let caller_function_id =
2151 helper.ensure_callee(&caller_qname, span, CalleeKindHint::Function);
2152 let argument_count = u8::try_from(argument_count).unwrap_or(u8::MAX);
2153
2154 let is_unqualified = !callee_qname.contains("::");
2157 if is_unqualified {
2158 if let Some((ffi_qualified, ffi_convention)) = ffi_registry.get(&callee_qname) {
2159 let ffi_target_id =
2161 helper.ensure_callee(ffi_qualified, span, CalleeKindHint::Function);
2162 helper.add_ffi_edge(caller_function_id, ffi_target_id, *ffi_convention);
2163 } else {
2164 let target_function_id =
2166 helper.ensure_callee(&callee_qname, span, CalleeKindHint::Function);
2167 helper.add_call_edge_full_with_span(
2168 caller_function_id,
2169 target_function_id,
2170 argument_count,
2171 false,
2172 vec![span],
2173 );
2174 }
2175 } else {
2176 let target_function_id =
2178 helper.ensure_callee(&callee_qname, span, CalleeKindHint::Function);
2179 helper.add_call_edge_full_with_span(
2180 caller_function_id,
2181 target_function_id,
2182 argument_count,
2183 false,
2184 vec![span],
2185 );
2186 }
2187 }
2188 }
2189 "declaration" => {
2190 if class_stack.is_empty() {
2193 process_global_variable_declaration(node, content, namespace_stack, helper)?;
2194 }
2195 }
2196 _ => {}
2197 }
2198
2199 let mut cursor = node.walk();
2201 for child in node.children(&mut cursor) {
2202 walk_tree_for_graph(
2203 child,
2204 content,
2205 ast_graph,
2206 helper,
2207 seen_includes,
2208 namespace_stack,
2209 class_stack,
2210 ffi_registry,
2211 pure_virtual_registry,
2212 budget,
2213 )?;
2214 }
2215
2216 Ok(())
2217}
2218
2219fn build_call_for_staging(
2221 ast_graph: &ASTGraph,
2222 call_node: Node<'_>,
2223 content: &[u8],
2224) -> GraphResult<Option<(String, String, usize, Span)>> {
2225 let call_context = ast_graph.find_enclosing(call_node.start_byte());
2227 let caller_qualified_name = if let Some(ctx) = call_context {
2228 ctx.qualified_name.clone()
2229 } else {
2230 return Ok(None);
2232 };
2233
2234 let Some(function_node) = call_node.child_by_field_name("function") else {
2235 return Ok(None);
2236 };
2237
2238 let callee_text = function_node
2239 .utf8_text(content)
2240 .map_err(|_| GraphBuilderError::ParseError {
2241 span: span_from_node(call_node),
2242 reason: "failed to read call expression".to_string(),
2243 })?
2244 .trim();
2245
2246 if callee_text.is_empty() {
2247 return Ok(None);
2248 }
2249
2250 let target_qualified_name = if let Some(ctx) = call_context {
2252 resolve_callee_name(callee_text, ctx, ast_graph)
2253 } else {
2254 callee_text.to_string()
2255 };
2256
2257 let span = span_from_node(call_node);
2258 let argument_count = count_arguments(call_node);
2259
2260 Ok(Some((
2261 caller_qualified_name,
2262 target_qualified_name,
2263 argument_count,
2264 span,
2265 )))
2266}
2267
2268fn build_import_edge(
2275 include_node: Node<'_>,
2276 content: &[u8],
2277 helper: &mut GraphBuildHelper,
2278 seen_includes: &mut HashSet<String>,
2279) -> GraphResult<()> {
2280 let path_node = include_node.child_by_field_name("path").or_else(|| {
2282 let mut cursor = include_node.walk();
2284 include_node.children(&mut cursor).find(|child| {
2285 matches!(
2286 child.kind(),
2287 "system_lib_string" | "string_literal" | "string_content"
2288 )
2289 })
2290 });
2291
2292 let Some(path_node) = path_node else {
2293 return Ok(());
2294 };
2295
2296 let include_path = path_node
2297 .utf8_text(content)
2298 .map_err(|_| GraphBuilderError::ParseError {
2299 span: span_from_node(include_node),
2300 reason: "failed to read include path".to_string(),
2301 })?
2302 .trim();
2303
2304 if include_path.is_empty() {
2305 return Ok(());
2306 }
2307
2308 let is_system_include = include_path.starts_with('<') && include_path.ends_with('>');
2310 let cleaned_path = if is_system_include {
2311 include_path.trim_start_matches('<').trim_end_matches('>')
2313 } else {
2314 include_path.trim_start_matches('"').trim_end_matches('"')
2316 };
2317
2318 if cleaned_path.is_empty() {
2319 return Ok(());
2320 }
2321
2322 if !seen_includes.insert(cleaned_path.to_string()) {
2324 return Ok(()); }
2326
2327 let file_module_id = helper.add_module("<file>", None);
2329
2330 let span = span_from_node(include_node);
2332 let import_id = helper.add_import(cleaned_path, Some(span));
2333
2334 helper.add_import_edge(file_module_id, import_id);
2337
2338 Ok(())
2339}
2340
2341fn collect_ffi_declarations(
2352 node: Node<'_>,
2353 content: &[u8],
2354 ffi_registry: &mut FfiRegistry,
2355 budget: &mut BuildBudget,
2356) -> GraphResult<()> {
2357 budget.checkpoint("cpp:collect_ffi_declarations")?;
2358 if node.kind() == "linkage_specification" {
2359 let abi = extract_ffi_abi(node, content);
2361 let convention = abi_to_convention(&abi);
2362
2363 if let Some(body_node) = node.child_by_field_name("body") {
2365 collect_ffi_from_body(body_node, content, &abi, convention, ffi_registry);
2366 }
2367 }
2368
2369 let mut cursor = node.walk();
2371 for child in node.children(&mut cursor) {
2372 collect_ffi_declarations(child, content, ffi_registry, budget)?;
2373 }
2374
2375 Ok(())
2376}
2377
2378fn collect_ffi_from_body(
2380 body_node: Node<'_>,
2381 content: &[u8],
2382 abi: &str,
2383 convention: FfiConvention,
2384 ffi_registry: &mut FfiRegistry,
2385) {
2386 match body_node.kind() {
2387 "declaration_list" => {
2388 let mut cursor = body_node.walk();
2390 for decl in body_node.children(&mut cursor) {
2391 if decl.kind() == "declaration"
2392 && let Some(fn_name) = extract_ffi_function_name(decl, content)
2393 {
2394 let qualified = format!("extern::{abi}::{fn_name}");
2395 ffi_registry.insert(fn_name, (qualified, convention));
2396 }
2397 }
2398 }
2399 "declaration" => {
2400 if let Some(fn_name) = extract_ffi_function_name(body_node, content) {
2402 let qualified = format!("extern::{abi}::{fn_name}");
2403 ffi_registry.insert(fn_name, (qualified, convention));
2404 }
2405 }
2406 _ => {}
2407 }
2408}
2409
2410fn extract_ffi_function_name(decl_node: Node<'_>, content: &[u8]) -> Option<String> {
2412 if let Some(declarator_node) = decl_node.child_by_field_name("declarator") {
2414 return extract_function_name_from_declarator(declarator_node, content);
2415 }
2416 None
2417}
2418
2419fn extract_function_name_from_declarator(node: Node<'_>, content: &[u8]) -> Option<String> {
2421 match node.kind() {
2422 "function_declarator" => {
2423 if let Some(inner) = node.child_by_field_name("declarator") {
2425 return extract_function_name_from_declarator(inner, content);
2426 }
2427 }
2428 "identifier" => {
2429 if let Ok(name) = node.utf8_text(content) {
2431 let name = name.trim();
2432 if !name.is_empty() {
2433 return Some(name.to_string());
2434 }
2435 }
2436 }
2437 "pointer_declarator" | "reference_declarator" => {
2438 if let Some(inner) = node.child_by_field_name("declarator") {
2440 return extract_function_name_from_declarator(inner, content);
2441 }
2442 }
2443 "parenthesized_declarator" => {
2444 let mut cursor = node.walk();
2446 for child in node.children(&mut cursor) {
2447 if let Some(name) = extract_function_name_from_declarator(child, content) {
2448 return Some(name);
2449 }
2450 }
2451 }
2452 _ => {}
2453 }
2454 None
2455}
2456
2457fn extract_ffi_abi(node: Node<'_>, content: &[u8]) -> String {
2461 if let Some(value_node) = node.child_by_field_name("value")
2463 && value_node.kind() == "string_literal"
2464 {
2465 let mut cursor = value_node.walk();
2467 for child in value_node.children(&mut cursor) {
2468 if child.kind() == "string_content"
2469 && let Ok(text) = child.utf8_text(content)
2470 {
2471 let trimmed = text.trim();
2472 if !trimmed.is_empty() {
2473 return trimmed.to_string();
2474 }
2475 }
2476 }
2477 }
2478 "C".to_string()
2480}
2481
2482fn abi_to_convention(abi: &str) -> FfiConvention {
2484 match abi.to_lowercase().as_str() {
2485 "system" => FfiConvention::System,
2486 "stdcall" => FfiConvention::Stdcall,
2487 "fastcall" => FfiConvention::Fastcall,
2488 "cdecl" => FfiConvention::Cdecl,
2489 _ => FfiConvention::C, }
2491}
2492
2493fn build_ffi_block_for_staging(
2497 node: Node<'_>,
2498 content: &[u8],
2499 helper: &mut GraphBuildHelper,
2500 namespace_stack: &[String],
2501) {
2502 let abi = extract_ffi_abi(node, content);
2504
2505 if let Some(body_node) = node.child_by_field_name("body") {
2507 build_ffi_from_body(body_node, content, &abi, helper, namespace_stack);
2508 }
2509}
2510
2511fn build_ffi_from_body(
2513 body_node: Node<'_>,
2514 content: &[u8],
2515 abi: &str,
2516 helper: &mut GraphBuildHelper,
2517 namespace_stack: &[String],
2518) {
2519 match body_node.kind() {
2520 "declaration_list" => {
2521 let mut cursor = body_node.walk();
2523 for decl in body_node.children(&mut cursor) {
2524 if decl.kind() == "declaration"
2525 && let Some(fn_name) = extract_ffi_function_name(decl, content)
2526 {
2527 let span = span_from_node(decl);
2528 let qualified = if namespace_stack.is_empty() {
2530 format!("extern::{abi}::{fn_name}")
2531 } else {
2532 format!("{}::extern::{abi}::{fn_name}", namespace_stack.join("::"))
2533 };
2534 helper.add_function(
2536 &qualified,
2537 Some(span),
2538 false, true, );
2541 }
2542 }
2543 }
2544 "declaration" => {
2545 if let Some(fn_name) = extract_ffi_function_name(body_node, content) {
2547 let span = span_from_node(body_node);
2548 let qualified = if namespace_stack.is_empty() {
2549 format!("extern::{abi}::{fn_name}")
2550 } else {
2551 format!("{}::extern::{abi}::{fn_name}", namespace_stack.join("::"))
2552 };
2553 helper.add_function(&qualified, Some(span), false, true);
2554 }
2555 }
2556 _ => {}
2557 }
2558}
2559
2560fn collect_pure_virtual_interfaces(
2570 node: Node<'_>,
2571 content: &[u8],
2572 registry: &mut PureVirtualRegistry,
2573 budget: &mut BuildBudget,
2574) -> GraphResult<()> {
2575 budget.checkpoint("cpp:collect_pure_virtual_interfaces")?;
2576 if matches!(node.kind(), "class_specifier" | "struct_specifier")
2577 && let Some(name_node) = node.child_by_field_name("name")
2578 && let Ok(class_name) = name_node.utf8_text(content)
2579 {
2580 let class_name = class_name.trim();
2581 if !class_name.is_empty() && has_pure_virtual_methods(node, content) {
2582 registry.insert(class_name.to_string());
2583 }
2584 }
2585
2586 let mut cursor = node.walk();
2588 for child in node.children(&mut cursor) {
2589 collect_pure_virtual_interfaces(child, content, registry, budget)?;
2590 }
2591
2592 Ok(())
2593}
2594
2595fn has_pure_virtual_methods(class_node: Node<'_>, content: &[u8]) -> bool {
2599 if let Some(body) = class_node.child_by_field_name("body") {
2600 let mut cursor = body.walk();
2601 for child in body.children(&mut cursor) {
2602 if child.kind() == "field_declaration" && is_pure_virtual_declaration(child, content) {
2604 return true;
2605 }
2606 }
2607 }
2608 false
2609}
2610
2611fn is_pure_virtual_declaration(decl_node: Node<'_>, content: &[u8]) -> bool {
2613 let mut has_virtual = false;
2614 let mut has_pure_specifier = false;
2615
2616 let mut cursor = decl_node.walk();
2618 for child in decl_node.children(&mut cursor) {
2619 match child.kind() {
2620 "virtual" => {
2621 has_virtual = true;
2622 }
2623 "number_literal" => {
2624 if let Ok(text) = child.utf8_text(content)
2627 && text.trim() == "0"
2628 {
2629 has_pure_specifier = true;
2630 }
2631 }
2632 _ => {}
2633 }
2634 }
2635
2636 has_virtual && has_pure_specifier
2637}
2638
2639fn build_inheritance_and_implements_edges(
2645 class_node: Node<'_>,
2646 content: &[u8],
2647 _qualified_class_name: &str,
2648 child_id: sqry_core::graph::unified::node::NodeId,
2649 helper: &mut GraphBuildHelper,
2650 namespace_stack: &[String],
2651 pure_virtual_registry: &PureVirtualRegistry,
2652) -> GraphResult<()> {
2653 let mut cursor = class_node.walk();
2655 let base_clause = class_node
2656 .children(&mut cursor)
2657 .find(|child| child.kind() == "base_class_clause");
2658
2659 let Some(base_clause) = base_clause else {
2660 return Ok(()); };
2662
2663 let mut clause_cursor = base_clause.walk();
2665 for child in base_clause.children(&mut clause_cursor) {
2666 match child.kind() {
2667 "type_identifier" => {
2668 let base_name = child
2669 .utf8_text(content)
2670 .map_err(|_| GraphBuilderError::ParseError {
2671 span: span_from_node(child),
2672 reason: "failed to read base class name".to_string(),
2673 })?
2674 .trim();
2675
2676 if !base_name.is_empty() {
2677 let qualified_base = if namespace_stack.is_empty() {
2679 base_name.to_string()
2680 } else {
2681 format!("{}::{}", namespace_stack.join("::"), base_name)
2682 };
2683
2684 if pure_virtual_registry.contains(base_name) {
2686 let interface_id = helper.add_interface(&qualified_base, None);
2688 helper.add_implements_edge(child_id, interface_id);
2689 } else {
2690 let parent_id = helper.add_class(&qualified_base, None);
2692 helper.add_inherits_edge(child_id, parent_id);
2693 }
2694 }
2695 }
2696 "qualified_identifier" => {
2697 let base_name = child
2699 .utf8_text(content)
2700 .map_err(|_| GraphBuilderError::ParseError {
2701 span: span_from_node(child),
2702 reason: "failed to read base class name".to_string(),
2703 })?
2704 .trim();
2705
2706 if !base_name.is_empty() {
2707 let simple_name = base_name.rsplit("::").next().unwrap_or(base_name);
2709
2710 if pure_virtual_registry.contains(simple_name) {
2711 let interface_id = helper.add_interface(base_name, None);
2712 helper.add_implements_edge(child_id, interface_id);
2713 } else {
2714 let parent_id = helper.add_class(base_name, None);
2715 helper.add_inherits_edge(child_id, parent_id);
2716 }
2717 }
2718 }
2719 "template_type" => {
2720 if let Some(template_name_node) = child.child_by_field_name("name")
2722 && let Ok(base_name) = template_name_node.utf8_text(content)
2723 {
2724 let base_name = base_name.trim();
2725 if !base_name.is_empty() {
2726 let qualified_base =
2727 if base_name.contains("::") || namespace_stack.is_empty() {
2728 base_name.to_string()
2729 } else {
2730 format!("{}::{}", namespace_stack.join("::"), base_name)
2731 };
2732
2733 if pure_virtual_registry.contains(base_name) {
2736 let interface_id = helper.add_interface(&qualified_base, None);
2737 helper.add_implements_edge(child_id, interface_id);
2738 } else {
2739 let parent_id = helper.add_class(&qualified_base, None);
2740 helper.add_inherits_edge(child_id, parent_id);
2741 }
2742 }
2743 }
2744 }
2745 _ => {
2746 }
2748 }
2749 }
2750
2751 Ok(())
2752}
2753
2754fn span_from_node(node: Node<'_>) -> Span {
2755 let start = node.start_position();
2756 let end = node.end_position();
2757 Span::new(
2758 sqry_core::graph::node::Position::new(start.row, start.column),
2759 sqry_core::graph::node::Position::new(end.row, end.column),
2760 )
2761}
2762
2763fn count_arguments(node: Node<'_>) -> usize {
2764 node.child_by_field_name("arguments").map_or(0, |args| {
2765 let mut count = 0;
2766 let mut cursor = args.walk();
2767 for child in args.children(&mut cursor) {
2768 if !matches!(child.kind(), "(" | ")" | ",") {
2769 count += 1;
2770 }
2771 }
2772 count
2773 })
2774}
2775
2776#[cfg(test)]
2777mod tests {
2778 use super::*;
2779 use sqry_core::graph::unified::build::test_helpers::{
2780 assert_has_node, assert_has_node_with_kind, assert_has_node_with_kind_exact,
2781 collect_call_edges,
2782 };
2783 use sqry_core::graph::unified::node::NodeKind;
2784 use tree_sitter::Parser;
2785
2786 fn parse_cpp(source: &str) -> Tree {
2787 let mut parser = Parser::new();
2788 parser
2789 .set_language(&tree_sitter_cpp::LANGUAGE.into())
2790 .expect("Failed to set Cpp language");
2791 parser
2792 .parse(source.as_bytes(), None)
2793 .expect("Failed to parse Cpp source")
2794 }
2795
2796 fn test_budget() -> BuildBudget {
2797 BuildBudget::new(Path::new("test.cpp"))
2798 }
2799
2800 fn extract_namespace_map_for_test(
2801 tree: &Tree,
2802 source: &str,
2803 ) -> HashMap<std::ops::Range<usize>, String> {
2804 let mut budget = test_budget();
2805 extract_namespace_map(tree.root_node(), source.as_bytes(), &mut budget)
2806 .expect("namespace extraction should succeed in tests")
2807 }
2808
2809 fn extract_cpp_contexts_for_test(
2810 tree: &Tree,
2811 source: &str,
2812 namespace_map: &HashMap<std::ops::Range<usize>, String>,
2813 ) -> Vec<FunctionContext> {
2814 let mut budget = test_budget();
2815 extract_cpp_contexts(
2816 tree.root_node(),
2817 source.as_bytes(),
2818 namespace_map,
2819 &mut budget,
2820 )
2821 .expect("context extraction should succeed in tests")
2822 }
2823
2824 fn extract_field_and_type_info_for_test(
2825 tree: &Tree,
2826 source: &str,
2827 namespace_map: &HashMap<std::ops::Range<usize>, String>,
2828 ) -> (QualifiedNameMap, QualifiedNameMap) {
2829 let mut budget = test_budget();
2830 extract_field_and_type_info(
2831 tree.root_node(),
2832 source.as_bytes(),
2833 namespace_map,
2834 &mut budget,
2835 )
2836 .expect("field/type extraction should succeed in tests")
2837 }
2838
2839 #[test]
2840 fn test_build_graph_times_out_with_expired_budget() {
2841 let source = r"
2842 namespace demo {
2843 class Service {
2844 public:
2845 void process() {}
2846 };
2847 }
2848 ";
2849 let tree = parse_cpp(source);
2850 let builder = CppGraphBuilder::new();
2851 let mut staging = StagingGraph::new();
2852 let mut budget = BuildBudget::already_expired(Path::new("timeout.cpp"));
2853
2854 let err = builder
2855 .build_graph_with_budget(
2856 &tree,
2857 source.as_bytes(),
2858 Path::new("timeout.cpp"),
2859 &mut staging,
2860 &mut budget,
2861 )
2862 .expect_err("expired budget should force timeout");
2863
2864 match err {
2865 GraphBuilderError::BuildTimedOut {
2866 file,
2867 phase,
2868 timeout_ms,
2869 } => {
2870 assert_eq!(file, PathBuf::from("timeout.cpp"));
2871 assert_eq!(phase, "cpp:extract_namespace_map");
2872 assert_eq!(timeout_ms, 1_000);
2873 }
2874 other => panic!("expected BuildTimedOut, got {other:?}"),
2875 }
2876 }
2877
2878 #[test]
2879 fn test_extract_class() {
2880 let source = "class User { }";
2881 let tree = parse_cpp(source);
2882 let mut staging = StagingGraph::new();
2883 let builder = CppGraphBuilder::new();
2884
2885 let result = builder.build_graph(
2886 &tree,
2887 source.as_bytes(),
2888 Path::new("test.cpp"),
2889 &mut staging,
2890 );
2891
2892 assert!(result.is_ok());
2893 assert_has_node_with_kind(&staging, "User", NodeKind::Class);
2894 }
2895
2896 #[test]
2897 fn test_extract_template_class() {
2898 let source = r"
2899 template <typename T>
2900 class Person {
2901 public:
2902 T name;
2903 T age;
2904 };
2905 ";
2906 let tree = parse_cpp(source);
2907 let mut staging = StagingGraph::new();
2908 let builder = CppGraphBuilder::new();
2909
2910 let result = builder.build_graph(
2911 &tree,
2912 source.as_bytes(),
2913 Path::new("test.cpp"),
2914 &mut staging,
2915 );
2916
2917 assert!(result.is_ok());
2918 assert_has_node_with_kind(&staging, "Person", NodeKind::Class);
2919 }
2920
2921 #[test]
2922 fn test_nested_named_types_emit_nodes() {
2923 let source = r"
2928 class Outer {
2929 public:
2930 class Inner { int z; };
2931 struct InnerS { int w; };
2932 union InnerU { int i; float f; };
2933 enum class InnerE { A, B };
2934 class L1 { public: class L2 { int q; }; };
2935 };
2936 namespace ns {
2937 class NsOuter { public: class NsInner { int n; }; };
2938 }
2939 ";
2940 let staging = build_cpp(source);
2941
2942 assert_has_node_with_kind_exact(&staging, "Outer::Inner", NodeKind::Class);
2944 assert_has_node_with_kind_exact(&staging, "Outer::InnerS", NodeKind::Struct);
2945 assert_has_node_with_kind_exact(&staging, "Outer::InnerU", NodeKind::Struct);
2947 assert_has_node_with_kind_exact(&staging, "Outer::InnerE", NodeKind::Enum);
2948 assert_has_node_with_kind_exact(&staging, "Outer::L1", NodeKind::Class);
2950 assert_has_node_with_kind_exact(&staging, "Outer::L1::L2", NodeKind::Class);
2951 assert_has_node_with_kind_exact(&staging, "ns::NsOuter", NodeKind::Class);
2953 assert_has_node_with_kind_exact(&staging, "ns::NsOuter::NsInner", NodeKind::Class);
2954
2955 assert_has_node_with_kind_exact(&staging, "Outer::Inner.z", NodeKind::Property);
2958 assert_has_node_with_kind_exact(&staging, "Outer::L1::L2.q", NodeKind::Property);
2959 assert_has_node_with_kind_exact(&staging, "ns::NsOuter::NsInner.n", NodeKind::Property);
2960 }
2961
2962 #[test]
2963 fn test_nested_enum_carries_enclosing_visibility() {
2964 let source = r"
2968 class Outer {
2969 private:
2970 enum class Secret { A, B };
2971 public:
2972 enum class Pub { X, Y };
2973 };
2974 ";
2975 let staging = build_cpp(source);
2976
2977 let secret = cpp_find_added_node(&staging, "Outer::Secret")
2978 .expect("nested enum Outer::Secret must be staged");
2979 assert_eq!(secret.kind, NodeKind::Enum, "Secret must be an Enum node");
2980 let secret_vis = staging.resolve_local_string(
2981 secret
2982 .visibility
2983 .expect("nested enum must carry a visibility id"),
2984 );
2985 assert_eq!(
2986 secret_vis,
2987 Some("private"),
2988 "nested enum under `private:` must be private"
2989 );
2990
2991 let pub_enum = cpp_find_added_node(&staging, "Outer::Pub")
2992 .expect("nested enum Outer::Pub must be staged");
2993 let pub_vis = staging.resolve_local_string(
2994 pub_enum
2995 .visibility
2996 .expect("nested enum must carry a visibility id"),
2997 );
2998 assert_eq!(
2999 pub_vis,
3000 Some("public"),
3001 "nested enum under `public:` must be public"
3002 );
3003 }
3004
3005 #[test]
3006 fn test_nested_class_emits_inheritance_edge() {
3007 let source = r"
3011 struct Base { virtual ~Base(); };
3012 class Outer {
3013 public:
3014 class Derived : public Base {};
3015 };
3016 ";
3017 let staging = build_cpp(source);
3018
3019 let derived_id = cpp_find_added_node_id(&staging, "Outer::Derived", NodeKind::Class)
3020 .expect("nested Derived class node must be staged");
3021
3022 let has_inherits = staging.operations().iter().any(|op| {
3023 matches!(
3024 op,
3025 StagingOp::AddEdge {
3026 source: src,
3027 kind: EdgeKind::Inherits,
3028 ..
3029 } if *src == derived_id
3030 )
3031 });
3032 assert!(
3033 has_inherits,
3034 "nested Derived must emit an Inherits edge to its base"
3035 );
3036 }
3037
3038 #[test]
3039 fn test_top_level_union_emits_struct_node() {
3040 let source = "union Value { int i; float f; };";
3044 let staging = build_cpp(source);
3045 assert_has_node_with_kind_exact(&staging, "Value", NodeKind::Struct);
3046 }
3047
3048 #[test]
3049 fn test_extract_function() {
3050 let source = r#"
3051 #include <cstdio>
3052 void hello() {
3053 std::printf("Hello");
3054 }
3055 "#;
3056 let tree = parse_cpp(source);
3057 let mut staging = StagingGraph::new();
3058 let builder = CppGraphBuilder::new();
3059
3060 let result = builder.build_graph(
3061 &tree,
3062 source.as_bytes(),
3063 Path::new("test.cpp"),
3064 &mut staging,
3065 );
3066
3067 assert!(result.is_ok());
3068 assert_has_node_with_kind(&staging, "hello", NodeKind::Function);
3069 }
3070
3071 #[test]
3072 fn test_extract_virtual_function() {
3073 let source = r"
3074 class Service {
3075 public:
3076 virtual void fetchData() {}
3077 };
3078 ";
3079 let tree = parse_cpp(source);
3080 let mut staging = StagingGraph::new();
3081 let builder = CppGraphBuilder::new();
3082
3083 let result = builder.build_graph(
3084 &tree,
3085 source.as_bytes(),
3086 Path::new("test.cpp"),
3087 &mut staging,
3088 );
3089
3090 assert!(result.is_ok());
3091 assert_has_node(&staging, "fetchData");
3092 }
3093
3094 #[test]
3095 fn test_extract_call_edge() {
3096 let source = r"
3097 void greet() {}
3098
3099 int main() {
3100 greet();
3101 return 0;
3102 }
3103 ";
3104 let tree = parse_cpp(source);
3105 let mut staging = StagingGraph::new();
3106 let builder = CppGraphBuilder::new();
3107
3108 let result = builder.build_graph(
3109 &tree,
3110 source.as_bytes(),
3111 Path::new("test.cpp"),
3112 &mut staging,
3113 );
3114
3115 assert!(result.is_ok());
3116 assert_has_node(&staging, "main");
3117 assert_has_node(&staging, "greet");
3118 let calls = collect_call_edges(&staging);
3119 assert!(!calls.is_empty());
3120 }
3121
3122 #[test]
3123 fn test_extract_member_call_edge() {
3124 let source = r"
3125 class Service {
3126 public:
3127 void helper() {}
3128 };
3129
3130 int main() {
3131 Service svc;
3132 svc.helper();
3133 return 0;
3134 }
3135 ";
3136 let tree = parse_cpp(source);
3137 let mut staging = StagingGraph::new();
3138 let builder = CppGraphBuilder::new();
3139
3140 let result = builder.build_graph(
3141 &tree,
3142 source.as_bytes(),
3143 Path::new("member.cpp"),
3144 &mut staging,
3145 );
3146
3147 assert!(result.is_ok());
3148 assert_has_node(&staging, "main");
3149 assert_has_node(&staging, "helper");
3150 let calls = collect_call_edges(&staging);
3151 assert!(!calls.is_empty());
3152 }
3153
3154 #[test]
3155 fn test_extract_namespace_map_simple() {
3156 let source = r"
3157 namespace demo {
3158 void func() {}
3159 }
3160 ";
3161 let tree = parse_cpp(source);
3162 let namespace_map = extract_namespace_map_for_test(&tree, source);
3163
3164 assert_eq!(namespace_map.len(), 1);
3166
3167 let (_, ns_prefix) = namespace_map.iter().next().unwrap();
3169 assert_eq!(ns_prefix, "demo::");
3170 }
3171
3172 #[test]
3173 fn test_extract_namespace_map_nested() {
3174 let source = r"
3175 namespace outer {
3176 namespace inner {
3177 void func() {}
3178 }
3179 }
3180 ";
3181 let tree = parse_cpp(source);
3182 let namespace_map = extract_namespace_map_for_test(&tree, source);
3183
3184 assert!(namespace_map.len() >= 2);
3186
3187 let ns_values: Vec<&String> = namespace_map.values().collect();
3189 assert!(ns_values.iter().any(|v| v.as_str() == "outer::"));
3190 assert!(ns_values.iter().any(|v| v.as_str() == "outer::inner::"));
3191 }
3192
3193 #[test]
3194 fn test_extract_namespace_map_multiple() {
3195 let source = r"
3196 namespace first {
3197 void func1() {}
3198 }
3199 namespace second {
3200 void func2() {}
3201 }
3202 ";
3203 let tree = parse_cpp(source);
3204 let namespace_map = extract_namespace_map_for_test(&tree, source);
3205
3206 assert_eq!(namespace_map.len(), 2);
3208
3209 let ns_values: Vec<&String> = namespace_map.values().collect();
3210 assert!(ns_values.iter().any(|v| v.as_str() == "first::"));
3211 assert!(ns_values.iter().any(|v| v.as_str() == "second::"));
3212 }
3213
3214 #[test]
3215 fn test_find_namespace_for_offset() {
3216 let source = r"
3217 namespace demo {
3218 void func() {}
3219 }
3220 ";
3221 let tree = parse_cpp(source);
3222 let namespace_map = extract_namespace_map_for_test(&tree, source);
3223
3224 let func_offset = source.find("func").unwrap();
3226 let ns = find_namespace_for_offset(func_offset, &namespace_map);
3227 assert_eq!(ns, "demo::");
3228
3229 let ns = find_namespace_for_offset(0, &namespace_map);
3231 assert_eq!(ns, "");
3232 }
3233
3234 #[test]
3235 fn test_extract_cpp_contexts_free_function() {
3236 let source = r"
3237 void helper() {}
3238 ";
3239 let tree = parse_cpp(source);
3240 let namespace_map = extract_namespace_map_for_test(&tree, source);
3241 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3242
3243 assert_eq!(contexts.len(), 1);
3244 assert_eq!(contexts[0].qualified_name, "helper");
3245 assert!(!contexts[0].is_static);
3246 assert!(!contexts[0].is_virtual);
3247 }
3248
3249 #[test]
3250 fn test_extract_cpp_contexts_namespace_function() {
3251 let source = r"
3252 namespace demo {
3253 void helper() {}
3254 }
3255 ";
3256 let tree = parse_cpp(source);
3257 let namespace_map = extract_namespace_map_for_test(&tree, source);
3258 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3259
3260 assert_eq!(contexts.len(), 1);
3261 assert_eq!(contexts[0].qualified_name, "demo::helper");
3262 assert_eq!(contexts[0].namespace_stack, vec!["demo"]);
3263 }
3264
3265 #[test]
3266 fn test_extract_cpp_contexts_class_method() {
3267 let source = r"
3268 class Service {
3269 public:
3270 void process() {}
3271 };
3272 ";
3273 let tree = parse_cpp(source);
3274 let namespace_map = extract_namespace_map_for_test(&tree, source);
3275 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3276
3277 assert_eq!(contexts.len(), 1);
3278 assert_eq!(contexts[0].qualified_name, "Service::process");
3279 assert_eq!(contexts[0].class_stack, vec!["Service"]);
3280 }
3281
3282 #[test]
3283 fn test_extract_cpp_contexts_namespace_and_class() {
3284 let source = r"
3285 namespace demo {
3286 class Service {
3287 public:
3288 void process() {}
3289 };
3290 }
3291 ";
3292 let tree = parse_cpp(source);
3293 let namespace_map = extract_namespace_map_for_test(&tree, source);
3294 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3295
3296 assert_eq!(contexts.len(), 1);
3297 assert_eq!(contexts[0].qualified_name, "demo::Service::process");
3298 assert_eq!(contexts[0].namespace_stack, vec!["demo"]);
3299 assert_eq!(contexts[0].class_stack, vec!["Service"]);
3300 }
3301
3302 #[test]
3303 fn test_extract_cpp_contexts_static_method() {
3304 let source = r"
3305 class Repository {
3306 public:
3307 static void save() {}
3308 };
3309 ";
3310 let tree = parse_cpp(source);
3311 let namespace_map = extract_namespace_map_for_test(&tree, source);
3312 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3313
3314 assert_eq!(contexts.len(), 1);
3315 assert_eq!(contexts[0].qualified_name, "Repository::save");
3316 assert!(contexts[0].is_static);
3317 }
3318
3319 #[test]
3320 fn test_extract_cpp_contexts_virtual_method() {
3321 let source = r"
3322 class Base {
3323 public:
3324 virtual void render() {}
3325 };
3326 ";
3327 let tree = parse_cpp(source);
3328 let namespace_map = extract_namespace_map_for_test(&tree, source);
3329 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3330
3331 assert_eq!(contexts.len(), 1);
3332 assert_eq!(contexts[0].qualified_name, "Base::render");
3333 assert!(contexts[0].is_virtual);
3334 }
3335
3336 #[test]
3337 fn test_extract_cpp_contexts_inline_function() {
3338 let source = r"
3339 inline void helper() {}
3340 ";
3341 let tree = parse_cpp(source);
3342 let namespace_map = extract_namespace_map_for_test(&tree, source);
3343 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3344
3345 assert_eq!(contexts.len(), 1);
3346 assert_eq!(contexts[0].qualified_name, "helper");
3347 assert!(contexts[0].is_inline);
3348 }
3349
3350 #[test]
3351 fn test_extract_cpp_contexts_out_of_line_definition() {
3352 let source = r"
3353 namespace demo {
3354 class Service {
3355 public:
3356 int process(int v);
3357 };
3358
3359 inline int Service::process(int v) {
3360 return v;
3361 }
3362 }
3363 ";
3364 let tree = parse_cpp(source);
3365 let namespace_map = extract_namespace_map_for_test(&tree, source);
3366 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3367
3368 assert_eq!(contexts.len(), 1);
3370 assert_eq!(contexts[0].qualified_name, "demo::Service::process");
3371 assert!(contexts[0].is_inline);
3372 }
3373
3374 #[test]
3375 fn test_extract_field_types_simple() {
3376 let source = r"
3377 class Service {
3378 public:
3379 Repository repo;
3380 };
3381 ";
3382 let tree = parse_cpp(source);
3383 let namespace_map = extract_namespace_map_for_test(&tree, source);
3384 let (field_types, _type_map) =
3385 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3386
3387 assert_eq!(field_types.len(), 1);
3389 assert_eq!(
3390 field_types.get(&("Service".to_string(), "repo".to_string())),
3391 Some(&"Repository".to_string())
3392 );
3393 }
3394
3395 #[test]
3396 fn test_extract_field_types_namespace() {
3397 let source = r"
3398 namespace demo {
3399 class Service {
3400 public:
3401 Repository repo;
3402 };
3403 }
3404 ";
3405 let tree = parse_cpp(source);
3406 let namespace_map = extract_namespace_map_for_test(&tree, source);
3407 let (field_types, _type_map) =
3408 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3409
3410 assert_eq!(field_types.len(), 1);
3412 assert_eq!(
3413 field_types.get(&("demo::Service".to_string(), "repo".to_string())),
3414 Some(&"Repository".to_string())
3415 );
3416 }
3417
3418 #[test]
3419 fn test_extract_field_types_no_collision() {
3420 let source = r"
3421 class ServiceA {
3422 public:
3423 Repository repo;
3424 };
3425
3426 class ServiceB {
3427 public:
3428 Repository repo;
3429 };
3430 ";
3431 let tree = parse_cpp(source);
3432 let namespace_map = extract_namespace_map_for_test(&tree, source);
3433 let (field_types, _type_map) =
3434 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3435
3436 assert_eq!(field_types.len(), 2);
3438 assert_eq!(
3439 field_types.get(&("ServiceA".to_string(), "repo".to_string())),
3440 Some(&"Repository".to_string())
3441 );
3442 assert_eq!(
3443 field_types.get(&("ServiceB".to_string(), "repo".to_string())),
3444 Some(&"Repository".to_string())
3445 );
3446 }
3447
3448 #[test]
3449 fn test_extract_using_declaration() {
3450 let source = r"
3451 using std::vector;
3452
3453 class Service {
3454 public:
3455 vector data;
3456 };
3457 ";
3458 let tree = parse_cpp(source);
3459 let namespace_map = extract_namespace_map_for_test(&tree, source);
3460 let (field_types, type_map) =
3461 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3462
3463 assert_eq!(field_types.len(), 1);
3465 assert_eq!(
3466 field_types.get(&("Service".to_string(), "data".to_string())),
3467 Some(&"std::vector".to_string()),
3468 "Field type should resolve 'vector' to 'std::vector' via using declaration"
3469 );
3470
3471 assert_eq!(
3473 type_map.get(&(String::new(), "vector".to_string())),
3474 Some(&"std::vector".to_string()),
3475 "Using declaration should map 'vector' to 'std::vector' in type_map"
3476 );
3477 }
3478
3479 #[test]
3480 fn test_extract_field_types_pointer() {
3481 let source = r"
3482 class Service {
3483 public:
3484 Repository* repo;
3485 };
3486 ";
3487 let tree = parse_cpp(source);
3488 let namespace_map = extract_namespace_map_for_test(&tree, source);
3489 let (field_types, _type_map) =
3490 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3491
3492 assert_eq!(field_types.len(), 1);
3494 assert_eq!(
3495 field_types.get(&("Service".to_string(), "repo".to_string())),
3496 Some(&"Repository".to_string())
3497 );
3498 }
3499
3500 #[test]
3501 fn test_extract_field_types_multiple_declarators() {
3502 let source = r"
3503 class Service {
3504 public:
3505 Repository repo_a, repo_b, repo_c;
3506 };
3507 ";
3508 let tree = parse_cpp(source);
3509 let namespace_map = extract_namespace_map_for_test(&tree, source);
3510 let (field_types, _type_map) =
3511 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3512
3513 assert_eq!(field_types.len(), 3);
3515 assert_eq!(
3516 field_types.get(&("Service".to_string(), "repo_a".to_string())),
3517 Some(&"Repository".to_string())
3518 );
3519 assert_eq!(
3520 field_types.get(&("Service".to_string(), "repo_b".to_string())),
3521 Some(&"Repository".to_string())
3522 );
3523 assert_eq!(
3524 field_types.get(&("Service".to_string(), "repo_c".to_string())),
3525 Some(&"Repository".to_string())
3526 );
3527 }
3528
3529 #[test]
3530 fn test_extract_field_types_nested_struct_with_parent_field() {
3531 let source = r"
3534 namespace demo {
3535 struct Outer {
3536 int outer_field;
3537 struct Inner {
3538 int inner_field;
3539 };
3540 Inner nested_instance;
3541 };
3542 }
3543 ";
3544 let tree = parse_cpp(source);
3545 let namespace_map = extract_namespace_map_for_test(&tree, source);
3546 let (field_types, _type_map) =
3547 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
3548
3549 assert!(
3552 field_types.len() >= 2,
3553 "Expected at least outer_field and nested_instance"
3554 );
3555
3556 assert_eq!(
3558 field_types.get(&("demo::Outer".to_string(), "outer_field".to_string())),
3559 Some(&"int".to_string())
3560 );
3561
3562 assert_eq!(
3564 field_types.get(&("demo::Outer".to_string(), "nested_instance".to_string())),
3565 Some(&"Inner".to_string())
3566 );
3567
3568 if field_types.contains_key(&("demo::Outer::Inner".to_string(), "inner_field".to_string()))
3570 {
3571 assert_eq!(
3573 field_types.get(&("demo::Outer::Inner".to_string(), "inner_field".to_string())),
3574 Some(&"int".to_string()),
3575 "Inner class fields must use parent-qualified FQN 'demo::Outer::Inner'"
3576 );
3577 }
3578 }
3579
3580 use sqry_core::graph::unified::build::staging::StagingOp;
3597 use sqry_core::graph::unified::edge::kind::{EdgeKind, TypeOfContext};
3598
3599 fn cpp_find_added_node<'a>(
3601 staging: &'a StagingGraph,
3602 canonical_name: &str,
3603 ) -> Option<&'a sqry_core::graph::unified::storage::arena::NodeEntry> {
3604 staging.operations().iter().find_map(|op| {
3605 if let StagingOp::AddNode { entry, .. } = op
3606 && staging.resolve_node_canonical_name(entry) == Some(canonical_name)
3607 {
3608 Some(entry)
3609 } else {
3610 None
3611 }
3612 })
3613 }
3614
3615 fn cpp_find_added_node_id(
3617 staging: &StagingGraph,
3618 canonical_name: &str,
3619 kind: NodeKind,
3620 ) -> Option<sqry_core::graph::unified::NodeId> {
3621 staging.operations().iter().find_map(|op| match op {
3622 StagingOp::AddNode {
3623 entry,
3624 expected_id: Some(id),
3625 } if entry.kind == kind
3626 && staging.resolve_node_canonical_name(entry) == Some(canonical_name) =>
3627 {
3628 Some(*id)
3629 }
3630 _ => None,
3631 })
3632 }
3633
3634 fn build_cpp(source: &str) -> StagingGraph {
3636 let tree = parse_cpp(source);
3637 let mut staging = StagingGraph::new();
3638 let builder = CppGraphBuilder::new();
3639 builder
3640 .build_graph(
3641 &tree,
3642 source.as_bytes(),
3643 Path::new("test.cpp"),
3644 &mut staging,
3645 )
3646 .expect("build_graph must succeed for the test fixture");
3647 staging
3648 }
3649
3650 #[test]
3656 fn test_struct_field_emits_property_with_field_context() {
3657 let source = "struct Point { int x; int y; };";
3658 let staging = build_cpp(source);
3659
3660 assert_has_node_with_kind_exact(&staging, "Point.x", NodeKind::Property);
3662 assert_has_node_with_kind_exact(&staging, "Point.y", NodeKind::Property);
3663
3664 let entry =
3665 cpp_find_added_node(&staging, "Point.x").expect("Point.x should be staged as a node");
3666 assert_eq!(entry.kind, NodeKind::Property, "x must be Property");
3667 assert!(!entry.is_static, "instance field is_static must be false");
3668 let vis = staging.resolve_local_string(entry.visibility.expect("visibility id"));
3669 assert_eq!(
3670 vis,
3671 Some("public"),
3672 "struct default visibility must be 'public'"
3673 );
3674 assert!(entry.end_line > 0, "field end_line must be set (got 0)");
3680 assert!(
3681 entry.end_line > entry.start_line
3682 || (entry.end_line == entry.start_line && entry.end_column > entry.start_column),
3683 "field span must be non-empty: [{}:{}..{}:{}]",
3684 entry.start_line,
3685 entry.start_column,
3686 entry.end_line,
3687 entry.end_column,
3688 );
3689
3690 let x_id = cpp_find_added_node_id(&staging, "Point.x", NodeKind::Property)
3692 .expect("Point.x Property NodeId");
3693 let edge = staging.operations().iter().find_map(|op| {
3694 if let StagingOp::AddEdge {
3695 source: src,
3696 kind: EdgeKind::TypeOf { context, name, .. },
3697 ..
3698 } = op
3699 && *src == x_id
3700 {
3701 Some((*context, *name))
3702 } else {
3703 None
3704 }
3705 });
3706 let (ctx, name) = edge.expect("TypeOf edge from Point.x should be staged");
3707 assert_eq!(
3708 ctx,
3709 Some(TypeOfContext::Field),
3710 "TypeOf edge context must be Field"
3711 );
3712 let resolved_name = name.and_then(|sid| staging.resolve_local_string(sid));
3713 assert_eq!(
3714 resolved_name,
3715 Some("x"),
3716 "TypeOf edge name must be the bare field name 'x'"
3717 );
3718
3719 let stale_variable = staging.nodes().any(|n| {
3721 n.entry.kind == NodeKind::Variable
3722 && matches!(
3723 staging.resolve_node_name(n.entry),
3724 Some("Point.x" | "Point.y" | "Point::x" | "Point::y")
3725 )
3726 });
3727 assert!(
3728 !stale_variable,
3729 "Point fields must not be emitted as NodeKind::Variable"
3730 );
3731 }
3732
3733 #[test]
3735 fn test_class_field_default_visibility_is_private() {
3736 let source = "class Foo { int hidden; };";
3737 let staging = build_cpp(source);
3738
3739 let entry = cpp_find_added_node(&staging, "Foo.hidden")
3740 .expect("Foo.hidden should be staged as a node");
3741 assert_eq!(entry.kind, NodeKind::Property);
3742 let vis = staging.resolve_local_string(entry.visibility.expect("visibility id"));
3743 assert_eq!(
3744 vis,
3745 Some("private"),
3746 "class default visibility must be 'private'"
3747 );
3748 }
3749
3750 #[test]
3752 fn test_class_field_respects_explicit_access_specifier() {
3753 let source = "class Foo { public: int public_field; protected: int prot_field; };";
3754 let staging = build_cpp(source);
3755
3756 let pub_entry = cpp_find_added_node(&staging, "Foo.public_field")
3757 .expect("Foo.public_field should be staged");
3758 assert_eq!(
3759 staging.resolve_local_string(pub_entry.visibility.expect("vis")),
3760 Some("public")
3761 );
3762
3763 let prot_entry = cpp_find_added_node(&staging, "Foo.prot_field")
3764 .expect("Foo.prot_field should be staged");
3765 assert_eq!(
3766 staging.resolve_local_string(prot_entry.visibility.expect("vis")),
3767 Some("protected")
3768 );
3769 }
3770
3771 #[test]
3774 fn test_const_field_emits_constant() {
3775 let source = "class Foo { const int kMax = 0; };";
3776 let staging = build_cpp(source);
3777
3778 assert_has_node_with_kind_exact(&staging, "Foo.kMax", NodeKind::Constant);
3779 let entry = cpp_find_added_node(&staging, "Foo.kMax").expect("Foo.kMax");
3780 assert_eq!(entry.kind, NodeKind::Constant);
3781 assert!(
3782 !entry.is_static,
3783 "const (non-static) field is_static must be false; only `static` keyword sets is_static"
3784 );
3785 }
3786
3787 #[test]
3791 fn test_constexpr_field_emits_constant() {
3792 let source = "class Foo { constexpr static int kAnswer = 42; };";
3793 let staging = build_cpp(source);
3794
3795 assert_has_node_with_kind_exact(&staging, "Foo.kAnswer", NodeKind::Constant);
3796 let entry = cpp_find_added_node(&staging, "Foo.kAnswer").expect("Foo.kAnswer");
3797 assert_eq!(entry.kind, NodeKind::Constant);
3798 assert!(
3799 entry.is_static,
3800 "static constexpr member must have is_static = true"
3801 );
3802 }
3803
3804 #[test]
3807 fn test_static_field_sets_is_static_true() {
3808 let source = "class Foo { static int counter; };";
3809 let staging = build_cpp(source);
3810
3811 let entry = cpp_find_added_node(&staging, "Foo.counter").expect("Foo.counter");
3812 assert_eq!(entry.kind, NodeKind::Property);
3813 assert!(entry.is_static, "static keyword must set is_static = true");
3814 }
3815
3816 #[test]
3819 fn test_bitfield_emits_property() {
3820 let source = "struct Flags { unsigned int low : 4; unsigned int high : 4; };";
3821 let staging = build_cpp(source);
3822
3823 assert_has_node_with_kind_exact(&staging, "Flags.low", NodeKind::Property);
3824 assert_has_node_with_kind_exact(&staging, "Flags.high", NodeKind::Property);
3825 }
3826
3827 #[test]
3833 fn test_anonymous_union_member_fields_emit_property() {
3834 let source = r"
3835class Variant {
3836public:
3837 int tag;
3838 union {
3839 int as_int;
3840 float as_float;
3841 };
3842};
3843";
3844 let staging = build_cpp(source);
3845
3846 assert_has_node_with_kind_exact(&staging, "Variant.tag", NodeKind::Property);
3848
3849 assert_has_node_with_kind_exact(&staging, "Variant.as_int", NodeKind::Property);
3852 assert_has_node_with_kind_exact(&staging, "Variant.as_float", NodeKind::Property);
3853
3854 let as_int = cpp_find_added_node(&staging, "Variant.as_int")
3857 .expect("Variant.as_int should be staged");
3858 let vis = staging.resolve_local_string(as_int.visibility.expect("visibility id"));
3859 assert_eq!(
3860 vis,
3861 Some("public"),
3862 "anonymous-union members must inherit OUTER access (`public:` here)"
3863 );
3864
3865 let bogus = staging.nodes().any(|n| {
3868 staging
3869 .resolve_node_name(n.entry)
3870 .is_some_and(|name| name.contains("::.") || name.starts_with("Variant::."))
3871 });
3872 assert!(
3873 !bogus,
3874 "anonymous union must not produce a synthetic qualifier"
3875 );
3876
3877 let stale_variable = staging.nodes().any(|n| {
3879 n.entry.kind == NodeKind::Variable
3880 && matches!(
3881 staging.resolve_node_name(n.entry),
3882 Some("Variant.tag" | "Variant.as_int" | "Variant.as_float")
3883 )
3884 });
3885 assert!(
3886 !stale_variable,
3887 "anonymous-union members + outer fields must not stay as Variable"
3888 );
3889 }
3890
3891 #[test]
3895 fn test_templated_class_field_emits_property() {
3896 let source = r"
3897template<class T>
3898struct Box {
3899 T value;
3900};
3901";
3902 let staging = build_cpp(source);
3903
3904 assert_has_node_with_kind_exact(&staging, "Box.value", NodeKind::Property);
3905 let entry = cpp_find_added_node(&staging, "Box.value").expect("Box.value");
3906 assert_eq!(entry.kind, NodeKind::Property);
3907 assert!(!entry.is_static);
3908 }
3909
3910 #[test]
3916 fn test_outer_class_field_with_nested_class_present() {
3917 let source = r"
3918class Outer {
3919public:
3920 int outer_value;
3921 class Inner {
3922 public:
3923 int x;
3924 };
3925};
3926";
3927 let staging = build_cpp(source);
3928
3929 assert_has_node_with_kind_exact(&staging, "Outer.outer_value", NodeKind::Property);
3931
3932 assert_has_node_with_kind_exact(&staging, "Outer::Inner.x", NodeKind::Property);
3936
3937 let legacy_hits: Vec<_> = staging
3940 .nodes()
3941 .filter(|n| staging.resolve_node_name(n.entry) == Some("Outer::outer_value"))
3942 .collect();
3943 assert!(
3944 legacy_hits.is_empty(),
3945 "legacy `Outer::outer_value` lookup must return 0 hits"
3946 );
3947
3948 for legacy in ["Inner.x", "Outer::Inner::x", "Outer.Inner.x"] {
3952 let hits: Vec<_> = staging
3953 .nodes()
3954 .filter(|n| staging.resolve_node_name(n.entry) == Some(legacy))
3955 .collect();
3956 assert!(
3957 hits.is_empty(),
3958 "nested-class field `{legacy}` must not appear; expected only `Outer::Inner.x`"
3959 );
3960 }
3961 }
3962
3963 #[test]
3967 fn test_outer_class_with_nested_struct_emits_inner_field() {
3968 let source = r"
3969class Outer {
3970private:
3971 struct Inner {
3972 int y;
3973 };
3974};
3975";
3976 let staging = build_cpp(source);
3977
3978 assert_has_node_with_kind_exact(&staging, "Outer::Inner.y", NodeKind::Property);
3979
3980 let entry = cpp_find_added_node(&staging, "Outer::Inner.y")
3981 .expect("Outer::Inner.y should be staged");
3982 let vis = staging.resolve_local_string(entry.visibility.expect("visibility id"));
3983 assert_eq!(
3984 vis,
3985 Some("public"),
3986 "nested struct field default visibility must be 'public' \
3987 regardless of OUTER access state"
3988 );
3989 }
3990
3991 #[test]
3999 fn test_legacy_double_colon_field_lookup_returns_zero() {
4000 let source = r"
4001class Foo {
4002public:
4003 int bar;
4004 static int baz;
4005 const int qux = 0;
4006};
4007struct Quux {
4008 int corge;
4009};
4010";
4011 let staging = build_cpp(source);
4012
4013 assert_has_node_with_kind_exact(&staging, "Foo.bar", NodeKind::Property);
4015 assert_has_node_with_kind_exact(&staging, "Foo.baz", NodeKind::Property);
4016 assert_has_node_with_kind_exact(&staging, "Foo.qux", NodeKind::Constant);
4017 assert_has_node_with_kind_exact(&staging, "Quux.corge", NodeKind::Property);
4018
4019 for legacy in ["Foo::bar", "Foo::baz", "Foo::qux", "Quux::corge"] {
4022 let hits: Vec<_> = staging
4023 .nodes()
4024 .filter(|n| staging.resolve_node_name(n.entry) == Some(legacy))
4025 .collect();
4026 assert!(
4027 hits.is_empty(),
4028 "legacy lookup for {legacy:?} must return 0 hits, got {} node(s) ({:?})",
4029 hits.len(),
4030 hits.iter()
4031 .map(|n| (n.entry.kind, staging.resolve_node_name(n.entry)))
4032 .collect::<Vec<_>>()
4033 );
4034 }
4035 }
4036
4037 #[test]
4040 fn test_namespaced_class_field_qualified_name() {
4041 let source = r"
4042namespace demo {
4043 class Service {
4044 public:
4045 int counter;
4046 };
4047}
4048";
4049 let staging = build_cpp(source);
4050
4051 assert_has_node_with_kind_exact(&staging, "demo::Service.counter", NodeKind::Property);
4052 }
4053}
4054
4055#[cfg(test)]
4056mod shape_tests {
4057 use super::{cf_bucket_for_cpp_kind, cpp_shape_mapping};
4058 use sqry_core::graph::unified::build::shape::{
4059 CfBucket, ShapeBudget, ShapeMapping, compute_shape_descriptor,
4060 };
4061
4062 const SAMPLE: &str = include_str!(concat!(
4063 env!("CARGO_MANIFEST_DIR"),
4064 "/../test-fixtures/shape/reference/sample.cpp"
4065 ));
4066
4067 fn parse(src: &str) -> tree_sitter::Tree {
4068 let lang: tree_sitter::Language = tree_sitter_cpp::LANGUAGE.into();
4069 let mut p = tree_sitter::Parser::new();
4070 p.set_language(&lang).expect("load cpp grammar");
4071 p.parse(src, None).expect("parse")
4072 }
4073
4074 fn function_named<'t>(tree: &'t tree_sitter::Tree, name: &str) -> tree_sitter::Node<'t> {
4076 let root = tree.root_node();
4077 let mut stack = vec![root];
4078 while let Some(node) = stack.pop() {
4079 if node.kind() == "function_definition"
4080 && function_def_name(node).as_deref() == Some(name)
4081 {
4082 return node;
4083 }
4084 let mut c = node.walk();
4085 for ch in node.children(&mut c) {
4086 stack.push(ch);
4087 }
4088 }
4089 panic!("no function_definition named {name}");
4090 }
4091
4092 fn function_def_name(node: tree_sitter::Node) -> Option<String> {
4094 let mut decl = node.child_by_field_name("declarator")?;
4095 for _ in 0..8 {
4096 if decl.kind() == "function_declarator" {
4097 let inner = decl.child_by_field_name("declarator")?;
4098 return inner.utf8_text(SAMPLE.as_bytes()).ok().map(str::to_owned);
4099 }
4100 decl = decl.child_by_field_name("declarator")?;
4101 }
4102 None
4103 }
4104
4105 #[test]
4106 fn cf_table_is_non_empty() {
4107 let mapping = cpp_shape_mapping();
4108 let lang: tree_sitter::Language = tree_sitter_cpp::LANGUAGE.into();
4109 let mut covered = 0;
4110 for id in 0..lang.node_kind_count() {
4111 if mapping.cf_bucket(id as u16).is_some() {
4112 covered += 1;
4113 }
4114 }
4115 assert!(
4116 covered >= 10,
4117 "expected many C++ CF kinds mapped, got {covered}"
4118 );
4119 }
4120
4121 #[test]
4122 fn histogram_covers_real_control_flow() {
4123 let tree = parse(SAMPLE);
4124 let func = function_named(&tree, "classify");
4125 let d = compute_shape_descriptor(
4126 func,
4127 SAMPLE.as_bytes(),
4128 cpp_shape_mapping(),
4129 &ShapeBudget::default(),
4130 );
4131 assert!(!d.is_unhashable());
4132 for bucket in [
4133 CfBucket::Branch,
4134 CfBucket::Loop,
4135 CfBucket::Match,
4136 CfBucket::Try,
4137 CfBucket::Catch,
4138 CfBucket::Throw,
4139 CfBucket::Return,
4140 CfBucket::BreakContinue,
4141 CfBucket::Call,
4142 CfBucket::Assign,
4143 ] {
4144 assert!(
4145 d.cf_histogram[bucket.index()] >= 1,
4146 "classify must exercise {bucket:?}"
4147 );
4148 }
4149 }
4150
4151 #[test]
4152 fn lambda_body_covers_closure() {
4153 let tree = parse(SAMPLE);
4154 let func = function_named(&tree, "adder");
4155 let d = compute_shape_descriptor(
4156 func,
4157 SAMPLE.as_bytes(),
4158 cpp_shape_mapping(),
4159 &ShapeBudget::default(),
4160 );
4161 assert!(
4162 d.cf_histogram[CfBucket::Closure.index()] >= 1,
4163 "lambda closure"
4164 );
4165 }
4166
4167 #[test]
4168 fn signature_shape_reads_arity_defaults_return() {
4169 let tree = parse(SAMPLE);
4170 let func = function_named(&tree, "classify");
4171 let mapping = cpp_shape_mapping();
4172 let shape = mapping.signature_shape(func, SAMPLE.as_bytes());
4173 assert_eq!(shape.arity_positional, 2);
4175 assert!(shape.has_defaults, "threshold = 0");
4176 assert!(shape.has_return_annotation, "int return type");
4177 }
4178
4179 #[test]
4182 fn ac6_cpp_classify_histogram_well_formed() {
4183 let tree = parse(SAMPLE);
4184 let func = function_named(&tree, "classify");
4185 let d = compute_shape_descriptor(
4186 func,
4187 SAMPLE.as_bytes(),
4188 cpp_shape_mapping(),
4189 &ShapeBudget::default(),
4190 );
4191 assert_eq!(d.cf_histogram[CfBucket::Branch.index()], 2, "two if levels");
4193 assert!(d.cf_histogram[CfBucket::Loop.index()] >= 2, "for + while");
4194 assert!(d.cf_histogram[CfBucket::Return.index()] >= 2, "two returns");
4195 }
4196
4197 #[test]
4198 fn unknown_kind_maps_to_none() {
4199 assert!(cf_bucket_for_cpp_kind("translation_unit").is_none());
4200 assert!(cf_bucket_for_cpp_kind("identifier").is_none());
4201 }
4202}