1use sqry_core::graph::unified::build::helper::CalleeKindHint;
19use sqry_core::graph::unified::build::shape::{CfBucket, ShapeMapping};
20use sqry_core::graph::unified::node::NodeKind;
21use sqry_core::graph::unified::storage::shape::SignatureShape;
22use sqry_core::graph::unified::{FfiConvention, GraphBuildHelper, StagingGraph};
23use sqry_core::graph::{GraphBuilder, GraphBuilderError, GraphResult, Language, Span};
24use std::{
25 collections::{HashMap, HashSet},
26 path::{Path, PathBuf},
27 sync::OnceLock,
28 time::{Duration, Instant},
29};
30use tree_sitter::{Node, Tree};
31
32const FILE_MODULE_NAME: &str = "<file_module>";
35
36type QualifiedNameMap = HashMap<(String, String), String>;
39
40type FfiRegistry = HashMap<String, (String, FfiConvention)>;
47
48type PureVirtualRegistry = HashSet<String>;
52
53const DEFAULT_GRAPH_BUILD_TIMEOUT_MS: u64 = 10_000;
54const MIN_GRAPH_BUILD_TIMEOUT_MS: u64 = 1_000;
55const MAX_GRAPH_BUILD_TIMEOUT_MS: u64 = 60_000;
56const BUDGET_CHECK_INTERVAL: u32 = 1024;
57
58fn cpp_graph_build_timeout() -> Duration {
59 let timeout_ms = std::env::var("SQRY_CPP_GRAPH_BUILD_TIMEOUT_MS")
60 .ok()
61 .and_then(|value| value.parse::<u64>().ok())
62 .unwrap_or(DEFAULT_GRAPH_BUILD_TIMEOUT_MS)
63 .clamp(MIN_GRAPH_BUILD_TIMEOUT_MS, MAX_GRAPH_BUILD_TIMEOUT_MS);
64 Duration::from_millis(timeout_ms)
65}
66
67struct BuildBudget {
68 file: PathBuf,
69 phase_timeout: Duration,
70 started_at: Instant,
71 checkpoints: u32,
72}
73
74impl BuildBudget {
75 fn new(file: &Path) -> Self {
76 Self {
77 file: file.to_path_buf(),
78 phase_timeout: cpp_graph_build_timeout(),
79 started_at: Instant::now(),
80 checkpoints: 0,
81 }
82 }
83
84 #[cfg(test)]
85 fn already_expired(file: &Path) -> Self {
86 Self {
87 file: file.to_path_buf(),
88 phase_timeout: Duration::from_secs(1),
89 started_at: Instant::now().checked_sub(Duration::from_secs(60)).unwrap(),
90 checkpoints: BUDGET_CHECK_INTERVAL - 1,
91 }
92 }
93
94 fn checkpoint(&mut self, phase: &'static str) -> GraphResult<()> {
95 self.checkpoints = self.checkpoints.wrapping_add(1);
96 if self.checkpoints.is_multiple_of(BUDGET_CHECK_INTERVAL)
97 && self.started_at.elapsed() > self.phase_timeout
98 {
99 return Err(GraphBuilderError::BuildTimedOut {
100 file: self.file.clone(),
101 phase,
102 #[allow(clippy::cast_possible_truncation)] timeout_ms: self.phase_timeout.as_millis() as u64,
104 });
105 }
106 Ok(())
107 }
108}
109
110#[derive(Debug)]
122struct ASTGraph {
123 contexts: Vec<FunctionContext>,
125 context_start_index: HashMap<usize, usize>,
127
128 field_types: QualifiedNameMap,
134
135 type_map: QualifiedNameMap,
142
143 namespace_map: HashMap<std::ops::Range<usize>, String>,
147}
148
149impl ASTGraph {
150 fn from_tree(root: Node, content: &[u8], budget: &mut BuildBudget) -> GraphResult<Self> {
152 let namespace_map = extract_namespace_map(root, content, budget)?;
154
155 let mut contexts = extract_cpp_contexts(root, content, &namespace_map, budget)?;
157 contexts.sort_by_key(|ctx| ctx.span.0);
158 let context_start_index = contexts
159 .iter()
160 .enumerate()
161 .map(|(idx, ctx)| (ctx.span.0, idx))
162 .collect();
163
164 let (field_types, type_map) =
166 extract_field_and_type_info(root, content, &namespace_map, budget)?;
167
168 Ok(Self {
169 contexts,
170 context_start_index,
171 field_types,
172 type_map,
173 namespace_map,
174 })
175 }
176
177 fn find_enclosing(&self, byte_pos: usize) -> Option<&FunctionContext> {
183 let insertion_point = self.contexts.partition_point(|ctx| ctx.span.0 <= byte_pos);
184 if insertion_point == 0 {
185 return None;
186 }
187
188 let candidate = &self.contexts[insertion_point - 1];
189 (byte_pos < candidate.span.1).then_some(candidate)
190 }
191
192 fn context_for_start(&self, start_byte: usize) -> Option<&FunctionContext> {
193 self.context_start_index
194 .get(&start_byte)
195 .and_then(|idx| self.contexts.get(*idx))
196 }
197}
198
199#[derive(Debug, Clone)]
201struct FunctionContext {
202 qualified_name: String,
204 span: (usize, usize),
206 is_static: bool,
209 #[allow(dead_code)]
213 is_virtual: bool,
214 #[allow(dead_code)]
218 is_inline: bool,
219 namespace_stack: Vec<String>,
221 class_stack: Vec<String>,
225 return_type: Option<String>,
227}
228
229impl FunctionContext {
230 #[allow(dead_code)] fn qualified_name(&self) -> &str {
232 &self.qualified_name
233 }
234}
235
236#[derive(Debug, Default, Clone, Copy)]
260pub struct CppGraphBuilder;
261
262impl CppGraphBuilder {
263 #[must_use]
265 pub fn new() -> Self {
266 Self
267 }
268
269 #[allow(clippy::unused_self)] #[allow(clippy::trivially_copy_pass_by_ref)] fn build_graph_with_budget(
272 #[allow(clippy::trivially_copy_pass_by_ref)] &self,
274 tree: &Tree,
275 content: &[u8],
276 file: &Path,
277 staging: &mut StagingGraph,
278 budget: &mut BuildBudget,
279 ) -> GraphResult<()> {
280 let mut helper = GraphBuildHelper::new(staging, file, Language::Cpp);
282
283 let ast_graph = ASTGraph::from_tree(tree.root_node(), content, budget)?;
285
286 let mut seen_includes: HashSet<String> = HashSet::new();
288
289 let mut namespace_stack: Vec<String> = Vec::new();
291 let mut class_stack: Vec<String> = Vec::new();
292
293 let mut ffi_registry = FfiRegistry::new();
296 collect_ffi_declarations(tree.root_node(), content, &mut ffi_registry, budget)?;
297
298 let mut pure_virtual_registry = PureVirtualRegistry::new();
300 collect_pure_virtual_interfaces(
301 tree.root_node(),
302 content,
303 &mut pure_virtual_registry,
304 budget,
305 )?;
306
307 walk_tree_for_graph(
309 tree.root_node(),
310 content,
311 &ast_graph,
312 &mut helper,
313 &mut seen_includes,
314 &mut namespace_stack,
315 &mut class_stack,
316 &ffi_registry,
317 &pure_virtual_registry,
318 budget,
319 )?;
320
321 Ok(())
322 }
323
324 #[allow(dead_code)] fn extract_class_attributes(node: &tree_sitter::Node, content: &[u8]) -> Vec<String> {
327 let mut attributes = Vec::new();
328 let mut cursor = node.walk();
329 for child in node.children(&mut cursor) {
330 if child.kind() == "modifiers" {
331 let mut mod_cursor = child.walk();
332 for modifier in child.children(&mut mod_cursor) {
333 if let Ok(mod_text) = modifier.utf8_text(content) {
334 match mod_text {
335 "template" => attributes.push("template".to_string()),
336 "sealed" => attributes.push("sealed".to_string()),
337 "abstract" => attributes.push("abstract".to_string()),
338 "open" => attributes.push("open".to_string()),
339 "final" => attributes.push("final".to_string()),
340 "inner" => attributes.push("inner".to_string()),
341 "value" => attributes.push("value".to_string()),
342 _ => {}
343 }
344 }
345 }
346 }
347 }
348 attributes
349 }
350
351 #[allow(dead_code)] fn extract_is_virtual(node: &tree_sitter::Node, content: &[u8]) -> bool {
354 if let Some(spec) = node.child_by_field_name("declaration_specifiers")
355 && let Ok(text) = spec.utf8_text(content)
356 && text.contains("virtual")
357 {
358 return true;
359 }
360
361 if let Ok(text) = node.utf8_text(content)
362 && text.contains("virtual")
363 {
364 return true;
365 }
366
367 if let Some(parent) = node.parent()
368 && (parent.kind() == "field_declaration" || parent.kind() == "declaration")
369 && let Ok(text) = parent.utf8_text(content)
370 && text.contains("virtual")
371 {
372 return true;
373 }
374
375 false
376 }
377
378 #[allow(dead_code)] fn extract_function_attributes(node: &tree_sitter::Node, content: &[u8]) -> Vec<String> {
381 let mut attributes = Vec::new();
382 for node_ref in [
383 node.child_by_field_name("declaration_specifiers"),
384 node.parent(),
385 ]
386 .into_iter()
387 .flatten()
388 {
389 if let Ok(text) = node_ref.utf8_text(content) {
390 for keyword in [
391 "virtual",
392 "inline",
393 "constexpr",
394 "operator",
395 "override",
396 "static",
397 ] {
398 if text.contains(keyword) && !attributes.contains(&keyword.to_string()) {
399 attributes.push(keyword.to_string());
400 }
401 }
402 }
403 }
404
405 if let Ok(text) = node.utf8_text(content) {
406 for keyword in [
407 "virtual",
408 "inline",
409 "constexpr",
410 "operator",
411 "override",
412 "static",
413 ] {
414 if text.contains(keyword) && !attributes.contains(&keyword.to_string()) {
415 attributes.push(keyword.to_string());
416 }
417 }
418 }
419
420 attributes
421 }
422}
423
424impl GraphBuilder for CppGraphBuilder {
425 fn language(&self) -> Language {
426 Language::Cpp
427 }
428
429 fn shape_mapping(&self) -> Option<&dyn ShapeMapping> {
430 Some(cpp_shape_mapping())
431 }
432
433 fn build_graph(
434 &self,
435 tree: &Tree,
436 content: &[u8],
437 file: &Path,
438 staging: &mut StagingGraph,
439 ) -> GraphResult<()> {
440 let mut budget = BuildBudget::new(file);
441 self.build_graph_with_budget(tree, content, file, staging, &mut budget)
442 }
443}
444
445pub struct CppShapeMapping {
454 cf_by_kind_id: Vec<Option<CfBucket>>,
455}
456
457impl CppShapeMapping {
458 fn build() -> Self {
460 let lang: tree_sitter::Language = tree_sitter_cpp::LANGUAGE.into();
461 let count = lang.node_kind_count();
462 let mut cf_by_kind_id = vec![None; count];
463 for (id, slot) in cf_by_kind_id.iter_mut().enumerate() {
464 let Ok(kind_id) = u16::try_from(id) else {
465 break;
466 };
467 if !lang.node_kind_is_named(kind_id) {
468 continue;
469 }
470 if let Some(name) = lang.node_kind_for_id(kind_id) {
471 *slot = cf_bucket_for_cpp_kind(name);
472 }
473 }
474 Self { cf_by_kind_id }
475 }
476}
477
478impl ShapeMapping for CppShapeMapping {
479 fn cf_bucket(&self, ts_node_kind_id: u16) -> Option<CfBucket> {
480 self.cf_by_kind_id
481 .get(ts_node_kind_id as usize)
482 .copied()
483 .flatten()
484 }
485
486 fn signature_shape(&self, fn_node: Node, _src: &[u8]) -> SignatureShape {
487 let mut shape = SignatureShape::default();
488 if let Some(params) = cpp_parameter_list(fn_node) {
492 let mut cursor = params.walk();
493 for child in params.named_children(&mut cursor) {
494 match child.kind() {
495 "parameter_declaration" => {
496 shape.arity_positional = shape.arity_positional.saturating_add(1);
497 }
498 "optional_parameter_declaration" => {
500 shape.arity_positional = shape.arity_positional.saturating_add(1);
501 shape.has_defaults = true;
502 }
503 "variadic_parameter_declaration" | "variadic_declarator" => {
505 shape.has_varargs = true;
506 }
507 _ => {}
508 }
509 }
510 }
511 shape.has_return_annotation = fn_node.child_by_field_name("type").is_some();
514 shape
515 }
516}
517
518fn cpp_parameter_list(fn_node: Node) -> Option<Node> {
522 let mut declarator = fn_node.child_by_field_name("declarator")?;
523 for _ in 0..8 {
526 if declarator.kind() == "function_declarator" {
527 return declarator.child_by_field_name("parameters");
528 }
529 match declarator.child_by_field_name("declarator") {
530 Some(inner) => declarator = inner,
531 None => break,
532 }
533 }
534 None
535}
536
537fn cf_bucket_for_cpp_kind(name: &str) -> Option<CfBucket> {
540 let bucket = match name {
541 "if_statement" | "conditional_expression" => CfBucket::Branch,
542 "for_statement" | "for_range_loop" | "while_statement" | "do_statement" => CfBucket::Loop,
543 "switch_statement" | "case_statement" => CfBucket::Match,
544 "try_statement" => CfBucket::Try,
545 "catch_clause" => CfBucket::Catch,
546 "throw_statement" | "throw_expression" => CfBucket::Throw,
547 "return_statement" | "co_return_statement" => CfBucket::Return,
548 "co_yield_expression" => CfBucket::Yield,
549 "co_await_expression" => CfBucket::Await,
550 "break_statement" | "continue_statement" | "goto_statement" => CfBucket::BreakContinue,
551 "call_expression" => CfBucket::Call,
552 "assignment_expression" | "init_declarator" | "declaration" => CfBucket::Assign,
553 "lambda_expression" => CfBucket::Closure,
554 _ => return None,
555 };
556 Some(bucket)
557}
558
559#[must_use]
561pub fn cpp_shape_mapping() -> &'static CppShapeMapping {
562 static MAPPING: OnceLock<CppShapeMapping> = OnceLock::new();
563 MAPPING.get_or_init(CppShapeMapping::build)
564}
565
566fn extract_namespace_map(
578 node: Node,
579 content: &[u8],
580 budget: &mut BuildBudget,
581) -> GraphResult<HashMap<std::ops::Range<usize>, String>> {
582 let mut map = HashMap::new();
583
584 let recursion_limits = sqry_core::config::RecursionLimits::load_or_default()
586 .expect("Failed to load recursion limits");
587 let file_ops_depth = recursion_limits
588 .effective_file_ops_depth()
589 .expect("Invalid file_ops_depth configuration");
590 let mut guard = sqry_core::query::security::RecursionGuard::new(file_ops_depth)
591 .expect("Failed to create recursion guard");
592
593 extract_namespaces_recursive(node, content, "", &mut map, &mut guard, budget).map_err(|e| {
594 match e {
595 timeout @ GraphBuilderError::BuildTimedOut { .. } => timeout,
596 other => GraphBuilderError::ParseError {
597 span: span_from_node(node),
598 reason: format!("C++ namespace extraction failed: {other}"),
599 },
600 }
601 })?;
602
603 Ok(map)
604}
605
606fn extract_namespaces_recursive(
612 node: Node,
613 content: &[u8],
614 current_ns: &str,
615 map: &mut HashMap<std::ops::Range<usize>, String>,
616 guard: &mut sqry_core::query::security::RecursionGuard,
617 budget: &mut BuildBudget,
618) -> GraphResult<()> {
619 budget.checkpoint("cpp:extract_namespace_map")?;
620 guard.enter().map_err(|e| GraphBuilderError::ParseError {
621 span: span_from_node(node),
622 reason: format!("C++ namespace extraction hit recursion limit: {e}"),
623 })?;
624
625 if node.kind() == "namespace_definition" {
626 let ns_name = if let Some(name_node) = node.child_by_field_name("name") {
628 extract_identifier(name_node, content)
629 } else {
630 String::from("anonymous")
632 };
633
634 let new_ns = if current_ns.is_empty() {
636 format!("{ns_name}::")
637 } else {
638 format!("{current_ns}{ns_name}::")
639 };
640
641 if let Some(body) = node.child_by_field_name("body") {
643 let range = body.start_byte()..body.end_byte();
644 map.insert(range, new_ns.clone());
645
646 let mut cursor = body.walk();
648 for child in body.children(&mut cursor) {
649 extract_namespaces_recursive(child, content, &new_ns, map, guard, budget)?;
650 }
651 }
652 } else {
653 let mut cursor = node.walk();
655 for child in node.children(&mut cursor) {
656 extract_namespaces_recursive(child, content, current_ns, map, guard, budget)?;
657 }
658 }
659
660 guard.exit();
661 Ok(())
662}
663
664fn extract_identifier(node: Node, content: &[u8]) -> String {
666 node.utf8_text(content).unwrap_or("").to_string()
667}
668
669fn find_namespace_for_offset(
671 byte_offset: usize,
672 namespace_map: &HashMap<std::ops::Range<usize>, String>,
673) -> String {
674 let mut matching_ranges: Vec<_> = namespace_map
676 .iter()
677 .filter(|(range, _)| range.contains(&byte_offset))
678 .collect();
679
680 matching_ranges.sort_by_key(|(range, _)| range.end - range.start);
682
683 matching_ranges
685 .first()
686 .map_or("", |(_, ns)| ns.as_str())
687 .to_string()
688}
689
690fn extract_cpp_contexts(
697 node: Node,
698 content: &[u8],
699 namespace_map: &HashMap<std::ops::Range<usize>, String>,
700 budget: &mut BuildBudget,
701) -> GraphResult<Vec<FunctionContext>> {
702 let mut contexts = Vec::new();
703 let mut class_stack = Vec::new();
704
705 let recursion_limits = sqry_core::config::RecursionLimits::load_or_default()
707 .expect("Failed to load recursion limits");
708 let file_ops_depth = recursion_limits
709 .effective_file_ops_depth()
710 .expect("Invalid file_ops_depth configuration");
711 let mut guard = sqry_core::query::security::RecursionGuard::new(file_ops_depth)
712 .expect("Failed to create recursion guard");
713
714 extract_contexts_recursive(
715 node,
716 content,
717 namespace_map,
718 &mut contexts,
719 &mut class_stack,
720 &mut guard,
721 budget,
722 )
723 .map_err(|e| match e {
724 timeout @ GraphBuilderError::BuildTimedOut { .. } => timeout,
725 other => GraphBuilderError::ParseError {
726 span: span_from_node(node),
727 reason: format!("C++ context extraction failed: {other}"),
728 },
729 })?;
730
731 Ok(contexts)
732}
733
734fn extract_contexts_recursive(
739 node: Node,
740 content: &[u8],
741 namespace_map: &HashMap<std::ops::Range<usize>, String>,
742 contexts: &mut Vec<FunctionContext>,
743 class_stack: &mut Vec<String>,
744 guard: &mut sqry_core::query::security::RecursionGuard,
745 budget: &mut BuildBudget,
746) -> GraphResult<()> {
747 budget.checkpoint("cpp:extract_contexts")?;
748 guard.enter().map_err(|e| GraphBuilderError::ParseError {
749 span: span_from_node(node),
750 reason: format!("C++ context extraction hit recursion limit: {e}"),
751 })?;
752
753 match node.kind() {
754 "class_specifier" | "struct_specifier" => {
755 if let Some(name_node) = node.child_by_field_name("name") {
757 let class_name = extract_identifier(name_node, content);
758 class_stack.push(class_name);
759
760 if let Some(body) = node.child_by_field_name("body") {
762 let mut cursor = body.walk();
763 for child in body.children(&mut cursor) {
764 extract_contexts_recursive(
765 child,
766 content,
767 namespace_map,
768 contexts,
769 class_stack,
770 guard,
771 budget,
772 )?;
773 }
774 }
775
776 class_stack.pop();
777 }
778 }
779
780 "function_definition" => {
781 if let Some(declarator) = node.child_by_field_name("declarator") {
783 let (func_name, class_prefix) =
784 extract_function_name_with_class(declarator, content);
785
786 let namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
788 let namespace_stack: Vec<String> = if namespace.is_empty() {
789 Vec::new()
790 } else {
791 namespace
792 .trim_end_matches("::")
793 .split("::")
794 .map(String::from)
795 .collect()
796 };
797
798 let effective_class_stack: Vec<String> = if !class_stack.is_empty() {
802 class_stack.clone()
803 } else if let Some(ref prefix) = class_prefix {
804 vec![prefix.clone()]
805 } else {
806 Vec::new()
807 };
808
809 let qualified_name =
811 build_qualified_name(&namespace_stack, &effective_class_stack, &func_name);
812
813 let is_static = is_static_function(node, content);
815 let is_virtual = is_virtual_function(node, content);
816 let is_inline = is_inline_function(node, content);
817
818 let return_type = node
820 .child_by_field_name("type")
821 .and_then(|type_node| type_node.utf8_text(content).ok())
822 .map(std::string::ToString::to_string);
823
824 let span = (node.start_byte(), node.end_byte());
826
827 contexts.push(FunctionContext {
828 qualified_name,
829 span,
830 is_static,
831 is_virtual,
832 is_inline,
833 namespace_stack,
834 class_stack: effective_class_stack,
835 return_type,
836 });
837 }
838
839 }
841
842 _ => {
843 let mut cursor = node.walk();
845 for child in node.children(&mut cursor) {
846 extract_contexts_recursive(
847 child,
848 content,
849 namespace_map,
850 contexts,
851 class_stack,
852 guard,
853 budget,
854 )?;
855 }
856 }
857 }
858
859 guard.exit();
860 Ok(())
861}
862
863#[derive(Debug, Clone, Copy, PartialEq, Eq)]
870enum TaggedSpecifierRole {
871 Definition,
874 ForwardDeclaration,
879 Reference,
884}
885
886fn classify_tagged_specifier(node: Node) -> TaggedSpecifierRole {
935 if node.child_by_field_name("body").is_some() {
936 return TaggedSpecifierRole::Definition;
937 }
938 let Some(parent) = node.parent() else {
939 return TaggedSpecifierRole::Reference;
940 };
941 if let Some(declarator) = parent.child_by_field_name("declarator")
949 && !declarator.is_missing()
950 {
951 return TaggedSpecifierRole::Reference;
952 }
953 if matches!(
954 parent.kind(),
955 "translation_unit"
956 | "declaration"
957 | "declaration_list"
958 | "compound_statement"
959 | "field_declaration"
960 | "field_declaration_list"
961 | "template_declaration"
962 | "linkage_specification"
963 | "preproc_if"
968 | "preproc_ifdef"
969 | "preproc_else"
970 | "preproc_elif"
971 | "preproc_elifdef"
972 ) {
973 TaggedSpecifierRole::ForwardDeclaration
974 } else {
975 TaggedSpecifierRole::Reference
976 }
977}
978
979fn build_qualified_name(namespace_stack: &[String], class_stack: &[String], name: &str) -> String {
980 let mut parts = Vec::new();
981
982 parts.extend(namespace_stack.iter().cloned());
984
985 for class_name in class_stack {
987 parts.push(class_name.clone());
988 }
989
990 parts.push(name.to_string());
992
993 parts.join("::")
994}
995
996fn extract_function_name_with_class(declarator: Node, content: &[u8]) -> (String, Option<String>) {
1001 match declarator.kind() {
1009 "function_declarator" => {
1010 if let Some(declarator_inner) = declarator.child_by_field_name("declarator") {
1012 extract_function_name_with_class(declarator_inner, content)
1013 } else {
1014 (extract_identifier(declarator, content), None)
1015 }
1016 }
1017 "qualified_identifier" => {
1018 let name = if let Some(name_node) = declarator.child_by_field_name("name") {
1020 extract_identifier(name_node, content)
1021 } else {
1022 extract_identifier(declarator, content)
1023 };
1024
1025 let class_prefix = declarator
1027 .child_by_field_name("scope")
1028 .map(|scope_node| extract_identifier(scope_node, content));
1029
1030 (name, class_prefix)
1031 }
1032 "field_identifier" | "identifier" | "destructor_name" | "operator_name" => {
1033 (extract_identifier(declarator, content), None)
1034 }
1035 _ => {
1036 (extract_identifier(declarator, content), None)
1038 }
1039 }
1040}
1041
1042#[allow(dead_code)]
1044fn extract_function_name(declarator: Node, content: &[u8]) -> String {
1045 extract_function_name_with_class(declarator, content).0
1046}
1047
1048fn is_static_function(node: Node, content: &[u8]) -> bool {
1050 has_specifier(node, "static", content)
1051}
1052
1053fn is_virtual_function(node: Node, content: &[u8]) -> bool {
1055 has_specifier(node, "virtual", content)
1056}
1057
1058fn is_inline_function(node: Node, content: &[u8]) -> bool {
1060 has_specifier(node, "inline", content)
1061}
1062
1063fn has_specifier(node: Node, specifier: &str, content: &[u8]) -> bool {
1065 let mut cursor = node.walk();
1067 for child in node.children(&mut cursor) {
1068 if (child.kind() == "storage_class_specifier"
1069 || child.kind() == "type_qualifier"
1070 || child.kind() == "virtual"
1071 || child.kind() == "inline")
1072 && let Ok(text) = child.utf8_text(content)
1073 && text == specifier
1074 {
1075 return true;
1076 }
1077 }
1078 false
1079}
1080
1081fn extract_field_and_type_info(
1091 node: Node,
1092 content: &[u8],
1093 namespace_map: &HashMap<std::ops::Range<usize>, String>,
1094 budget: &mut BuildBudget,
1095) -> GraphResult<(QualifiedNameMap, QualifiedNameMap)> {
1096 let mut field_types = HashMap::new();
1097 let mut type_map = HashMap::new();
1098 let mut class_stack = Vec::new();
1099
1100 let mut declared_classes: HashSet<String> = HashSet::new();
1108 let mut collect_stack: Vec<String> = Vec::new();
1109 collect_declared_class_fqns(
1110 node,
1111 content,
1112 namespace_map,
1113 &mut declared_classes,
1114 &mut collect_stack,
1115 budget,
1116 )?;
1117
1118 extract_fields_recursive(
1119 node,
1120 content,
1121 namespace_map,
1122 &declared_classes,
1123 &mut field_types,
1124 &mut type_map,
1125 &mut class_stack,
1126 budget,
1127 )?;
1128
1129 Ok((field_types, type_map))
1130}
1131
1132fn build_class_fqn(class_name: &str, namespace: &str, class_stack: &[String]) -> String {
1139 if let Some(parent_fqn) = class_stack.last() {
1140 format!("{parent_fqn}::{class_name}")
1141 } else if namespace.is_empty() {
1142 class_name.to_string()
1143 } else {
1144 format!("{}::{}", namespace.trim_end_matches("::"), class_name)
1145 }
1146}
1147
1148fn collect_declared_class_fqns(
1152 node: Node,
1153 content: &[u8],
1154 namespace_map: &HashMap<std::ops::Range<usize>, String>,
1155 declared: &mut HashSet<String>,
1156 class_stack: &mut Vec<String>,
1157 budget: &mut BuildBudget,
1158) -> GraphResult<()> {
1159 budget.checkpoint("cpp:collect_declared_classes")?;
1160 match node.kind() {
1161 "class_specifier" | "struct_specifier" => {
1162 if let Some(name_node) = node.child_by_field_name("name") {
1163 let class_name = extract_identifier(name_node, content);
1164 let namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
1165 let class_fqn = build_class_fqn(&class_name, &namespace, class_stack);
1166
1167 declared.insert(class_fqn.clone());
1168 class_stack.push(class_fqn);
1169
1170 let mut cursor = node.walk();
1171 for child in node.children(&mut cursor) {
1172 collect_declared_class_fqns(
1173 child,
1174 content,
1175 namespace_map,
1176 declared,
1177 class_stack,
1178 budget,
1179 )?;
1180 }
1181
1182 class_stack.pop();
1183 }
1184 }
1185 _ => {
1186 let mut cursor = node.walk();
1187 for child in node.children(&mut cursor) {
1188 collect_declared_class_fqns(
1189 child,
1190 content,
1191 namespace_map,
1192 declared,
1193 class_stack,
1194 budget,
1195 )?;
1196 }
1197 }
1198 }
1199 Ok(())
1200}
1201
1202fn is_cpp_primitive(name: &str) -> bool {
1206 matches!(
1207 name,
1208 "int"
1209 | "void"
1210 | "bool"
1211 | "char"
1212 | "double"
1213 | "float"
1214 | "long"
1215 | "short"
1216 | "unsigned"
1217 | "signed"
1218 | "wchar_t"
1219 | "auto"
1220 | "char8_t"
1221 | "char16_t"
1222 | "char32_t"
1223 | "int8_t"
1224 | "int16_t"
1225 | "int32_t"
1226 | "int64_t"
1227 | "uint8_t"
1228 | "uint16_t"
1229 | "uint32_t"
1230 | "uint64_t"
1231 | "intptr_t"
1232 | "uintptr_t"
1233 | "size_t"
1234 | "ssize_t"
1235 | "ptrdiff_t"
1236 )
1237}
1238
1239fn qualify_field_type(
1254 resolved_type: &str,
1255 class_fqn: &str,
1256 namespace: &str,
1257 declared_classes: &HashSet<String>,
1258) -> String {
1259 if resolved_type.contains("::") || is_cpp_primitive(resolved_type) {
1261 return resolved_type.to_string();
1262 }
1263
1264 let candidate = format!("{class_fqn}::{resolved_type}");
1266 if declared_classes.contains(&candidate) {
1267 return candidate;
1268 }
1269
1270 let mut prefix = class_fqn;
1272 while let Some(idx) = prefix.rfind("::") {
1273 prefix = &prefix[..idx];
1274 let candidate = format!("{prefix}::{resolved_type}");
1275 if declared_classes.contains(&candidate) {
1276 return candidate;
1277 }
1278 }
1279
1280 let namespace_key = namespace.trim_end_matches("::");
1282 if !namespace_key.is_empty() {
1283 let candidate = format!("{namespace_key}::{resolved_type}");
1284 if declared_classes.contains(&candidate) {
1285 return candidate;
1286 }
1287 }
1288
1289 resolved_type.to_string()
1293}
1294
1295fn extract_fields_recursive(
1297 node: Node,
1298 content: &[u8],
1299 namespace_map: &HashMap<std::ops::Range<usize>, String>,
1300 declared_classes: &HashSet<String>,
1301 field_types: &mut HashMap<(String, String), String>,
1302 type_map: &mut HashMap<(String, String), String>,
1303 class_stack: &mut Vec<String>,
1304 budget: &mut BuildBudget,
1305) -> GraphResult<()> {
1306 budget.checkpoint("cpp:extract_fields")?;
1307 match node.kind() {
1308 "class_specifier" | "struct_specifier" => {
1309 if let Some(name_node) = node.child_by_field_name("name") {
1311 let class_name = extract_identifier(name_node, content);
1312 let namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
1313
1314 let class_fqn = build_class_fqn(&class_name, &namespace, class_stack);
1316
1317 class_stack.push(class_fqn.clone());
1318
1319 let mut cursor = node.walk();
1321 for child in node.children(&mut cursor) {
1322 extract_fields_recursive(
1323 child,
1324 content,
1325 namespace_map,
1326 declared_classes,
1327 field_types,
1328 type_map,
1329 class_stack,
1330 budget,
1331 )?;
1332 }
1333
1334 class_stack.pop();
1335 }
1336 }
1337
1338 "field_declaration" => {
1339 if let Some(class_fqn) = class_stack.last() {
1341 extract_field_declaration(
1342 node,
1343 content,
1344 class_fqn,
1345 namespace_map,
1346 declared_classes,
1347 field_types,
1348 type_map,
1349 );
1350 }
1351
1352 let mut cursor = node.walk();
1357 for child in node.children(&mut cursor) {
1358 extract_fields_recursive(
1359 child,
1360 content,
1361 namespace_map,
1362 declared_classes,
1363 field_types,
1364 type_map,
1365 class_stack,
1366 budget,
1367 )?;
1368 }
1369 }
1370
1371 "using_directive" => {
1372 extract_using_directive(node, content, namespace_map, type_map);
1374 }
1375
1376 "using_declaration" => {
1377 extract_using_declaration(node, content, namespace_map, type_map);
1379 }
1380
1381 _ => {
1382 let mut cursor = node.walk();
1384 for child in node.children(&mut cursor) {
1385 extract_fields_recursive(
1386 child,
1387 content,
1388 namespace_map,
1389 declared_classes,
1390 field_types,
1391 type_map,
1392 class_stack,
1393 budget,
1394 )?;
1395 }
1396 }
1397 }
1398
1399 Ok(())
1400}
1401
1402fn extract_field_declaration(
1404 node: Node,
1405 content: &[u8],
1406 class_fqn: &str,
1407 namespace_map: &HashMap<std::ops::Range<usize>, String>,
1408 declared_classes: &HashSet<String>,
1409 field_types: &mut HashMap<(String, String), String>,
1410 type_map: &HashMap<(String, String), String>,
1411) {
1412 let mut field_type = None;
1417 let mut field_names = Vec::new();
1418
1419 let mut cursor = node.walk();
1420 for child in node.children(&mut cursor) {
1421 match child.kind() {
1422 "type_identifier" | "primitive_type" | "qualified_identifier" | "template_type" => {
1423 field_type = Some(extract_type_name(child, content));
1424 }
1425 "field_identifier" => {
1426 field_names.push(extract_identifier(child, content));
1428 }
1429 "field_declarator"
1430 | "init_declarator"
1431 | "pointer_declarator"
1432 | "reference_declarator"
1433 | "array_declarator" => {
1434 if let Some(name) = extract_field_name(child, content) {
1436 field_names.push(name);
1437 }
1438 }
1439 _ => {}
1440 }
1441 }
1442
1443 if let Some(ftype) = field_type {
1447 let namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
1448 let resolved = resolve_type_to_fqn(&ftype, &namespace, type_map);
1449 let field_type_fqn = qualify_field_type(&resolved, class_fqn, &namespace, declared_classes);
1450
1451 for fname in field_names {
1453 field_types.insert((class_fqn.to_string(), fname), field_type_fqn.clone());
1454 }
1455 }
1456}
1457
1458fn extract_type_name(type_node: Node, content: &[u8]) -> String {
1460 match type_node.kind() {
1461 "type_identifier" | "primitive_type" => extract_identifier(type_node, content),
1462 "qualified_identifier" => {
1463 extract_identifier(type_node, content)
1465 }
1466 "template_type" => {
1467 if let Some(name) = type_node.child_by_field_name("name") {
1469 extract_identifier(name, content)
1470 } else {
1471 extract_identifier(type_node, content)
1472 }
1473 }
1474 _ => {
1475 extract_identifier(type_node, content)
1477 }
1478 }
1479}
1480
1481fn extract_field_name(declarator: Node, content: &[u8]) -> Option<String> {
1483 match declarator.kind() {
1484 "field_declarator" => {
1485 if let Some(declarator_inner) = declarator.child_by_field_name("declarator") {
1487 extract_field_name(declarator_inner, content)
1488 } else {
1489 Some(extract_identifier(declarator, content))
1490 }
1491 }
1492 "field_identifier" | "identifier" => Some(extract_identifier(declarator, content)),
1493 "pointer_declarator" | "reference_declarator" | "array_declarator" => {
1494 if let Some(declarator_inner) = declarator.child_by_field_name("declarator") {
1496 extract_field_name(declarator_inner, content)
1497 } else {
1498 None
1499 }
1500 }
1501 "init_declarator" => {
1502 if let Some(declarator_inner) = declarator.child_by_field_name("declarator") {
1504 extract_field_name(declarator_inner, content)
1505 } else {
1506 None
1507 }
1508 }
1509 _ => None,
1510 }
1511}
1512
1513fn resolve_type_to_fqn(
1515 type_name: &str,
1516 namespace: &str,
1517 type_map: &HashMap<(String, String), String>,
1518) -> String {
1519 if type_name.contains("::") {
1521 return type_name.to_string();
1522 }
1523
1524 let namespace_key = namespace.trim_end_matches("::").to_string();
1526 if let Some(fqn) = type_map.get(&(namespace_key.clone(), type_name.to_string())) {
1527 return fqn.clone();
1528 }
1529
1530 if let Some(fqn) = type_map.get(&(String::new(), type_name.to_string())) {
1532 return fqn.clone();
1533 }
1534
1535 type_name.to_string()
1537}
1538
1539fn extract_using_directive(
1541 node: Node,
1542 content: &[u8],
1543 namespace_map: &HashMap<std::ops::Range<usize>, String>,
1544 _type_map: &mut HashMap<(String, String), String>,
1545) {
1546 let _namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
1550
1551 if let Some(name_node) = node.child_by_field_name("name") {
1553 let _using_ns = extract_identifier(name_node, content);
1554 }
1558}
1559
1560fn extract_using_declaration(
1565 node: Node,
1566 content: &[u8],
1567 namespace_map: &HashMap<std::ops::Range<usize>, String>,
1568 type_map: &mut HashMap<(String, String), String>,
1569) {
1570 let namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
1571 let namespace_key = namespace.trim_end_matches("::").to_string();
1572
1573 let mut cursor = node.walk();
1575 for child in node.children(&mut cursor) {
1576 if child.kind() == "qualified_identifier" || child.kind() == "identifier" {
1577 let fqn = extract_identifier(child, content);
1578
1579 if let Some(simple_name) = fqn.split("::").last() {
1581 type_map.insert((namespace_key, simple_name.to_string()), fqn);
1583 }
1584 break;
1585 }
1586 }
1587}
1588
1589fn resolve_callee_name(
1611 function_node: Node<'_>,
1612 callee_name: &str,
1613 caller_ctx: &FunctionContext,
1614 ast_graph: &ASTGraph,
1615 content: &[u8],
1616) -> String {
1617 if function_node.kind() == "field_expression" {
1619 if let Some(fqn) = resolve_member_call(function_node, caller_ctx, ast_graph, content) {
1620 return fqn;
1621 }
1622 return resolve_callee_name_namespace_prefixed(callee_name, caller_ctx);
1623 }
1624
1625 if !callee_name.starts_with("::")
1629 && callee_name.contains("::")
1630 && let Some(fqn) = resolve_static_call(function_node, callee_name, ast_graph)
1631 {
1632 return fqn;
1633 }
1634
1635 resolve_callee_name_namespace_prefixed(callee_name, caller_ctx)
1636}
1637
1638fn resolve_callee_name_namespace_prefixed(
1643 callee_name: &str,
1644 caller_ctx: &FunctionContext,
1645) -> String {
1646 if callee_name.starts_with("::") {
1648 return callee_name.trim_start_matches("::").to_string();
1649 }
1650
1651 if callee_name.contains("::") {
1653 if !caller_ctx.namespace_stack.is_empty() {
1655 let namespace_prefix = caller_ctx.namespace_stack.join("::");
1656 return format!("{namespace_prefix}::{callee_name}");
1657 }
1658 return callee_name.to_string();
1659 }
1660
1661 let mut parts = Vec::new();
1663
1664 if !caller_ctx.namespace_stack.is_empty() {
1666 parts.extend(caller_ctx.namespace_stack.iter().cloned());
1667 }
1668
1669 parts.push(callee_name.to_string());
1674
1675 parts.join("::")
1676}
1677
1678fn enclosing_class_fqn(caller_ctx: &FunctionContext) -> Option<String> {
1687 if caller_ctx.class_stack.is_empty() {
1688 return None;
1689 }
1690 let mut parts: Vec<&str> = Vec::new();
1691 parts.extend(caller_ctx.namespace_stack.iter().map(String::as_str));
1692 parts.extend(caller_ctx.class_stack.iter().map(String::as_str));
1693 Some(parts.join("::"))
1694}
1695
1696fn resolve_member_call(
1705 function_node: Node<'_>,
1706 caller_ctx: &FunctionContext,
1707 ast_graph: &ASTGraph,
1708 content: &[u8],
1709) -> Option<String> {
1710 let receiver_node = function_node.child_by_field_name("argument")?;
1711 let method_node = function_node.child_by_field_name("field")?;
1712
1713 if !matches!(receiver_node.kind(), "identifier" | "field_identifier") {
1716 return None;
1717 }
1718
1719 let receiver_text = receiver_node.utf8_text(content).ok()?.trim();
1720 let method_text = method_node.utf8_text(content).ok()?.trim();
1721 if receiver_text.is_empty() || method_text.is_empty() {
1722 return None;
1723 }
1724
1725 let class_fqn = enclosing_class_fqn(caller_ctx)?;
1726 let field_type = ast_graph
1727 .field_types
1728 .get(&(class_fqn, receiver_text.to_string()))?;
1729
1730 Some(format!("{field_type}::{method_text}"))
1731}
1732
1733fn resolve_static_call(
1739 function_node: Node<'_>,
1740 callee_name: &str,
1741 ast_graph: &ASTGraph,
1742) -> Option<String> {
1743 let (qualifier, method) = callee_name.rsplit_once("::")?;
1744 if qualifier.is_empty() || qualifier.contains("::") || method.is_empty() {
1746 return None;
1747 }
1748
1749 let namespace = find_namespace_for_offset(function_node.start_byte(), &ast_graph.namespace_map);
1750 let namespace_key = namespace.trim_end_matches("::").to_string();
1751
1752 let resolved_qualifier = ast_graph
1753 .type_map
1754 .get(&(namespace_key, qualifier.to_string()))
1755 .or_else(|| {
1756 ast_graph
1757 .type_map
1758 .get(&(String::new(), qualifier.to_string()))
1759 })?;
1760
1761 Some(format!("{resolved_qualifier}::{method}"))
1762}
1763
1764fn strip_type_qualifiers(type_text: &str) -> String {
1771 let mut result = type_text.trim().to_string();
1772
1773 result = result.replace("const ", "");
1775 result = result.replace("volatile ", "");
1776 result = result.replace("mutable ", "");
1777 result = result.replace("constexpr ", "");
1778
1779 result = result.replace(" const", "");
1781 result = result.replace(" volatile", "");
1782 result = result.replace(" mutable", "");
1783 result = result.replace(" constexpr", "");
1784
1785 result = result.replace(['*', '&'], "");
1787
1788 result = result.trim().to_string();
1790
1791 if let Some(last_part) = result.split("::").last() {
1793 result = last_part.to_string();
1794 }
1795
1796 if let Some(open_bracket) = result.find('<') {
1798 result = result[..open_bracket].to_string();
1799 }
1800
1801 result.trim().to_string()
1802}
1803
1804#[allow(clippy::unnecessary_wraps, clippy::too_many_lines)]
1820fn process_field_declaration(
1821 node: Node,
1822 content: &[u8],
1823 class_qualified_name: &str,
1824 visibility: &str,
1825 helper: &mut GraphBuildHelper,
1826) -> GraphResult<()> {
1827 let mut field_type_text = None;
1829 let mut field_names = Vec::new();
1830 let mut is_static_kw = false;
1833 let mut is_const = false;
1834 let mut is_constexpr = false;
1835
1836 let mut cursor = node.walk();
1837 for child in node.children(&mut cursor) {
1838 match child.kind() {
1839 "type_identifier" | "primitive_type" => {
1840 if let Ok(text) = child.utf8_text(content) {
1841 field_type_text = Some(text.to_string());
1842 }
1843 }
1844 "qualified_identifier" => {
1845 if let Ok(text) = child.utf8_text(content) {
1847 field_type_text = Some(text.to_string());
1848 }
1849 }
1850 "template_type" => {
1851 if let Ok(text) = child.utf8_text(content) {
1853 field_type_text = Some(text.to_string());
1854 }
1855 }
1856 "sized_type_specifier" => {
1857 if let Ok(text) = child.utf8_text(content) {
1859 field_type_text = Some(text.to_string());
1860 }
1861 }
1862 "type_qualifier" => {
1863 if let Ok(text) = child.utf8_text(content) {
1870 let trimmed = text.trim();
1871 if trimmed == "const" {
1872 is_const = true;
1873 } else if trimmed == "constexpr" {
1874 is_constexpr = true;
1875 }
1876 if field_type_text.is_none() {
1877 field_type_text = Some(text.to_string());
1878 }
1879 }
1880 }
1881 "storage_class_specifier" => {
1882 if let Ok(text) = child.utf8_text(content) {
1885 let trimmed = text.trim();
1886 if trimmed == "static" {
1887 is_static_kw = true;
1888 } else if trimmed == "constexpr" {
1889 is_constexpr = true;
1890 }
1891 }
1892 }
1893 "auto" => {
1894 field_type_text = Some("auto".to_string());
1896 }
1897 "decltype" => {
1898 if let Ok(text) = child.utf8_text(content) {
1900 field_type_text = Some(text.to_string());
1901 }
1902 }
1903 "struct_specifier" | "class_specifier" | "enum_specifier" | "union_specifier" => {
1904 if let Ok(text) = child.utf8_text(content) {
1906 field_type_text = Some(text.to_string());
1907 }
1908 }
1909 "field_identifier" => {
1910 if let Ok(name) = child.utf8_text(content) {
1911 field_names.push(name.trim().to_string());
1912 }
1913 }
1914 "field_declarator"
1915 | "pointer_declarator"
1916 | "reference_declarator"
1917 | "init_declarator" => {
1918 if let Some(name) = extract_field_name(child, content) {
1920 field_names.push(name);
1921 }
1922 }
1923 _ => {}
1924 }
1925 }
1926
1927 if let Some(type_text) = field_type_text {
1929 let base_type = strip_type_qualifiers(&type_text);
1930 let is_constant = is_const || is_constexpr;
1931
1932 for field_name in field_names {
1933 let field_qualified = format!("{class_qualified_name}.{field_name}");
1936 let span = span_from_node(node);
1937
1938 let field_id = if is_constant {
1942 helper.add_constant_with_name_static_and_visibility(
1943 &field_name,
1944 &field_qualified,
1945 Some(span),
1946 is_static_kw,
1947 Some(visibility),
1948 )
1949 } else {
1950 helper.add_property_with_name_static_and_visibility(
1951 &field_name,
1952 &field_qualified,
1953 Some(span),
1954 is_static_kw,
1955 Some(visibility),
1956 )
1957 };
1958
1959 let type_id = helper.add_type(&base_type, None);
1961
1962 helper.add_typeof_edge_with_context(
1964 field_id,
1965 type_id,
1966 Some(sqry_core::graph::unified::edge::kind::TypeOfContext::Field),
1967 None,
1968 Some(&field_name),
1969 );
1970
1971 helper.add_reference_edge(field_id, type_id);
1974 }
1975 }
1976
1977 Ok(())
1978}
1979
1980#[allow(clippy::unnecessary_wraps)]
1982fn process_global_variable_declaration(
1983 node: Node,
1984 content: &[u8],
1985 namespace_stack: &[String],
1986 helper: &mut GraphBuildHelper,
1987) -> GraphResult<()> {
1988 if node.kind() != "declaration" {
1990 return Ok(());
1991 }
1992
1993 let mut cursor_check = node.walk();
1996 for child in node.children(&mut cursor_check) {
1997 if child.kind() == "function_declarator" {
1998 return Ok(());
1999 }
2000 }
2001
2002 let mut type_text = None;
2004 let mut var_names = Vec::new();
2005
2006 let mut cursor = node.walk();
2007 for child in node.children(&mut cursor) {
2008 match child.kind() {
2009 "type_identifier" | "primitive_type" | "qualified_identifier" | "template_type" => {
2010 if let Ok(text) = child.utf8_text(content) {
2011 type_text = Some(text.to_string());
2012 }
2013 }
2014 "init_declarator" => {
2015 if let Some(declarator) = child.child_by_field_name("declarator")
2017 && let Some(name) = extract_declarator_name(declarator, content)
2018 {
2019 var_names.push(name);
2020 }
2021 }
2022 "pointer_declarator" | "reference_declarator" => {
2023 if let Some(name) = extract_declarator_name(child, content) {
2024 var_names.push(name);
2025 }
2026 }
2027 "identifier" => {
2028 if let Ok(name) = child.utf8_text(content) {
2030 var_names.push(name.to_string());
2031 }
2032 }
2033 _ => {}
2034 }
2035 }
2036
2037 if let Some(type_text) = type_text {
2038 let base_type = strip_type_qualifiers(&type_text);
2039
2040 for var_name in var_names {
2041 let qualified = if namespace_stack.is_empty() {
2043 var_name.clone()
2044 } else {
2045 format!("{}::{}", namespace_stack.join("::"), var_name)
2046 };
2047
2048 let span = span_from_node(node);
2049
2050 let var_id = helper.add_node_with_visibility(
2052 &qualified,
2053 Some(span),
2054 sqry_core::graph::unified::node::NodeKind::Variable,
2055 Some("public"),
2056 );
2057 helper.mark_definition(var_id);
2059
2060 let type_id = helper.add_type(&base_type, None);
2062
2063 helper.add_typeof_edge(var_id, type_id);
2065 helper.add_reference_edge(var_id, type_id);
2066 }
2067 }
2068
2069 Ok(())
2070}
2071
2072fn extract_declarator_name(node: Node, content: &[u8]) -> Option<String> {
2074 match node.kind() {
2075 "identifier" => {
2076 if let Ok(name) = node.utf8_text(content) {
2077 Some(name.to_string())
2078 } else {
2079 None
2080 }
2081 }
2082 "pointer_declarator" | "reference_declarator" | "array_declarator" => {
2083 if let Some(inner) = node.child_by_field_name("declarator") {
2085 extract_declarator_name(inner, content)
2086 } else {
2087 let mut cursor = node.walk();
2089 for child in node.children(&mut cursor) {
2090 if child.kind() == "identifier"
2091 && let Ok(name) = child.utf8_text(content)
2092 {
2093 return Some(name.to_string());
2094 }
2095 }
2096 None
2097 }
2098 }
2099 "init_declarator" => {
2100 if let Some(inner) = node.child_by_field_name("declarator") {
2102 extract_declarator_name(inner, content)
2103 } else {
2104 None
2105 }
2106 }
2107 "field_declarator" => {
2108 if let Some(inner) = node.child_by_field_name("declarator") {
2110 extract_declarator_name(inner, content)
2111 } else {
2112 if let Ok(name) = node.utf8_text(content) {
2114 Some(name.to_string())
2115 } else {
2116 None
2117 }
2118 }
2119 }
2120 _ => None,
2121 }
2122}
2123
2124fn resolve_elaborated_reference(
2143 helper: &GraphBuildHelper,
2144 namespace_stack: &[String],
2145 class_stack: &[String],
2146 inner_name: &str,
2147 inner_kind: NodeKind,
2148) -> String {
2149 let mut kinds = vec![inner_kind];
2152 for candidate in [NodeKind::Class, NodeKind::Struct, NodeKind::Enum] {
2153 if candidate != inner_kind {
2154 kinds.push(candidate);
2155 }
2156 }
2157
2158 for depth in (0..=class_stack.len()).rev() {
2159 let qualified = build_qualified_name(namespace_stack, &class_stack[..depth], inner_name);
2160 if kinds
2161 .iter()
2162 .any(|kind| helper.lookup_node(&qualified, *kind).is_some())
2163 {
2164 return qualified;
2165 }
2166 }
2167
2168 build_qualified_name(namespace_stack, &[], inner_name)
2169}
2170
2171fn class_body_members<'tree>(body_node: Node<'tree>) -> Vec<Node<'tree>> {
2185 fn push_members<'tree>(node: Node<'tree>, out: &mut Vec<Node<'tree>>, depth: usize) {
2186 if depth > 16 {
2189 return;
2190 }
2191 let mut cursor = node.walk();
2192 for child in node.children(&mut cursor) {
2193 if child.kind().starts_with("preproc_") {
2194 push_members(child, out, depth + 1);
2195 } else {
2196 out.push(child);
2197 }
2198 }
2199 }
2200
2201 let mut out = Vec::new();
2202 push_members(body_node, &mut out, 0);
2203 out
2204}
2205
2206#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
2208fn walk_class_body(
2209 body_node: Node,
2210 content: &[u8],
2211 class_qualified_name: &str,
2212 is_struct: bool,
2213 ast_graph: &ASTGraph,
2214 helper: &mut GraphBuildHelper,
2215 seen_includes: &mut HashSet<String>,
2216 namespace_stack: &mut Vec<String>,
2217 class_stack: &mut Vec<String>,
2218 ffi_registry: &FfiRegistry,
2219 pure_virtual_registry: &PureVirtualRegistry,
2220 budget: &mut BuildBudget,
2221) -> GraphResult<()> {
2222 let mut current_visibility = if is_struct { "public" } else { "private" };
2224
2225 for child in class_body_members(body_node) {
2226 budget.checkpoint("cpp:walk_class_body")?;
2227 match child.kind() {
2228 "access_specifier" => {
2229 if let Ok(text) = child.utf8_text(content) {
2231 let spec = text.trim().trim_end_matches(':').trim();
2232 current_visibility = spec;
2233 }
2234 }
2235 "field_declaration" => {
2236 let mut handled_nested = false;
2252 let mut inner_cursor = child.walk();
2253 for inner in child.children(&mut inner_cursor) {
2254 let kind = inner.kind();
2255 if !matches!(
2256 kind,
2257 "class_specifier"
2258 | "struct_specifier"
2259 | "union_specifier"
2260 | "enum_specifier"
2261 ) {
2262 continue;
2263 }
2264
2265 let is_struct_or_union = matches!(kind, "struct_specifier" | "union_specifier");
2266
2267 if let Some(name_node) = inner.child_by_field_name("name") {
2268 if let Ok(inner_name) = name_node.utf8_text(content) {
2280 let inner_name = inner_name.trim();
2281 let nested_qualified = format!("{class_qualified_name}::{inner_name}");
2282 let nested_span = span_from_node(inner);
2283
2284 let inner_kind = if kind == "enum_specifier" {
2285 NodeKind::Enum
2286 } else if is_struct_or_union {
2287 NodeKind::Struct
2288 } else {
2289 NodeKind::Class
2290 };
2291 let role = classify_tagged_specifier(inner);
2292
2293 if role == TaggedSpecifierRole::Reference {
2308 let referenced_qualified = resolve_elaborated_reference(
2309 helper,
2310 namespace_stack,
2311 class_stack,
2312 inner_name,
2313 inner_kind,
2314 );
2315 helper.add_call_site_node(
2316 &referenced_qualified,
2317 nested_span,
2318 inner_kind,
2319 );
2320 continue;
2321 }
2322
2323 if role == TaggedSpecifierRole::ForwardDeclaration {
2328 helper.add_bodyless_declaration_node(
2329 &nested_qualified,
2330 nested_span,
2331 inner_kind,
2332 Some(current_visibility),
2333 );
2334 continue;
2335 }
2336
2337 if kind == "enum_specifier" {
2341 helper.add_enum_with_visibility(
2345 &nested_qualified,
2346 Some(nested_span),
2347 Some(current_visibility),
2348 );
2349 } else {
2350 let nested_id = if is_struct_or_union {
2351 helper.add_struct_with_visibility(
2352 &nested_qualified,
2353 Some(nested_span),
2354 Some(current_visibility),
2355 )
2356 } else {
2357 helper.add_class_with_visibility(
2358 &nested_qualified,
2359 Some(nested_span),
2360 Some(current_visibility),
2361 )
2362 };
2363 build_inheritance_and_implements_edges(
2364 inner,
2365 content,
2366 &nested_qualified,
2367 nested_id,
2368 helper,
2369 namespace_stack,
2370 pure_virtual_registry,
2371 )?;
2372 }
2373
2374 if matches!(
2379 kind,
2380 "class_specifier" | "struct_specifier" | "union_specifier"
2381 ) && let Some(body) = inner.child_by_field_name("body")
2382 {
2383 walk_class_body(
2384 body,
2385 content,
2386 &nested_qualified,
2387 is_struct_or_union,
2388 ast_graph,
2389 helper,
2390 seen_includes,
2391 namespace_stack,
2392 class_stack,
2393 ffi_registry,
2394 pure_virtual_registry,
2395 budget,
2396 )?;
2397 }
2398 handled_nested = true;
2399 }
2400 } else if let Some(body) = inner.child_by_field_name("body") {
2401 let mut anon_cursor = body.walk();
2406 for anon_child in body.children(&mut anon_cursor) {
2407 if anon_child.kind() == "field_declaration" {
2408 process_field_declaration(
2409 anon_child,
2410 content,
2411 class_qualified_name,
2412 current_visibility,
2413 helper,
2414 )?;
2415 }
2416 }
2417 handled_nested = true;
2418 }
2419 }
2420
2421 let _ = handled_nested;
2434 process_field_declaration(
2435 child,
2436 content,
2437 class_qualified_name,
2438 current_visibility,
2439 helper,
2440 )?;
2441 }
2442 "function_definition" => {
2443 if let Some(context) = ast_graph.context_for_start(child.start_byte()) {
2446 let span = span_from_node(child);
2447 helper.add_method_with_signature(
2448 &context.qualified_name,
2449 Some(span),
2450 false, context.is_static,
2452 Some(current_visibility),
2453 context.return_type.as_deref(),
2454 );
2455 }
2456 walk_tree_for_graph(
2458 child,
2459 content,
2460 ast_graph,
2461 helper,
2462 seen_includes,
2463 namespace_stack,
2464 class_stack,
2465 ffi_registry,
2466 pure_virtual_registry,
2467 budget,
2468 )?;
2469 }
2470 _ => {
2471 walk_tree_for_graph(
2473 child,
2474 content,
2475 ast_graph,
2476 helper,
2477 seen_includes,
2478 namespace_stack,
2479 class_stack,
2480 ffi_registry,
2481 pure_virtual_registry,
2482 budget,
2483 )?;
2484 }
2485 }
2486 }
2487
2488 Ok(())
2489}
2490
2491#[allow(clippy::too_many_arguments)]
2493#[allow(clippy::too_many_lines)] fn walk_tree_for_graph(
2495 node: Node,
2496 content: &[u8],
2497 ast_graph: &ASTGraph,
2498 helper: &mut GraphBuildHelper,
2499 seen_includes: &mut HashSet<String>,
2500 namespace_stack: &mut Vec<String>,
2501 class_stack: &mut Vec<String>,
2502 ffi_registry: &FfiRegistry,
2503 pure_virtual_registry: &PureVirtualRegistry,
2504 budget: &mut BuildBudget,
2505) -> GraphResult<()> {
2506 budget.checkpoint("cpp:walk_tree_for_graph")?;
2507 match node.kind() {
2508 "preproc_include" => {
2509 build_import_edge(node, content, helper, seen_includes)?;
2511 }
2512 "linkage_specification" => {
2513 build_ffi_block_for_staging(node, content, helper, namespace_stack);
2515 }
2516 "namespace_definition" => {
2517 if let Some(name_node) = node.child_by_field_name("name")
2519 && let Ok(ns_name) = name_node.utf8_text(content)
2520 {
2521 namespace_stack.push(ns_name.trim().to_string());
2522
2523 let mut cursor = node.walk();
2525 for child in node.children(&mut cursor) {
2526 walk_tree_for_graph(
2527 child,
2528 content,
2529 ast_graph,
2530 helper,
2531 seen_includes,
2532 namespace_stack,
2533 class_stack,
2534 ffi_registry,
2535 pure_virtual_registry,
2536 budget,
2537 )?;
2538 }
2539
2540 namespace_stack.pop();
2541 return Ok(());
2542 }
2543 }
2544 "class_specifier" | "struct_specifier" | "union_specifier" => {
2545 if let Some(name_node) = node.child_by_field_name("name")
2547 && let Ok(class_name) = name_node.utf8_text(content)
2548 {
2549 let class_name = class_name.trim();
2550 let span = span_from_node(node);
2551 let is_struct = matches!(node.kind(), "struct_specifier" | "union_specifier");
2554
2555 let qualified_class =
2557 build_qualified_name(namespace_stack, class_stack, class_name);
2558
2559 let role = classify_tagged_specifier(node);
2567 let node_kind = if is_struct {
2568 NodeKind::Struct
2569 } else {
2570 NodeKind::Class
2571 };
2572 let class_id = match role {
2573 TaggedSpecifierRole::Reference => {
2574 helper.add_call_site_node(&qualified_class, span, node_kind)
2575 }
2576 TaggedSpecifierRole::ForwardDeclaration => helper
2577 .add_bodyless_declaration_node(
2578 &qualified_class,
2579 span,
2580 node_kind,
2581 Some("public"),
2582 ),
2583 TaggedSpecifierRole::Definition => {
2584 if is_struct {
2585 helper.add_struct_with_visibility(
2587 &qualified_class,
2588 Some(span),
2589 Some("public"),
2590 )
2591 } else {
2592 helper.add_class_with_visibility(
2593 &qualified_class,
2594 Some(span),
2595 Some("public"),
2596 )
2597 }
2598 }
2599 };
2600
2601 build_inheritance_and_implements_edges(
2604 node,
2605 content,
2606 &qualified_class,
2607 class_id,
2608 helper,
2609 namespace_stack,
2610 pure_virtual_registry,
2611 )?;
2612
2613 if class_stack.is_empty() && role == TaggedSpecifierRole::Definition {
2620 let module_id = helper.add_module(FILE_MODULE_NAME, None);
2621 helper.add_export_edge(module_id, class_id);
2622 }
2623
2624 class_stack.push(class_name.to_string());
2626
2627 if let Some(body) = node.child_by_field_name("body") {
2630 walk_class_body(
2631 body,
2632 content,
2633 &qualified_class,
2634 is_struct,
2635 ast_graph,
2636 helper,
2637 seen_includes,
2638 namespace_stack,
2639 class_stack,
2640 ffi_registry,
2641 pure_virtual_registry,
2642 budget,
2643 )?;
2644 }
2645
2646 class_stack.pop();
2647 return Ok(());
2648 }
2649 }
2650 "enum_specifier" => {
2651 if let Some(name_node) = node.child_by_field_name("name")
2652 && let Ok(enum_name) = name_node.utf8_text(content)
2653 {
2654 let enum_name = enum_name.trim();
2655 let span = span_from_node(node);
2656 let qualified_enum = build_qualified_name(namespace_stack, class_stack, enum_name);
2657
2658 match classify_tagged_specifier(node) {
2666 TaggedSpecifierRole::Reference => {
2667 helper.add_call_site_node(&qualified_enum, span, NodeKind::Enum);
2668 }
2669 TaggedSpecifierRole::ForwardDeclaration => {
2670 helper.add_bodyless_declaration_node(
2671 &qualified_enum,
2672 span,
2673 NodeKind::Enum,
2674 None,
2675 );
2676 }
2677 TaggedSpecifierRole::Definition => {
2678 let enum_id = helper.add_enum(&qualified_enum, Some(span));
2679
2680 if class_stack.is_empty() {
2683 let module_id = helper.add_module(FILE_MODULE_NAME, None);
2684 helper.add_export_edge(module_id, enum_id);
2685 }
2686 }
2687 }
2688 }
2689 }
2690 "function_definition" => {
2691 if !class_stack.is_empty() {
2695 let mut cursor = node.walk();
2698 for child in node.children(&mut cursor) {
2699 walk_tree_for_graph(
2700 child,
2701 content,
2702 ast_graph,
2703 helper,
2704 seen_includes,
2705 namespace_stack,
2706 class_stack,
2707 ffi_registry,
2708 pure_virtual_registry,
2709 budget,
2710 )?;
2711 }
2712 return Ok(());
2713 }
2714
2715 if let Some(context) = ast_graph.context_for_start(node.start_byte()) {
2717 let span = span_from_node(node);
2718
2719 if context.class_stack.is_empty() {
2721 let visibility = if context.is_static {
2724 "private"
2725 } else {
2726 "public"
2727 };
2728 let fn_id = helper.add_function_with_signature(
2729 &context.qualified_name,
2730 Some(span),
2731 false, false, Some(visibility),
2734 context.return_type.as_deref(),
2735 );
2736
2737 if !context.is_static {
2739 let module_id = helper.add_module(FILE_MODULE_NAME, None);
2740 helper.add_export_edge(module_id, fn_id);
2741 }
2742 } else {
2743 helper.add_method_with_signature(
2748 &context.qualified_name,
2749 Some(span),
2750 false, context.is_static,
2752 Some("public"), context.return_type.as_deref(),
2754 );
2755 }
2756 }
2757 }
2758 "call_expression" => {
2759 if let Ok(Some((caller_qname, callee_qname, argument_count, span))) =
2761 build_call_for_staging(ast_graph, node, content)
2762 {
2763 let caller_function_id =
2765 helper.ensure_callee(&caller_qname, span, CalleeKindHint::Function);
2766 let argument_count = u8::try_from(argument_count).unwrap_or(u8::MAX);
2767
2768 let is_unqualified = !callee_qname.contains("::");
2771 if is_unqualified {
2772 if let Some((ffi_qualified, ffi_convention)) = ffi_registry.get(&callee_qname) {
2773 let ffi_target_id =
2775 helper.ensure_callee(ffi_qualified, span, CalleeKindHint::Function);
2776 helper.add_ffi_edge(caller_function_id, ffi_target_id, *ffi_convention);
2777 } else {
2778 let target_function_id =
2780 helper.ensure_callee(&callee_qname, span, CalleeKindHint::Function);
2781 helper.add_call_edge_full_with_span(
2782 caller_function_id,
2783 target_function_id,
2784 argument_count,
2785 false,
2786 vec![span],
2787 );
2788 }
2789 } else {
2790 let target_function_id =
2792 helper.ensure_callee(&callee_qname, span, CalleeKindHint::Function);
2793 helper.add_call_edge_full_with_span(
2794 caller_function_id,
2795 target_function_id,
2796 argument_count,
2797 false,
2798 vec![span],
2799 );
2800 }
2801 }
2802 }
2803 "declaration" => {
2804 if class_stack.is_empty() {
2807 process_global_variable_declaration(node, content, namespace_stack, helper)?;
2808 }
2809 }
2810 _ => {}
2811 }
2812
2813 let mut cursor = node.walk();
2815 for child in node.children(&mut cursor) {
2816 walk_tree_for_graph(
2817 child,
2818 content,
2819 ast_graph,
2820 helper,
2821 seen_includes,
2822 namespace_stack,
2823 class_stack,
2824 ffi_registry,
2825 pure_virtual_registry,
2826 budget,
2827 )?;
2828 }
2829
2830 Ok(())
2831}
2832
2833fn build_call_for_staging(
2835 ast_graph: &ASTGraph,
2836 call_node: Node<'_>,
2837 content: &[u8],
2838) -> GraphResult<Option<(String, String, usize, Span)>> {
2839 let call_context = ast_graph.find_enclosing(call_node.start_byte());
2841 let caller_qualified_name = if let Some(ctx) = call_context {
2842 ctx.qualified_name.clone()
2843 } else {
2844 return Ok(None);
2846 };
2847
2848 let Some(function_node) = call_node.child_by_field_name("function") else {
2849 return Ok(None);
2850 };
2851
2852 let callee_text = function_node
2853 .utf8_text(content)
2854 .map_err(|_| GraphBuilderError::ParseError {
2855 span: span_from_node(call_node),
2856 reason: "failed to read call expression".to_string(),
2857 })?
2858 .trim();
2859
2860 if callee_text.is_empty() {
2861 return Ok(None);
2862 }
2863
2864 let target_qualified_name = if let Some(ctx) = call_context {
2866 resolve_callee_name(function_node, callee_text, ctx, ast_graph, content)
2867 } else {
2868 callee_text.to_string()
2869 };
2870
2871 let span = span_from_node(call_node);
2872 let argument_count = count_arguments(call_node);
2873
2874 Ok(Some((
2875 caller_qualified_name,
2876 target_qualified_name,
2877 argument_count,
2878 span,
2879 )))
2880}
2881
2882fn build_import_edge(
2889 include_node: Node<'_>,
2890 content: &[u8],
2891 helper: &mut GraphBuildHelper,
2892 seen_includes: &mut HashSet<String>,
2893) -> GraphResult<()> {
2894 let path_node = include_node.child_by_field_name("path").or_else(|| {
2896 let mut cursor = include_node.walk();
2898 include_node.children(&mut cursor).find(|child| {
2899 matches!(
2900 child.kind(),
2901 "system_lib_string" | "string_literal" | "string_content"
2902 )
2903 })
2904 });
2905
2906 let Some(path_node) = path_node else {
2907 return Ok(());
2908 };
2909
2910 let include_path = path_node
2911 .utf8_text(content)
2912 .map_err(|_| GraphBuilderError::ParseError {
2913 span: span_from_node(include_node),
2914 reason: "failed to read include path".to_string(),
2915 })?
2916 .trim();
2917
2918 if include_path.is_empty() {
2919 return Ok(());
2920 }
2921
2922 let is_system_include = include_path.starts_with('<') && include_path.ends_with('>');
2924 let cleaned_path = if is_system_include {
2925 include_path.trim_start_matches('<').trim_end_matches('>')
2927 } else {
2928 include_path.trim_start_matches('"').trim_end_matches('"')
2930 };
2931
2932 if cleaned_path.is_empty() {
2933 return Ok(());
2934 }
2935
2936 if !seen_includes.insert(cleaned_path.to_string()) {
2938 return Ok(()); }
2940
2941 let file_module_id = helper.add_module("<file>", None);
2943
2944 let span = span_from_node(include_node);
2946 let import_id = helper.add_import(cleaned_path, Some(span));
2947
2948 helper.add_import_edge(file_module_id, import_id);
2951
2952 Ok(())
2953}
2954
2955fn collect_ffi_declarations(
2966 node: Node<'_>,
2967 content: &[u8],
2968 ffi_registry: &mut FfiRegistry,
2969 budget: &mut BuildBudget,
2970) -> GraphResult<()> {
2971 budget.checkpoint("cpp:collect_ffi_declarations")?;
2972 if node.kind() == "linkage_specification" {
2973 let abi = extract_ffi_abi(node, content);
2975 let convention = abi_to_convention(&abi);
2976
2977 if let Some(body_node) = node.child_by_field_name("body") {
2979 collect_ffi_from_body(body_node, content, &abi, convention, ffi_registry);
2980 }
2981 }
2982
2983 let mut cursor = node.walk();
2985 for child in node.children(&mut cursor) {
2986 collect_ffi_declarations(child, content, ffi_registry, budget)?;
2987 }
2988
2989 Ok(())
2990}
2991
2992fn collect_ffi_from_body(
2994 body_node: Node<'_>,
2995 content: &[u8],
2996 abi: &str,
2997 convention: FfiConvention,
2998 ffi_registry: &mut FfiRegistry,
2999) {
3000 match body_node.kind() {
3001 "declaration_list" => {
3002 let mut cursor = body_node.walk();
3004 for decl in body_node.children(&mut cursor) {
3005 if decl.kind() == "declaration"
3006 && let Some(fn_name) = extract_ffi_function_name(decl, content)
3007 {
3008 let qualified = format!("extern::{abi}::{fn_name}");
3009 ffi_registry.insert(fn_name, (qualified, convention));
3010 }
3011 }
3012 }
3013 "declaration" => {
3014 if let Some(fn_name) = extract_ffi_function_name(body_node, content) {
3016 let qualified = format!("extern::{abi}::{fn_name}");
3017 ffi_registry.insert(fn_name, (qualified, convention));
3018 }
3019 }
3020 _ => {}
3021 }
3022}
3023
3024fn extract_ffi_function_name(decl_node: Node<'_>, content: &[u8]) -> Option<String> {
3026 if let Some(declarator_node) = decl_node.child_by_field_name("declarator") {
3028 return extract_function_name_from_declarator(declarator_node, content);
3029 }
3030 None
3031}
3032
3033fn extract_function_name_from_declarator(node: Node<'_>, content: &[u8]) -> Option<String> {
3035 match node.kind() {
3036 "function_declarator" => {
3037 if let Some(inner) = node.child_by_field_name("declarator") {
3039 return extract_function_name_from_declarator(inner, content);
3040 }
3041 }
3042 "identifier" => {
3043 if let Ok(name) = node.utf8_text(content) {
3045 let name = name.trim();
3046 if !name.is_empty() {
3047 return Some(name.to_string());
3048 }
3049 }
3050 }
3051 "pointer_declarator" | "reference_declarator" => {
3052 if let Some(inner) = node.child_by_field_name("declarator") {
3054 return extract_function_name_from_declarator(inner, content);
3055 }
3056 }
3057 "parenthesized_declarator" => {
3058 let mut cursor = node.walk();
3060 for child in node.children(&mut cursor) {
3061 if let Some(name) = extract_function_name_from_declarator(child, content) {
3062 return Some(name);
3063 }
3064 }
3065 }
3066 _ => {}
3067 }
3068 None
3069}
3070
3071fn extract_ffi_abi(node: Node<'_>, content: &[u8]) -> String {
3075 if let Some(value_node) = node.child_by_field_name("value")
3077 && value_node.kind() == "string_literal"
3078 {
3079 let mut cursor = value_node.walk();
3081 for child in value_node.children(&mut cursor) {
3082 if child.kind() == "string_content"
3083 && let Ok(text) = child.utf8_text(content)
3084 {
3085 let trimmed = text.trim();
3086 if !trimmed.is_empty() {
3087 return trimmed.to_string();
3088 }
3089 }
3090 }
3091 }
3092 "C".to_string()
3094}
3095
3096fn abi_to_convention(abi: &str) -> FfiConvention {
3098 match abi.to_lowercase().as_str() {
3099 "system" => FfiConvention::System,
3100 "stdcall" => FfiConvention::Stdcall,
3101 "fastcall" => FfiConvention::Fastcall,
3102 "cdecl" => FfiConvention::Cdecl,
3103 _ => FfiConvention::C, }
3105}
3106
3107fn build_ffi_block_for_staging(
3111 node: Node<'_>,
3112 content: &[u8],
3113 helper: &mut GraphBuildHelper,
3114 namespace_stack: &[String],
3115) {
3116 let abi = extract_ffi_abi(node, content);
3118
3119 if let Some(body_node) = node.child_by_field_name("body") {
3121 build_ffi_from_body(body_node, content, &abi, helper, namespace_stack);
3122 }
3123}
3124
3125fn build_ffi_from_body(
3127 body_node: Node<'_>,
3128 content: &[u8],
3129 abi: &str,
3130 helper: &mut GraphBuildHelper,
3131 namespace_stack: &[String],
3132) {
3133 match body_node.kind() {
3134 "declaration_list" => {
3135 let mut cursor = body_node.walk();
3137 for decl in body_node.children(&mut cursor) {
3138 if decl.kind() == "declaration"
3139 && let Some(fn_name) = extract_ffi_function_name(decl, content)
3140 {
3141 let span = span_from_node(decl);
3142 let qualified = if namespace_stack.is_empty() {
3144 format!("extern::{abi}::{fn_name}")
3145 } else {
3146 format!("{}::extern::{abi}::{fn_name}", namespace_stack.join("::"))
3147 };
3148 helper.add_function(
3150 &qualified,
3151 Some(span),
3152 false, true, );
3155 }
3156 }
3157 }
3158 "declaration" => {
3159 if let Some(fn_name) = extract_ffi_function_name(body_node, content) {
3161 let span = span_from_node(body_node);
3162 let qualified = if namespace_stack.is_empty() {
3163 format!("extern::{abi}::{fn_name}")
3164 } else {
3165 format!("{}::extern::{abi}::{fn_name}", namespace_stack.join("::"))
3166 };
3167 helper.add_function(&qualified, Some(span), false, true);
3168 }
3169 }
3170 _ => {}
3171 }
3172}
3173
3174fn collect_pure_virtual_interfaces(
3184 node: Node<'_>,
3185 content: &[u8],
3186 registry: &mut PureVirtualRegistry,
3187 budget: &mut BuildBudget,
3188) -> GraphResult<()> {
3189 budget.checkpoint("cpp:collect_pure_virtual_interfaces")?;
3190 if matches!(node.kind(), "class_specifier" | "struct_specifier")
3191 && let Some(name_node) = node.child_by_field_name("name")
3192 && let Ok(class_name) = name_node.utf8_text(content)
3193 {
3194 let class_name = class_name.trim();
3195 if !class_name.is_empty() && has_pure_virtual_methods(node, content) {
3196 registry.insert(class_name.to_string());
3197 }
3198 }
3199
3200 let mut cursor = node.walk();
3202 for child in node.children(&mut cursor) {
3203 collect_pure_virtual_interfaces(child, content, registry, budget)?;
3204 }
3205
3206 Ok(())
3207}
3208
3209fn has_pure_virtual_methods(class_node: Node<'_>, content: &[u8]) -> bool {
3213 if let Some(body) = class_node.child_by_field_name("body") {
3214 let mut cursor = body.walk();
3215 for child in body.children(&mut cursor) {
3216 if child.kind() == "field_declaration" && is_pure_virtual_declaration(child, content) {
3218 return true;
3219 }
3220 }
3221 }
3222 false
3223}
3224
3225fn is_pure_virtual_declaration(decl_node: Node<'_>, content: &[u8]) -> bool {
3227 let mut has_virtual = false;
3228 let mut has_pure_specifier = false;
3229
3230 let mut cursor = decl_node.walk();
3232 for child in decl_node.children(&mut cursor) {
3233 match child.kind() {
3234 "virtual" => {
3235 has_virtual = true;
3236 }
3237 "number_literal" => {
3238 if let Ok(text) = child.utf8_text(content)
3241 && text.trim() == "0"
3242 {
3243 has_pure_specifier = true;
3244 }
3245 }
3246 _ => {}
3247 }
3248 }
3249
3250 has_virtual && has_pure_specifier
3251}
3252
3253fn build_inheritance_and_implements_edges(
3259 class_node: Node<'_>,
3260 content: &[u8],
3261 _qualified_class_name: &str,
3262 child_id: sqry_core::graph::unified::node::NodeId,
3263 helper: &mut GraphBuildHelper,
3264 namespace_stack: &[String],
3265 pure_virtual_registry: &PureVirtualRegistry,
3266) -> GraphResult<()> {
3267 let mut cursor = class_node.walk();
3269 let base_clause = class_node
3270 .children(&mut cursor)
3271 .find(|child| child.kind() == "base_class_clause");
3272
3273 let Some(base_clause) = base_clause else {
3274 return Ok(()); };
3276
3277 let mut clause_cursor = base_clause.walk();
3279 for child in base_clause.children(&mut clause_cursor) {
3280 match child.kind() {
3281 "type_identifier" => {
3282 let base_name = child
3283 .utf8_text(content)
3284 .map_err(|_| GraphBuilderError::ParseError {
3285 span: span_from_node(child),
3286 reason: "failed to read base class name".to_string(),
3287 })?
3288 .trim();
3289
3290 if !base_name.is_empty() {
3291 let qualified_base = if namespace_stack.is_empty() {
3293 base_name.to_string()
3294 } else {
3295 format!("{}::{}", namespace_stack.join("::"), base_name)
3296 };
3297
3298 if pure_virtual_registry.contains(base_name) {
3300 let interface_id = helper.add_interface(&qualified_base, None);
3302 helper.add_implements_edge(child_id, interface_id);
3303 } else {
3304 let parent_id = helper.add_class(&qualified_base, None);
3306 helper.add_inherits_edge(child_id, parent_id);
3307 }
3308 }
3309 }
3310 "qualified_identifier" => {
3311 let base_name = child
3313 .utf8_text(content)
3314 .map_err(|_| GraphBuilderError::ParseError {
3315 span: span_from_node(child),
3316 reason: "failed to read base class name".to_string(),
3317 })?
3318 .trim();
3319
3320 if !base_name.is_empty() {
3321 let simple_name = base_name.rsplit("::").next().unwrap_or(base_name);
3323
3324 if pure_virtual_registry.contains(simple_name) {
3325 let interface_id = helper.add_interface(base_name, None);
3326 helper.add_implements_edge(child_id, interface_id);
3327 } else {
3328 let parent_id = helper.add_class(base_name, None);
3329 helper.add_inherits_edge(child_id, parent_id);
3330 }
3331 }
3332 }
3333 "template_type" => {
3334 if let Some(template_name_node) = child.child_by_field_name("name")
3336 && let Ok(base_name) = template_name_node.utf8_text(content)
3337 {
3338 let base_name = base_name.trim();
3339 if !base_name.is_empty() {
3340 let qualified_base =
3341 if base_name.contains("::") || namespace_stack.is_empty() {
3342 base_name.to_string()
3343 } else {
3344 format!("{}::{}", namespace_stack.join("::"), base_name)
3345 };
3346
3347 if pure_virtual_registry.contains(base_name) {
3350 let interface_id = helper.add_interface(&qualified_base, None);
3351 helper.add_implements_edge(child_id, interface_id);
3352 } else {
3353 let parent_id = helper.add_class(&qualified_base, None);
3354 helper.add_inherits_edge(child_id, parent_id);
3355 }
3356 }
3357 }
3358 }
3359 _ => {
3360 }
3362 }
3363 }
3364
3365 Ok(())
3366}
3367
3368fn span_from_node(node: Node<'_>) -> Span {
3369 let start = node.start_position();
3370 let end = node.end_position();
3371 Span::new(
3372 sqry_core::graph::node::Position::new(start.row, start.column),
3373 sqry_core::graph::node::Position::new(end.row, end.column),
3374 )
3375}
3376
3377fn count_arguments(node: Node<'_>) -> usize {
3378 node.child_by_field_name("arguments").map_or(0, |args| {
3379 let mut count = 0;
3380 let mut cursor = args.walk();
3381 for child in args.children(&mut cursor) {
3382 if !matches!(child.kind(), "(" | ")" | ",") {
3383 count += 1;
3384 }
3385 }
3386 count
3387 })
3388}
3389
3390#[cfg(test)]
3391mod tests {
3392 use super::*;
3393 use sqry_core::graph::unified::build::test_helpers::{
3394 assert_has_ffi_call_edge, assert_has_node, assert_has_node_with_kind,
3395 assert_has_node_with_kind_exact, collect_call_edges,
3396 };
3397 use sqry_core::graph::unified::node::NodeKind;
3398 use tree_sitter::Parser;
3399
3400 fn parse_cpp(source: &str) -> Tree {
3401 let mut parser = Parser::new();
3402 parser
3403 .set_language(&tree_sitter_cpp::LANGUAGE.into())
3404 .expect("Failed to set Cpp language");
3405 parser
3406 .parse(source.as_bytes(), None)
3407 .expect("Failed to parse Cpp source")
3408 }
3409
3410 fn test_budget() -> BuildBudget {
3411 BuildBudget::new(Path::new("test.cpp"))
3412 }
3413
3414 fn extract_namespace_map_for_test(
3415 tree: &Tree,
3416 source: &str,
3417 ) -> HashMap<std::ops::Range<usize>, String> {
3418 let mut budget = test_budget();
3419 extract_namespace_map(tree.root_node(), source.as_bytes(), &mut budget)
3420 .expect("namespace extraction should succeed in tests")
3421 }
3422
3423 fn extract_cpp_contexts_for_test(
3424 tree: &Tree,
3425 source: &str,
3426 namespace_map: &HashMap<std::ops::Range<usize>, String>,
3427 ) -> Vec<FunctionContext> {
3428 let mut budget = test_budget();
3429 extract_cpp_contexts(
3430 tree.root_node(),
3431 source.as_bytes(),
3432 namespace_map,
3433 &mut budget,
3434 )
3435 .expect("context extraction should succeed in tests")
3436 }
3437
3438 fn extract_field_and_type_info_for_test(
3439 tree: &Tree,
3440 source: &str,
3441 namespace_map: &HashMap<std::ops::Range<usize>, String>,
3442 ) -> (QualifiedNameMap, QualifiedNameMap) {
3443 let mut budget = test_budget();
3444 extract_field_and_type_info(
3445 tree.root_node(),
3446 source.as_bytes(),
3447 namespace_map,
3448 &mut budget,
3449 )
3450 .expect("field/type extraction should succeed in tests")
3451 }
3452
3453 #[test]
3454 fn test_build_graph_times_out_with_expired_budget() {
3455 let source = r"
3456 namespace demo {
3457 class Service {
3458 public:
3459 void process() {}
3460 };
3461 }
3462 ";
3463 let tree = parse_cpp(source);
3464 let builder = CppGraphBuilder::new();
3465 let mut staging = StagingGraph::new();
3466 let mut budget = BuildBudget::already_expired(Path::new("timeout.cpp"));
3467
3468 let err = builder
3469 .build_graph_with_budget(
3470 &tree,
3471 source.as_bytes(),
3472 Path::new("timeout.cpp"),
3473 &mut staging,
3474 &mut budget,
3475 )
3476 .expect_err("expired budget should force timeout");
3477
3478 match err {
3479 GraphBuilderError::BuildTimedOut {
3480 file,
3481 phase,
3482 timeout_ms,
3483 } => {
3484 assert_eq!(file, PathBuf::from("timeout.cpp"));
3485 assert_eq!(phase, "cpp:extract_namespace_map");
3486 assert_eq!(timeout_ms, 1_000);
3487 }
3488 other => panic!("expected BuildTimedOut, got {other:?}"),
3489 }
3490 }
3491
3492 #[test]
3493 fn test_extract_class() {
3494 let source = "class User { }";
3495 let tree = parse_cpp(source);
3496 let mut staging = StagingGraph::new();
3497 let builder = CppGraphBuilder::new();
3498
3499 let result = builder.build_graph(
3500 &tree,
3501 source.as_bytes(),
3502 Path::new("test.cpp"),
3503 &mut staging,
3504 );
3505
3506 assert!(result.is_ok());
3507 assert_has_node_with_kind(&staging, "User", NodeKind::Class);
3508 }
3509
3510 #[test]
3511 fn test_extract_template_class() {
3512 let source = r"
3513 template <typename T>
3514 class Person {
3515 public:
3516 T name;
3517 T age;
3518 };
3519 ";
3520 let tree = parse_cpp(source);
3521 let mut staging = StagingGraph::new();
3522 let builder = CppGraphBuilder::new();
3523
3524 let result = builder.build_graph(
3525 &tree,
3526 source.as_bytes(),
3527 Path::new("test.cpp"),
3528 &mut staging,
3529 );
3530
3531 assert!(result.is_ok());
3532 assert_has_node_with_kind(&staging, "Person", NodeKind::Class);
3533 }
3534
3535 #[test]
3536 fn test_nested_named_types_emit_nodes() {
3537 let source = r"
3542 class Outer {
3543 public:
3544 class Inner { int z; };
3545 struct InnerS { int w; };
3546 union InnerU { int i; float f; };
3547 enum class InnerE { A, B };
3548 class L1 { public: class L2 { int q; }; };
3549 };
3550 namespace ns {
3551 class NsOuter { public: class NsInner { int n; }; };
3552 }
3553 ";
3554 let staging = build_cpp(source);
3555
3556 assert_has_node_with_kind_exact(&staging, "Outer::Inner", NodeKind::Class);
3558 assert_has_node_with_kind_exact(&staging, "Outer::InnerS", NodeKind::Struct);
3559 assert_has_node_with_kind_exact(&staging, "Outer::InnerU", NodeKind::Struct);
3561 assert_has_node_with_kind_exact(&staging, "Outer::InnerE", NodeKind::Enum);
3562 assert_has_node_with_kind_exact(&staging, "Outer::L1", NodeKind::Class);
3564 assert_has_node_with_kind_exact(&staging, "Outer::L1::L2", NodeKind::Class);
3565 assert_has_node_with_kind_exact(&staging, "ns::NsOuter", NodeKind::Class);
3567 assert_has_node_with_kind_exact(&staging, "ns::NsOuter::NsInner", NodeKind::Class);
3568
3569 assert_has_node_with_kind_exact(&staging, "Outer::Inner.z", NodeKind::Property);
3572 assert_has_node_with_kind_exact(&staging, "Outer::L1::L2.q", NodeKind::Property);
3573 assert_has_node_with_kind_exact(&staging, "ns::NsOuter::NsInner.n", NodeKind::Property);
3574 }
3575
3576 #[test]
3577 fn test_nested_enum_carries_enclosing_visibility() {
3578 let source = r"
3582 class Outer {
3583 private:
3584 enum class Secret { A, B };
3585 public:
3586 enum class Pub { X, Y };
3587 };
3588 ";
3589 let staging = build_cpp(source);
3590
3591 let secret = cpp_find_added_node(&staging, "Outer::Secret")
3592 .expect("nested enum Outer::Secret must be staged");
3593 assert_eq!(secret.kind, NodeKind::Enum, "Secret must be an Enum node");
3594 let secret_vis = staging.resolve_local_string(
3595 secret
3596 .visibility
3597 .expect("nested enum must carry a visibility id"),
3598 );
3599 assert_eq!(
3600 secret_vis,
3601 Some("private"),
3602 "nested enum under `private:` must be private"
3603 );
3604
3605 let pub_enum = cpp_find_added_node(&staging, "Outer::Pub")
3606 .expect("nested enum Outer::Pub must be staged");
3607 let pub_vis = staging.resolve_local_string(
3608 pub_enum
3609 .visibility
3610 .expect("nested enum must carry a visibility id"),
3611 );
3612 assert_eq!(
3613 pub_vis,
3614 Some("public"),
3615 "nested enum under `public:` must be public"
3616 );
3617 }
3618
3619 #[test]
3620 fn test_nested_class_emits_inheritance_edge() {
3621 let source = r"
3625 struct Base { virtual ~Base(); };
3626 class Outer {
3627 public:
3628 class Derived : public Base {};
3629 };
3630 ";
3631 let staging = build_cpp(source);
3632
3633 let derived_id = cpp_find_added_node_id(&staging, "Outer::Derived", NodeKind::Class)
3634 .expect("nested Derived class node must be staged");
3635
3636 let has_inherits = staging.operations().iter().any(|op| {
3637 matches!(
3638 op,
3639 StagingOp::AddEdge {
3640 source: src,
3641 kind: EdgeKind::Inherits,
3642 ..
3643 } if *src == derived_id
3644 )
3645 });
3646 assert!(
3647 has_inherits,
3648 "nested Derived must emit an Inherits edge to its base"
3649 );
3650 }
3651
3652 #[test]
3653 fn test_top_level_union_emits_struct_node() {
3654 let source = "union Value { int i; float f; };";
3658 let staging = build_cpp(source);
3659 assert_has_node_with_kind_exact(&staging, "Value", NodeKind::Struct);
3660 }
3661
3662 #[test]
3663 fn test_extract_function() {
3664 let source = r#"
3665 #include <cstdio>
3666 void hello() {
3667 std::printf("Hello");
3668 }
3669 "#;
3670 let tree = parse_cpp(source);
3671 let mut staging = StagingGraph::new();
3672 let builder = CppGraphBuilder::new();
3673
3674 let result = builder.build_graph(
3675 &tree,
3676 source.as_bytes(),
3677 Path::new("test.cpp"),
3678 &mut staging,
3679 );
3680
3681 assert!(result.is_ok());
3682 assert_has_node_with_kind(&staging, "hello", NodeKind::Function);
3683 }
3684
3685 #[test]
3686 fn test_extract_virtual_function() {
3687 let source = r"
3688 class Service {
3689 public:
3690 virtual void fetchData() {}
3691 };
3692 ";
3693 let tree = parse_cpp(source);
3694 let mut staging = StagingGraph::new();
3695 let builder = CppGraphBuilder::new();
3696
3697 let result = builder.build_graph(
3698 &tree,
3699 source.as_bytes(),
3700 Path::new("test.cpp"),
3701 &mut staging,
3702 );
3703
3704 assert!(result.is_ok());
3705 assert_has_node(&staging, "fetchData");
3706 }
3707
3708 #[test]
3709 fn test_extract_call_edge() {
3710 let source = r"
3711 void greet() {}
3712
3713 int main() {
3714 greet();
3715 return 0;
3716 }
3717 ";
3718 let tree = parse_cpp(source);
3719 let mut staging = StagingGraph::new();
3720 let builder = CppGraphBuilder::new();
3721
3722 let result = builder.build_graph(
3723 &tree,
3724 source.as_bytes(),
3725 Path::new("test.cpp"),
3726 &mut staging,
3727 );
3728
3729 assert!(result.is_ok());
3730 assert_has_node(&staging, "main");
3731 assert_has_node(&staging, "greet");
3732 let calls = collect_call_edges(&staging);
3733 assert!(!calls.is_empty());
3734 }
3735
3736 #[test]
3737 fn test_extract_member_call_edge() {
3738 let source = r"
3739 class Service {
3740 public:
3741 void helper() {}
3742 };
3743
3744 int main() {
3745 Service svc;
3746 svc.helper();
3747 return 0;
3748 }
3749 ";
3750 let tree = parse_cpp(source);
3751 let mut staging = StagingGraph::new();
3752 let builder = CppGraphBuilder::new();
3753
3754 let result = builder.build_graph(
3755 &tree,
3756 source.as_bytes(),
3757 Path::new("member.cpp"),
3758 &mut staging,
3759 );
3760
3761 assert!(result.is_ok());
3762 assert_has_node(&staging, "main");
3763 assert_has_node(&staging, "helper");
3764 let calls = collect_call_edges(&staging);
3765 assert!(!calls.is_empty());
3766 }
3767
3768 #[test]
3769 fn test_extract_namespace_map_simple() {
3770 let source = r"
3771 namespace demo {
3772 void func() {}
3773 }
3774 ";
3775 let tree = parse_cpp(source);
3776 let namespace_map = extract_namespace_map_for_test(&tree, source);
3777
3778 assert_eq!(namespace_map.len(), 1);
3780
3781 let (_, ns_prefix) = namespace_map.iter().next().unwrap();
3783 assert_eq!(ns_prefix, "demo::");
3784 }
3785
3786 #[test]
3787 fn test_extract_namespace_map_nested() {
3788 let source = r"
3789 namespace outer {
3790 namespace inner {
3791 void func() {}
3792 }
3793 }
3794 ";
3795 let tree = parse_cpp(source);
3796 let namespace_map = extract_namespace_map_for_test(&tree, source);
3797
3798 assert!(namespace_map.len() >= 2);
3800
3801 let ns_values: Vec<&String> = namespace_map.values().collect();
3803 assert!(ns_values.iter().any(|v| v.as_str() == "outer::"));
3804 assert!(ns_values.iter().any(|v| v.as_str() == "outer::inner::"));
3805 }
3806
3807 #[test]
3808 fn test_extract_namespace_map_multiple() {
3809 let source = r"
3810 namespace first {
3811 void func1() {}
3812 }
3813 namespace second {
3814 void func2() {}
3815 }
3816 ";
3817 let tree = parse_cpp(source);
3818 let namespace_map = extract_namespace_map_for_test(&tree, source);
3819
3820 assert_eq!(namespace_map.len(), 2);
3822
3823 let ns_values: Vec<&String> = namespace_map.values().collect();
3824 assert!(ns_values.iter().any(|v| v.as_str() == "first::"));
3825 assert!(ns_values.iter().any(|v| v.as_str() == "second::"));
3826 }
3827
3828 #[test]
3829 fn test_find_namespace_for_offset() {
3830 let source = r"
3831 namespace demo {
3832 void func() {}
3833 }
3834 ";
3835 let tree = parse_cpp(source);
3836 let namespace_map = extract_namespace_map_for_test(&tree, source);
3837
3838 let func_offset = source.find("func").unwrap();
3840 let ns = find_namespace_for_offset(func_offset, &namespace_map);
3841 assert_eq!(ns, "demo::");
3842
3843 let ns = find_namespace_for_offset(0, &namespace_map);
3845 assert_eq!(ns, "");
3846 }
3847
3848 #[test]
3849 fn test_extract_cpp_contexts_free_function() {
3850 let source = r"
3851 void helper() {}
3852 ";
3853 let tree = parse_cpp(source);
3854 let namespace_map = extract_namespace_map_for_test(&tree, source);
3855 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3856
3857 assert_eq!(contexts.len(), 1);
3858 assert_eq!(contexts[0].qualified_name, "helper");
3859 assert!(!contexts[0].is_static);
3860 assert!(!contexts[0].is_virtual);
3861 }
3862
3863 #[test]
3864 fn test_extract_cpp_contexts_namespace_function() {
3865 let source = r"
3866 namespace demo {
3867 void helper() {}
3868 }
3869 ";
3870 let tree = parse_cpp(source);
3871 let namespace_map = extract_namespace_map_for_test(&tree, source);
3872 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3873
3874 assert_eq!(contexts.len(), 1);
3875 assert_eq!(contexts[0].qualified_name, "demo::helper");
3876 assert_eq!(contexts[0].namespace_stack, vec!["demo"]);
3877 }
3878
3879 #[test]
3880 fn test_extract_cpp_contexts_class_method() {
3881 let source = r"
3882 class Service {
3883 public:
3884 void process() {}
3885 };
3886 ";
3887 let tree = parse_cpp(source);
3888 let namespace_map = extract_namespace_map_for_test(&tree, source);
3889 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3890
3891 assert_eq!(contexts.len(), 1);
3892 assert_eq!(contexts[0].qualified_name, "Service::process");
3893 assert_eq!(contexts[0].class_stack, vec!["Service"]);
3894 }
3895
3896 #[test]
3897 fn test_extract_cpp_contexts_namespace_and_class() {
3898 let source = r"
3899 namespace demo {
3900 class Service {
3901 public:
3902 void process() {}
3903 };
3904 }
3905 ";
3906 let tree = parse_cpp(source);
3907 let namespace_map = extract_namespace_map_for_test(&tree, source);
3908 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3909
3910 assert_eq!(contexts.len(), 1);
3911 assert_eq!(contexts[0].qualified_name, "demo::Service::process");
3912 assert_eq!(contexts[0].namespace_stack, vec!["demo"]);
3913 assert_eq!(contexts[0].class_stack, vec!["Service"]);
3914 }
3915
3916 #[test]
3917 fn test_extract_cpp_contexts_static_method() {
3918 let source = r"
3919 class Repository {
3920 public:
3921 static void save() {}
3922 };
3923 ";
3924 let tree = parse_cpp(source);
3925 let namespace_map = extract_namespace_map_for_test(&tree, source);
3926 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3927
3928 assert_eq!(contexts.len(), 1);
3929 assert_eq!(contexts[0].qualified_name, "Repository::save");
3930 assert!(contexts[0].is_static);
3931 }
3932
3933 #[test]
3934 fn test_extract_cpp_contexts_virtual_method() {
3935 let source = r"
3936 class Base {
3937 public:
3938 virtual void render() {}
3939 };
3940 ";
3941 let tree = parse_cpp(source);
3942 let namespace_map = extract_namespace_map_for_test(&tree, source);
3943 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3944
3945 assert_eq!(contexts.len(), 1);
3946 assert_eq!(contexts[0].qualified_name, "Base::render");
3947 assert!(contexts[0].is_virtual);
3948 }
3949
3950 #[test]
3951 fn test_extract_cpp_contexts_inline_function() {
3952 let source = r"
3953 inline void helper() {}
3954 ";
3955 let tree = parse_cpp(source);
3956 let namespace_map = extract_namespace_map_for_test(&tree, source);
3957 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3958
3959 assert_eq!(contexts.len(), 1);
3960 assert_eq!(contexts[0].qualified_name, "helper");
3961 assert!(contexts[0].is_inline);
3962 }
3963
3964 #[test]
3965 fn test_extract_cpp_contexts_out_of_line_definition() {
3966 let source = r"
3967 namespace demo {
3968 class Service {
3969 public:
3970 int process(int v);
3971 };
3972
3973 inline int Service::process(int v) {
3974 return v;
3975 }
3976 }
3977 ";
3978 let tree = parse_cpp(source);
3979 let namespace_map = extract_namespace_map_for_test(&tree, source);
3980 let contexts = extract_cpp_contexts_for_test(&tree, source, &namespace_map);
3981
3982 assert_eq!(contexts.len(), 1);
3984 assert_eq!(contexts[0].qualified_name, "demo::Service::process");
3985 assert!(contexts[0].is_inline);
3986 }
3987
3988 #[test]
3989 fn test_extract_field_types_simple() {
3990 let source = r"
3991 class Service {
3992 public:
3993 Repository repo;
3994 };
3995 ";
3996 let tree = parse_cpp(source);
3997 let namespace_map = extract_namespace_map_for_test(&tree, source);
3998 let (field_types, _type_map) =
3999 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
4000
4001 assert_eq!(field_types.len(), 1);
4003 assert_eq!(
4004 field_types.get(&("Service".to_string(), "repo".to_string())),
4005 Some(&"Repository".to_string())
4006 );
4007 }
4008
4009 #[test]
4010 fn test_extract_field_types_namespace() {
4011 let source = r"
4012 namespace demo {
4013 class Service {
4014 public:
4015 Repository repo;
4016 };
4017 }
4018 ";
4019 let tree = parse_cpp(source);
4020 let namespace_map = extract_namespace_map_for_test(&tree, source);
4021 let (field_types, _type_map) =
4022 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
4023
4024 assert_eq!(field_types.len(), 1);
4026 assert_eq!(
4027 field_types.get(&("demo::Service".to_string(), "repo".to_string())),
4028 Some(&"Repository".to_string())
4029 );
4030 }
4031
4032 #[test]
4033 fn test_extract_field_types_no_collision() {
4034 let source = r"
4035 class ServiceA {
4036 public:
4037 Repository repo;
4038 };
4039
4040 class ServiceB {
4041 public:
4042 Repository repo;
4043 };
4044 ";
4045 let tree = parse_cpp(source);
4046 let namespace_map = extract_namespace_map_for_test(&tree, source);
4047 let (field_types, _type_map) =
4048 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
4049
4050 assert_eq!(field_types.len(), 2);
4052 assert_eq!(
4053 field_types.get(&("ServiceA".to_string(), "repo".to_string())),
4054 Some(&"Repository".to_string())
4055 );
4056 assert_eq!(
4057 field_types.get(&("ServiceB".to_string(), "repo".to_string())),
4058 Some(&"Repository".to_string())
4059 );
4060 }
4061
4062 #[test]
4063 fn test_extract_using_declaration() {
4064 let source = r"
4065 using std::vector;
4066
4067 class Service {
4068 public:
4069 vector data;
4070 };
4071 ";
4072 let tree = parse_cpp(source);
4073 let namespace_map = extract_namespace_map_for_test(&tree, source);
4074 let (field_types, type_map) =
4075 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
4076
4077 assert_eq!(field_types.len(), 1);
4079 assert_eq!(
4080 field_types.get(&("Service".to_string(), "data".to_string())),
4081 Some(&"std::vector".to_string()),
4082 "Field type should resolve 'vector' to 'std::vector' via using declaration"
4083 );
4084
4085 assert_eq!(
4087 type_map.get(&(String::new(), "vector".to_string())),
4088 Some(&"std::vector".to_string()),
4089 "Using declaration should map 'vector' to 'std::vector' in type_map"
4090 );
4091 }
4092
4093 #[test]
4094 fn test_extract_field_types_pointer() {
4095 let source = r"
4096 class Service {
4097 public:
4098 Repository* repo;
4099 };
4100 ";
4101 let tree = parse_cpp(source);
4102 let namespace_map = extract_namespace_map_for_test(&tree, source);
4103 let (field_types, _type_map) =
4104 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
4105
4106 assert_eq!(field_types.len(), 1);
4108 assert_eq!(
4109 field_types.get(&("Service".to_string(), "repo".to_string())),
4110 Some(&"Repository".to_string())
4111 );
4112 }
4113
4114 #[test]
4115 fn test_extract_field_types_multiple_declarators() {
4116 let source = r"
4117 class Service {
4118 public:
4119 Repository repo_a, repo_b, repo_c;
4120 };
4121 ";
4122 let tree = parse_cpp(source);
4123 let namespace_map = extract_namespace_map_for_test(&tree, source);
4124 let (field_types, _type_map) =
4125 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
4126
4127 assert_eq!(field_types.len(), 3);
4129 assert_eq!(
4130 field_types.get(&("Service".to_string(), "repo_a".to_string())),
4131 Some(&"Repository".to_string())
4132 );
4133 assert_eq!(
4134 field_types.get(&("Service".to_string(), "repo_b".to_string())),
4135 Some(&"Repository".to_string())
4136 );
4137 assert_eq!(
4138 field_types.get(&("Service".to_string(), "repo_c".to_string())),
4139 Some(&"Repository".to_string())
4140 );
4141 }
4142
4143 #[test]
4144 fn test_extract_field_types_nested_struct_with_parent_field() {
4145 let source = r"
4148 namespace demo {
4149 struct Outer {
4150 int outer_field;
4151 struct Inner {
4152 int inner_field;
4153 };
4154 Inner nested_instance;
4155 };
4156 }
4157 ";
4158 let tree = parse_cpp(source);
4159 let namespace_map = extract_namespace_map_for_test(&tree, source);
4160 let (field_types, _type_map) =
4161 extract_field_and_type_info_for_test(&tree, source, &namespace_map);
4162
4163 assert!(
4166 field_types.len() >= 2,
4167 "Expected at least outer_field and nested_instance"
4168 );
4169
4170 assert_eq!(
4172 field_types.get(&("demo::Outer".to_string(), "outer_field".to_string())),
4173 Some(&"int".to_string())
4174 );
4175
4176 assert_eq!(
4182 field_types.get(&("demo::Outer".to_string(), "nested_instance".to_string())),
4183 Some(&"demo::Outer::Inner".to_string())
4184 );
4185
4186 if field_types.contains_key(&("demo::Outer::Inner".to_string(), "inner_field".to_string()))
4188 {
4189 assert_eq!(
4191 field_types.get(&("demo::Outer::Inner".to_string(), "inner_field".to_string())),
4192 Some(&"int".to_string()),
4193 "Inner class fields must use parent-qualified FQN 'demo::Outer::Inner'"
4194 );
4195 }
4196 }
4197
4198 use sqry_core::graph::unified::build::staging::StagingOp;
4215 use sqry_core::graph::unified::edge::kind::{EdgeKind, TypeOfContext};
4216
4217 fn cpp_find_added_node<'a>(
4219 staging: &'a StagingGraph,
4220 canonical_name: &str,
4221 ) -> Option<&'a sqry_core::graph::unified::storage::arena::NodeEntry> {
4222 staging.operations().iter().find_map(|op| {
4223 if let StagingOp::AddNode { entry, .. } = op
4224 && staging.resolve_node_canonical_name(entry) == Some(canonical_name)
4225 {
4226 Some(entry)
4227 } else {
4228 None
4229 }
4230 })
4231 }
4232
4233 fn cpp_find_added_node_id(
4235 staging: &StagingGraph,
4236 canonical_name: &str,
4237 kind: NodeKind,
4238 ) -> Option<sqry_core::graph::unified::NodeId> {
4239 staging.operations().iter().find_map(|op| match op {
4240 StagingOp::AddNode {
4241 entry,
4242 expected_id: Some(id),
4243 } if entry.kind == kind
4244 && staging.resolve_node_canonical_name(entry) == Some(canonical_name) =>
4245 {
4246 Some(*id)
4247 }
4248 _ => None,
4249 })
4250 }
4251
4252 fn build_cpp(source: &str) -> StagingGraph {
4254 let tree = parse_cpp(source);
4255 let mut staging = StagingGraph::new();
4256 let builder = CppGraphBuilder::new();
4257 builder
4258 .build_graph(
4259 &tree,
4260 source.as_bytes(),
4261 Path::new("test.cpp"),
4262 &mut staging,
4263 )
4264 .expect("build_graph must succeed for the test fixture");
4265 staging
4266 }
4267
4268 fn staged_node_name_by_id(
4269 staging: &StagingGraph,
4270 id: sqry_core::graph::unified::NodeId,
4271 ) -> Option<&str> {
4272 staging.nodes().find_map(|node| {
4273 if node.expected_id == Some(id) {
4274 staging.resolve_node_canonical_name(node.entry)
4275 } else {
4276 None
4277 }
4278 })
4279 }
4280
4281 fn call_edge_pairs(staging: &StagingGraph) -> Vec<(String, String)> {
4282 staging
4283 .edges()
4284 .filter_map(|edge| {
4285 if matches!(edge.kind, EdgeKind::Calls { .. }) {
4286 let source = staged_node_name_by_id(staging, edge.source)?.to_string();
4287 let target = staged_node_name_by_id(staging, edge.target)?.to_string();
4288 Some((source, target))
4289 } else {
4290 None
4291 }
4292 })
4293 .collect()
4294 }
4295
4296 fn assert_has_call_edge(staging: &StagingGraph, caller: &str, callee: &str) {
4297 let calls = call_edge_pairs(staging);
4298 assert!(
4299 calls
4300 .iter()
4301 .any(|(source, target)| source == caller && target == callee),
4302 "expected Calls edge {caller} -> {callee}; staged Calls edges: {calls:?}"
4303 );
4304 }
4305
4306 fn assert_no_call_target(staging: &StagingGraph, forbidden_target: &str) {
4307 let calls = call_edge_pairs(staging);
4308 assert!(
4309 !calls.iter().any(|(_, target)| target == forbidden_target),
4310 "unexpected Calls edge target {forbidden_target}; staged Calls edges: {calls:?}"
4311 );
4312 }
4313
4314 fn assert_no_call_target_suffix(staging: &StagingGraph, forbidden_suffix: &str) {
4315 let calls = call_edge_pairs(staging);
4316 assert!(
4317 !calls
4318 .iter()
4319 .any(|(_, target)| target.ends_with(forbidden_suffix)),
4320 "unexpected Calls edge target ending with {forbidden_suffix}; staged Calls edges: {calls:?}"
4321 );
4322 }
4323
4324 #[test]
4325 fn test_issue_466_t1_member_call_through_field_resolves_to_method_fqn() {
4326 let source = r"
4327namespace demo {
4328 struct Repository { void save(); };
4329 struct Service { Repository repo; void run() { repo.save(); } };
4330}
4331";
4332 let staging = build_cpp(source);
4333
4334 assert_has_call_edge(&staging, "demo::Service::run", "demo::Repository::save");
4335 assert_no_call_target(&staging, "demo::repo.save");
4336 assert_no_call_target(&staging, "Repository::save");
4337 }
4338
4339 #[test]
4340 fn test_issue_466_t2_same_namespace_static_call_uses_fallback_prefix() {
4341 let source = r"
4342namespace demo {
4343 struct Repository { static void save(); };
4344 void use() { Repository::save(); }
4345}
4346";
4347 let staging = build_cpp(source);
4348
4349 assert_has_call_edge(&staging, "demo::use", "demo::Repository::save");
4350 }
4351
4352 #[test]
4353 fn test_issue_466_t3_using_declaration_alias_resolves_static_call() {
4354 let source = r"
4355namespace lib { struct Widget { static void make(); }; }
4356namespace app { using lib::Widget; void run() { Widget::make(); } }
4357";
4358 let staging = build_cpp(source);
4359
4360 assert_has_call_edge(&staging, "app::run", "lib::Widget::make");
4361 assert_no_call_target(&staging, "app::Widget::make");
4362 }
4363
4364 #[test]
4365 fn test_issue_466_t4_unknown_receiver_does_not_invent_member_target() {
4366 let source = r"
4367namespace demo {
4368 struct Service { void run(int* p) { p->frobnicate(); } };
4369}
4370";
4371 let staging = build_cpp(source);
4372
4373 assert_no_call_target_suffix(&staging, "::frobnicate");
4374 assert_has_call_edge(&staging, "demo::Service::run", "demo::p->frobnicate");
4375 }
4376
4377 #[test]
4378 fn test_issue_466_t5_qualified_and_ffi_fallback_behavior_is_unchanged() {
4379 let source = r#"
4380extern "C" { int printf(const char*); }
4381namespace demo { void helper() {} }
4382void run() {
4383 demo::helper();
4384 printf("x");
4385}
4386"#;
4387 let staging = build_cpp(source);
4388
4389 assert_has_call_edge(&staging, "run", "demo::helper");
4390 assert_has_ffi_call_edge(&staging, "run", "extern::C::printf");
4391 }
4392
4393 #[test]
4394 fn test_issue_466_t6_same_class_name_collision_does_not_cross_namespace() {
4395 let source = r"
4396namespace a { struct Repository { void save(); }; }
4397namespace b {
4398 struct Repository { void wipe(); };
4399 struct Service { Repository repo; void run() { repo.save(); } };
4400}
4401";
4402 let staging = build_cpp(source);
4403
4404 assert_no_call_target(&staging, "a::Repository::save");
4405 assert_has_call_edge(&staging, "b::Service::run", "b::Repository::save");
4406 }
4407
4408 #[test]
4409 fn test_issue_466_t7_nested_class_member_access_resolves() {
4410 let source = r"
4411namespace demo {
4412 struct Inner { void tick(); };
4413 struct Outer { struct Nested { Inner inner; void go() { inner.tick(); } }; };
4414}
4415";
4416 let staging = build_cpp(source);
4417
4418 assert_has_call_edge(&staging, "demo::Outer::Nested::go", "demo::Inner::tick");
4419 }
4420
4421 #[test]
4422 fn test_issue_466_t8_out_of_class_method_definition_resolves_member_field() {
4423 let source = r"
4424namespace demo {
4425 struct Repository { void save(); };
4426 struct Service { Repository repo; void run(); };
4427 void Service::run() { repo.save(); }
4428}
4429";
4430 let staging = build_cpp(source);
4431
4432 assert_has_call_edge(&staging, "demo::Service::run", "demo::Repository::save");
4433 }
4434
4435 #[test]
4436 fn test_issue_466_t9_same_named_fields_bind_to_enclosing_class() {
4437 let source = r"
4438namespace demo {
4439 struct Base { struct Handle { void base_op(); }; Handle h; };
4440 struct Repository { void save(); };
4441 struct Service { Repository h; void run() { h.save(); } };
4442}
4443";
4444 let staging = build_cpp(source);
4445
4446 assert_has_call_edge(&staging, "demo::Service::run", "demo::Repository::save");
4447 assert_no_call_target(&staging, "demo::Base::Handle::base_op");
4448 }
4449
4450 #[test]
4456 fn test_struct_field_emits_property_with_field_context() {
4457 let source = "struct Point { int x; int y; };";
4458 let staging = build_cpp(source);
4459
4460 assert_has_node_with_kind_exact(&staging, "Point.x", NodeKind::Property);
4462 assert_has_node_with_kind_exact(&staging, "Point.y", NodeKind::Property);
4463
4464 let entry =
4465 cpp_find_added_node(&staging, "Point.x").expect("Point.x should be staged as a node");
4466 assert_eq!(entry.kind, NodeKind::Property, "x must be Property");
4467 assert!(!entry.is_static, "instance field is_static must be false");
4468 let vis = staging.resolve_local_string(entry.visibility.expect("visibility id"));
4469 assert_eq!(
4470 vis,
4471 Some("public"),
4472 "struct default visibility must be 'public'"
4473 );
4474 assert!(entry.end_line > 0, "field end_line must be set (got 0)");
4480 assert!(
4481 entry.end_line > entry.start_line
4482 || (entry.end_line == entry.start_line && entry.end_column > entry.start_column),
4483 "field span must be non-empty: [{}:{}..{}:{}]",
4484 entry.start_line,
4485 entry.start_column,
4486 entry.end_line,
4487 entry.end_column,
4488 );
4489
4490 let x_id = cpp_find_added_node_id(&staging, "Point.x", NodeKind::Property)
4492 .expect("Point.x Property NodeId");
4493 let edge = staging.operations().iter().find_map(|op| {
4494 if let StagingOp::AddEdge {
4495 source: src,
4496 kind: EdgeKind::TypeOf { context, name, .. },
4497 ..
4498 } = op
4499 && *src == x_id
4500 {
4501 Some((*context, *name))
4502 } else {
4503 None
4504 }
4505 });
4506 let (ctx, name) = edge.expect("TypeOf edge from Point.x should be staged");
4507 assert_eq!(
4508 ctx,
4509 Some(TypeOfContext::Field),
4510 "TypeOf edge context must be Field"
4511 );
4512 let resolved_name = name.and_then(|sid| staging.resolve_local_string(sid));
4513 assert_eq!(
4514 resolved_name,
4515 Some("x"),
4516 "TypeOf edge name must be the bare field name 'x'"
4517 );
4518
4519 let stale_variable = staging.nodes().any(|n| {
4521 n.entry.kind == NodeKind::Variable
4522 && matches!(
4523 staging.resolve_node_name(n.entry),
4524 Some("Point.x" | "Point.y" | "Point::x" | "Point::y")
4525 )
4526 });
4527 assert!(
4528 !stale_variable,
4529 "Point fields must not be emitted as NodeKind::Variable"
4530 );
4531 }
4532
4533 #[test]
4535 fn test_class_field_default_visibility_is_private() {
4536 let source = "class Foo { int hidden; };";
4537 let staging = build_cpp(source);
4538
4539 let entry = cpp_find_added_node(&staging, "Foo.hidden")
4540 .expect("Foo.hidden should be staged as a node");
4541 assert_eq!(entry.kind, NodeKind::Property);
4542 let vis = staging.resolve_local_string(entry.visibility.expect("visibility id"));
4543 assert_eq!(
4544 vis,
4545 Some("private"),
4546 "class default visibility must be 'private'"
4547 );
4548 }
4549
4550 #[test]
4552 fn test_class_field_respects_explicit_access_specifier() {
4553 let source = "class Foo { public: int public_field; protected: int prot_field; };";
4554 let staging = build_cpp(source);
4555
4556 let pub_entry = cpp_find_added_node(&staging, "Foo.public_field")
4557 .expect("Foo.public_field should be staged");
4558 assert_eq!(
4559 staging.resolve_local_string(pub_entry.visibility.expect("vis")),
4560 Some("public")
4561 );
4562
4563 let prot_entry = cpp_find_added_node(&staging, "Foo.prot_field")
4564 .expect("Foo.prot_field should be staged");
4565 assert_eq!(
4566 staging.resolve_local_string(prot_entry.visibility.expect("vis")),
4567 Some("protected")
4568 );
4569 }
4570
4571 #[test]
4574 fn test_const_field_emits_constant() {
4575 let source = "class Foo { const int kMax = 0; };";
4576 let staging = build_cpp(source);
4577
4578 assert_has_node_with_kind_exact(&staging, "Foo.kMax", NodeKind::Constant);
4579 let entry = cpp_find_added_node(&staging, "Foo.kMax").expect("Foo.kMax");
4580 assert_eq!(entry.kind, NodeKind::Constant);
4581 assert!(
4582 !entry.is_static,
4583 "const (non-static) field is_static must be false; only `static` keyword sets is_static"
4584 );
4585 }
4586
4587 #[test]
4591 fn test_constexpr_field_emits_constant() {
4592 let source = "class Foo { constexpr static int kAnswer = 42; };";
4593 let staging = build_cpp(source);
4594
4595 assert_has_node_with_kind_exact(&staging, "Foo.kAnswer", NodeKind::Constant);
4596 let entry = cpp_find_added_node(&staging, "Foo.kAnswer").expect("Foo.kAnswer");
4597 assert_eq!(entry.kind, NodeKind::Constant);
4598 assert!(
4599 entry.is_static,
4600 "static constexpr member must have is_static = true"
4601 );
4602 }
4603
4604 #[test]
4607 fn test_static_field_sets_is_static_true() {
4608 let source = "class Foo { static int counter; };";
4609 let staging = build_cpp(source);
4610
4611 let entry = cpp_find_added_node(&staging, "Foo.counter").expect("Foo.counter");
4612 assert_eq!(entry.kind, NodeKind::Property);
4613 assert!(entry.is_static, "static keyword must set is_static = true");
4614 }
4615
4616 #[test]
4619 fn test_bitfield_emits_property() {
4620 let source = "struct Flags { unsigned int low : 4; unsigned int high : 4; };";
4621 let staging = build_cpp(source);
4622
4623 assert_has_node_with_kind_exact(&staging, "Flags.low", NodeKind::Property);
4624 assert_has_node_with_kind_exact(&staging, "Flags.high", NodeKind::Property);
4625 }
4626
4627 #[test]
4633 fn test_anonymous_union_member_fields_emit_property() {
4634 let source = r"
4635class Variant {
4636public:
4637 int tag;
4638 union {
4639 int as_int;
4640 float as_float;
4641 };
4642};
4643";
4644 let staging = build_cpp(source);
4645
4646 assert_has_node_with_kind_exact(&staging, "Variant.tag", NodeKind::Property);
4648
4649 assert_has_node_with_kind_exact(&staging, "Variant.as_int", NodeKind::Property);
4652 assert_has_node_with_kind_exact(&staging, "Variant.as_float", NodeKind::Property);
4653
4654 let as_int = cpp_find_added_node(&staging, "Variant.as_int")
4657 .expect("Variant.as_int should be staged");
4658 let vis = staging.resolve_local_string(as_int.visibility.expect("visibility id"));
4659 assert_eq!(
4660 vis,
4661 Some("public"),
4662 "anonymous-union members must inherit OUTER access (`public:` here)"
4663 );
4664
4665 let bogus = staging.nodes().any(|n| {
4668 staging
4669 .resolve_node_name(n.entry)
4670 .is_some_and(|name| name.contains("::.") || name.starts_with("Variant::."))
4671 });
4672 assert!(
4673 !bogus,
4674 "anonymous union must not produce a synthetic qualifier"
4675 );
4676
4677 let stale_variable = staging.nodes().any(|n| {
4679 n.entry.kind == NodeKind::Variable
4680 && matches!(
4681 staging.resolve_node_name(n.entry),
4682 Some("Variant.tag" | "Variant.as_int" | "Variant.as_float")
4683 )
4684 });
4685 assert!(
4686 !stale_variable,
4687 "anonymous-union members + outer fields must not stay as Variable"
4688 );
4689 }
4690
4691 #[test]
4695 fn test_templated_class_field_emits_property() {
4696 let source = r"
4697template<class T>
4698struct Box {
4699 T value;
4700};
4701";
4702 let staging = build_cpp(source);
4703
4704 assert_has_node_with_kind_exact(&staging, "Box.value", NodeKind::Property);
4705 let entry = cpp_find_added_node(&staging, "Box.value").expect("Box.value");
4706 assert_eq!(entry.kind, NodeKind::Property);
4707 assert!(!entry.is_static);
4708 }
4709
4710 #[test]
4716 fn test_outer_class_field_with_nested_class_present() {
4717 let source = r"
4718class Outer {
4719public:
4720 int outer_value;
4721 class Inner {
4722 public:
4723 int x;
4724 };
4725};
4726";
4727 let staging = build_cpp(source);
4728
4729 assert_has_node_with_kind_exact(&staging, "Outer.outer_value", NodeKind::Property);
4731
4732 assert_has_node_with_kind_exact(&staging, "Outer::Inner.x", NodeKind::Property);
4736
4737 let legacy_hits: Vec<_> = staging
4740 .nodes()
4741 .filter(|n| staging.resolve_node_name(n.entry) == Some("Outer::outer_value"))
4742 .collect();
4743 assert!(
4744 legacy_hits.is_empty(),
4745 "legacy `Outer::outer_value` lookup must return 0 hits"
4746 );
4747
4748 for legacy in ["Inner.x", "Outer::Inner::x", "Outer.Inner.x"] {
4752 let hits: Vec<_> = staging
4753 .nodes()
4754 .filter(|n| staging.resolve_node_name(n.entry) == Some(legacy))
4755 .collect();
4756 assert!(
4757 hits.is_empty(),
4758 "nested-class field `{legacy}` must not appear; expected only `Outer::Inner.x`"
4759 );
4760 }
4761 }
4762
4763 #[test]
4767 fn test_outer_class_with_nested_struct_emits_inner_field() {
4768 let source = r"
4769class Outer {
4770private:
4771 struct Inner {
4772 int y;
4773 };
4774};
4775";
4776 let staging = build_cpp(source);
4777
4778 assert_has_node_with_kind_exact(&staging, "Outer::Inner.y", NodeKind::Property);
4779
4780 let entry = cpp_find_added_node(&staging, "Outer::Inner.y")
4781 .expect("Outer::Inner.y should be staged");
4782 let vis = staging.resolve_local_string(entry.visibility.expect("visibility id"));
4783 assert_eq!(
4784 vis,
4785 Some("public"),
4786 "nested struct field default visibility must be 'public' \
4787 regardless of OUTER access state"
4788 );
4789 }
4790
4791 #[test]
4799 fn test_legacy_double_colon_field_lookup_returns_zero() {
4800 let source = r"
4801class Foo {
4802public:
4803 int bar;
4804 static int baz;
4805 const int qux = 0;
4806};
4807struct Quux {
4808 int corge;
4809};
4810";
4811 let staging = build_cpp(source);
4812
4813 assert_has_node_with_kind_exact(&staging, "Foo.bar", NodeKind::Property);
4815 assert_has_node_with_kind_exact(&staging, "Foo.baz", NodeKind::Property);
4816 assert_has_node_with_kind_exact(&staging, "Foo.qux", NodeKind::Constant);
4817 assert_has_node_with_kind_exact(&staging, "Quux.corge", NodeKind::Property);
4818
4819 for legacy in ["Foo::bar", "Foo::baz", "Foo::qux", "Quux::corge"] {
4822 let hits: Vec<_> = staging
4823 .nodes()
4824 .filter(|n| staging.resolve_node_name(n.entry) == Some(legacy))
4825 .collect();
4826 assert!(
4827 hits.is_empty(),
4828 "legacy lookup for {legacy:?} must return 0 hits, got {} node(s) ({:?})",
4829 hits.len(),
4830 hits.iter()
4831 .map(|n| (n.entry.kind, staging.resolve_node_name(n.entry)))
4832 .collect::<Vec<_>>()
4833 );
4834 }
4835 }
4836
4837 #[test]
4840 fn test_namespaced_class_field_qualified_name() {
4841 let source = r"
4842namespace demo {
4843 class Service {
4844 public:
4845 int counter;
4846 };
4847}
4848";
4849 let staging = build_cpp(source);
4850
4851 assert_has_node_with_kind_exact(&staging, "demo::Service.counter", NodeKind::Property);
4852 }
4853}
4854
4855#[cfg(test)]
4856mod shape_tests {
4857 use super::{cf_bucket_for_cpp_kind, cpp_shape_mapping};
4858 use sqry_core::graph::unified::build::shape::{
4859 CfBucket, ShapeBudget, ShapeMapping, compute_shape_descriptor,
4860 };
4861
4862 const SAMPLE: &str = include_str!(concat!(
4863 env!("CARGO_MANIFEST_DIR"),
4864 "/../test-fixtures/shape/reference/sample.cpp"
4865 ));
4866
4867 fn parse(src: &str) -> tree_sitter::Tree {
4868 let lang: tree_sitter::Language = tree_sitter_cpp::LANGUAGE.into();
4869 let mut p = tree_sitter::Parser::new();
4870 p.set_language(&lang).expect("load cpp grammar");
4871 p.parse(src, None).expect("parse")
4872 }
4873
4874 fn function_named<'t>(tree: &'t tree_sitter::Tree, name: &str) -> tree_sitter::Node<'t> {
4876 let root = tree.root_node();
4877 let mut stack = vec![root];
4878 while let Some(node) = stack.pop() {
4879 if node.kind() == "function_definition"
4880 && function_def_name(node).as_deref() == Some(name)
4881 {
4882 return node;
4883 }
4884 let mut c = node.walk();
4885 for ch in node.children(&mut c) {
4886 stack.push(ch);
4887 }
4888 }
4889 panic!("no function_definition named {name}");
4890 }
4891
4892 fn function_def_name(node: tree_sitter::Node) -> Option<String> {
4894 let mut decl = node.child_by_field_name("declarator")?;
4895 for _ in 0..8 {
4896 if decl.kind() == "function_declarator" {
4897 let inner = decl.child_by_field_name("declarator")?;
4898 return inner.utf8_text(SAMPLE.as_bytes()).ok().map(str::to_owned);
4899 }
4900 decl = decl.child_by_field_name("declarator")?;
4901 }
4902 None
4903 }
4904
4905 #[test]
4906 fn cf_table_is_non_empty() {
4907 let mapping = cpp_shape_mapping();
4908 let lang: tree_sitter::Language = tree_sitter_cpp::LANGUAGE.into();
4909 let mut covered = 0;
4910 for id in 0..lang.node_kind_count() {
4911 if mapping.cf_bucket(id as u16).is_some() {
4912 covered += 1;
4913 }
4914 }
4915 assert!(
4916 covered >= 10,
4917 "expected many C++ CF kinds mapped, got {covered}"
4918 );
4919 }
4920
4921 #[test]
4922 fn histogram_covers_real_control_flow() {
4923 let tree = parse(SAMPLE);
4924 let func = function_named(&tree, "classify");
4925 let d = compute_shape_descriptor(
4926 func,
4927 SAMPLE.as_bytes(),
4928 cpp_shape_mapping(),
4929 &ShapeBudget::default(),
4930 );
4931 assert!(!d.is_unhashable());
4932 for bucket in [
4933 CfBucket::Branch,
4934 CfBucket::Loop,
4935 CfBucket::Match,
4936 CfBucket::Try,
4937 CfBucket::Catch,
4938 CfBucket::Throw,
4939 CfBucket::Return,
4940 CfBucket::BreakContinue,
4941 CfBucket::Call,
4942 CfBucket::Assign,
4943 ] {
4944 assert!(
4945 d.cf_histogram[bucket.index()] >= 1,
4946 "classify must exercise {bucket:?}"
4947 );
4948 }
4949 }
4950
4951 #[test]
4952 fn lambda_body_covers_closure() {
4953 let tree = parse(SAMPLE);
4954 let func = function_named(&tree, "adder");
4955 let d = compute_shape_descriptor(
4956 func,
4957 SAMPLE.as_bytes(),
4958 cpp_shape_mapping(),
4959 &ShapeBudget::default(),
4960 );
4961 assert!(
4962 d.cf_histogram[CfBucket::Closure.index()] >= 1,
4963 "lambda closure"
4964 );
4965 }
4966
4967 #[test]
4968 fn signature_shape_reads_arity_defaults_return() {
4969 let tree = parse(SAMPLE);
4970 let func = function_named(&tree, "classify");
4971 let mapping = cpp_shape_mapping();
4972 let shape = mapping.signature_shape(func, SAMPLE.as_bytes());
4973 assert_eq!(shape.arity_positional, 2);
4975 assert!(shape.has_defaults, "threshold = 0");
4976 assert!(shape.has_return_annotation, "int return type");
4977 }
4978
4979 #[test]
4982 fn ac6_cpp_classify_histogram_well_formed() {
4983 let tree = parse(SAMPLE);
4984 let func = function_named(&tree, "classify");
4985 let d = compute_shape_descriptor(
4986 func,
4987 SAMPLE.as_bytes(),
4988 cpp_shape_mapping(),
4989 &ShapeBudget::default(),
4990 );
4991 assert_eq!(d.cf_histogram[CfBucket::Branch.index()], 2, "two if levels");
4993 assert!(d.cf_histogram[CfBucket::Loop.index()] >= 2, "for + while");
4994 assert!(d.cf_histogram[CfBucket::Return.index()] >= 2, "two returns");
4995 }
4996
4997 #[test]
4998 fn unknown_kind_maps_to_none() {
4999 assert!(cf_bucket_for_cpp_kind("translation_unit").is_none());
5000 assert!(cf_bucket_for_cpp_kind("identifier").is_none());
5001 }
5002}