1use sqry_core::graph::unified::{FfiConvention, GraphBuildHelper, StagingGraph};
19use sqry_core::graph::{GraphBuilder, GraphBuilderError, GraphResult, Language, Position, Span};
20use std::{
21 collections::{HashMap, HashSet},
22 path::Path,
23};
24use tree_sitter::{Node, Tree};
25
26const FILE_MODULE_NAME: &str = "<file_module>";
29
30type QualifiedNameMap = HashMap<(String, String), String>;
33
34type FfiRegistry = HashMap<String, (String, FfiConvention)>;
41
42type PureVirtualRegistry = HashSet<String>;
46
47#[allow(dead_code)] trait SpanExt {
50 fn from_node(node: &tree_sitter::Node) -> Self;
51}
52
53impl SpanExt for Span {
54 fn from_node(node: &tree_sitter::Node) -> Self {
55 Span::new(
56 Position::new(node.start_position().row, node.start_position().column),
57 Position::new(node.end_position().row, node.end_position().column),
58 )
59 }
60}
61
62#[derive(Debug)]
74struct ASTGraph {
75 contexts: Vec<FunctionContext>,
77
78 #[allow(dead_code)]
84 field_types: QualifiedNameMap,
85
86 #[allow(dead_code)]
93 type_map: QualifiedNameMap,
94
95 #[allow(dead_code)]
99 namespace_map: HashMap<std::ops::Range<usize>, String>,
100}
101
102impl ASTGraph {
103 fn from_tree(root: Node, content: &[u8]) -> Self {
105 let namespace_map = extract_namespace_map(root, content);
107
108 let contexts = extract_cpp_contexts(root, content, &namespace_map);
110
111 let (field_types, type_map) = extract_field_and_type_info(root, content, &namespace_map);
113
114 Self {
115 contexts,
116 field_types,
117 type_map,
118 namespace_map,
119 }
120 }
121
122 fn find_enclosing(&self, byte_pos: usize) -> Option<&FunctionContext> {
124 self.contexts
125 .iter()
126 .filter(|ctx| byte_pos >= ctx.span.0 && byte_pos < ctx.span.1)
127 .max_by_key(|ctx| ctx.depth)
128 }
129}
130
131#[derive(Debug, Clone)]
133struct FunctionContext {
134 qualified_name: String,
136 span: (usize, usize),
138 depth: usize,
140 is_static: bool,
143 #[allow(dead_code)]
146 is_virtual: bool,
147 #[allow(dead_code)]
150 is_inline: bool,
151 namespace_stack: Vec<String>,
153 #[allow(dead_code)] class_stack: Vec<String>,
157 return_type: Option<String>,
159}
160
161impl FunctionContext {
162 #[allow(dead_code)] fn qualified_name(&self) -> &str {
164 &self.qualified_name
165 }
166}
167
168#[derive(Debug, Default, Clone, Copy)]
192pub struct CppGraphBuilder;
193
194impl CppGraphBuilder {
195 #[must_use]
197 pub fn new() -> Self {
198 Self
199 }
200
201 #[allow(dead_code)] fn extract_class_attributes(node: &tree_sitter::Node, content: &[u8]) -> Vec<String> {
204 let mut attributes = Vec::new();
205 let mut cursor = node.walk();
206 for child in node.children(&mut cursor) {
207 if child.kind() == "modifiers" {
208 let mut mod_cursor = child.walk();
209 for modifier in child.children(&mut mod_cursor) {
210 if let Ok(mod_text) = modifier.utf8_text(content) {
211 match mod_text {
212 "template" => attributes.push("template".to_string()),
213 "sealed" => attributes.push("sealed".to_string()),
214 "abstract" => attributes.push("abstract".to_string()),
215 "open" => attributes.push("open".to_string()),
216 "final" => attributes.push("final".to_string()),
217 "inner" => attributes.push("inner".to_string()),
218 "value" => attributes.push("value".to_string()),
219 _ => {}
220 }
221 }
222 }
223 }
224 }
225 attributes
226 }
227
228 #[allow(dead_code)] fn extract_is_virtual(node: &tree_sitter::Node, content: &[u8]) -> bool {
231 if let Some(spec) = node.child_by_field_name("declaration_specifiers")
232 && let Ok(text) = spec.utf8_text(content)
233 && text.contains("virtual")
234 {
235 return true;
236 }
237
238 if let Ok(text) = node.utf8_text(content)
239 && text.contains("virtual")
240 {
241 return true;
242 }
243
244 if let Some(parent) = node.parent()
245 && (parent.kind() == "field_declaration" || parent.kind() == "declaration")
246 && let Ok(text) = parent.utf8_text(content)
247 && text.contains("virtual")
248 {
249 return true;
250 }
251
252 false
253 }
254
255 #[allow(dead_code)] fn extract_function_attributes(node: &tree_sitter::Node, content: &[u8]) -> Vec<String> {
258 let mut attributes = Vec::new();
259 for node_ref in [
260 node.child_by_field_name("declaration_specifiers"),
261 node.parent(),
262 ]
263 .into_iter()
264 .flatten()
265 {
266 if let Ok(text) = node_ref.utf8_text(content) {
267 for keyword in [
268 "virtual",
269 "inline",
270 "constexpr",
271 "operator",
272 "override",
273 "static",
274 ] {
275 if text.contains(keyword) && !attributes.contains(&keyword.to_string()) {
276 attributes.push(keyword.to_string());
277 }
278 }
279 }
280 }
281
282 if let Ok(text) = node.utf8_text(content) {
283 for keyword in [
284 "virtual",
285 "inline",
286 "constexpr",
287 "operator",
288 "override",
289 "static",
290 ] {
291 if text.contains(keyword) && !attributes.contains(&keyword.to_string()) {
292 attributes.push(keyword.to_string());
293 }
294 }
295 }
296
297 attributes
298 }
299}
300
301impl GraphBuilder for CppGraphBuilder {
302 fn language(&self) -> Language {
303 Language::Cpp
304 }
305
306 fn build_graph(
307 &self,
308 tree: &Tree,
309 content: &[u8],
310 file: &Path,
311 staging: &mut StagingGraph,
312 ) -> GraphResult<()> {
313 let mut helper = GraphBuildHelper::new(staging, file, Language::Cpp);
315
316 let ast_graph = ASTGraph::from_tree(tree.root_node(), content);
318
319 let mut seen_includes: HashSet<String> = HashSet::new();
321
322 let mut namespace_stack: Vec<String> = Vec::new();
324 let mut class_stack: Vec<String> = Vec::new();
325
326 let mut ffi_registry = FfiRegistry::new();
329 collect_ffi_declarations(tree.root_node(), content, &mut ffi_registry);
330
331 let mut pure_virtual_registry = PureVirtualRegistry::new();
333 collect_pure_virtual_interfaces(tree.root_node(), content, &mut pure_virtual_registry);
334
335 walk_tree_for_graph(
337 tree.root_node(),
338 content,
339 &ast_graph,
340 &mut helper,
341 &mut seen_includes,
342 &mut namespace_stack,
343 &mut class_stack,
344 &ffi_registry,
345 &pure_virtual_registry,
346 )?;
347
348 Ok(())
349 }
350}
351
352fn extract_namespace_map(node: Node, content: &[u8]) -> HashMap<std::ops::Range<usize>, String> {
364 let mut map = HashMap::new();
365
366 let recursion_limits = sqry_core::config::RecursionLimits::load_or_default()
368 .expect("Failed to load recursion limits");
369 let file_ops_depth = recursion_limits
370 .effective_file_ops_depth()
371 .expect("Invalid file_ops_depth configuration");
372 let mut guard = sqry_core::query::security::RecursionGuard::new(file_ops_depth)
373 .expect("Failed to create recursion guard");
374
375 if let Err(e) = extract_namespaces_recursive(node, content, "", &mut map, &mut guard) {
376 eprintln!("Warning: C++ namespace extraction hit recursion limit: {e}");
377 }
378
379 map
380}
381
382fn extract_namespaces_recursive(
388 node: Node,
389 content: &[u8],
390 current_ns: &str,
391 map: &mut HashMap<std::ops::Range<usize>, String>,
392 guard: &mut sqry_core::query::security::RecursionGuard,
393) -> Result<(), sqry_core::query::security::RecursionError> {
394 guard.enter()?;
395
396 if node.kind() == "namespace_definition" {
397 let ns_name = if let Some(name_node) = node.child_by_field_name("name") {
399 extract_identifier(name_node, content)
400 } else {
401 String::from("anonymous")
403 };
404
405 let new_ns = if current_ns.is_empty() {
407 format!("{ns_name}::")
408 } else {
409 format!("{current_ns}{ns_name}::")
410 };
411
412 if let Some(body) = node.child_by_field_name("body") {
414 let range = body.start_byte()..body.end_byte();
415 map.insert(range, new_ns.clone());
416
417 let mut cursor = body.walk();
419 for child in body.children(&mut cursor) {
420 extract_namespaces_recursive(child, content, &new_ns, map, guard)?;
421 }
422 }
423 } else {
424 let mut cursor = node.walk();
426 for child in node.children(&mut cursor) {
427 extract_namespaces_recursive(child, content, current_ns, map, guard)?;
428 }
429 }
430
431 guard.exit();
432 Ok(())
433}
434
435fn extract_identifier(node: Node, content: &[u8]) -> String {
437 node.utf8_text(content).unwrap_or("").to_string()
438}
439
440fn find_namespace_for_offset(
442 byte_offset: usize,
443 namespace_map: &HashMap<std::ops::Range<usize>, String>,
444) -> String {
445 let mut matching_ranges: Vec<_> = namespace_map
447 .iter()
448 .filter(|(range, _)| range.contains(&byte_offset))
449 .collect();
450
451 matching_ranges.sort_by_key(|(range, _)| range.end - range.start);
453
454 matching_ranges
456 .first()
457 .map_or("", |(_, ns)| ns.as_str())
458 .to_string()
459}
460
461fn extract_cpp_contexts(
468 node: Node,
469 content: &[u8],
470 namespace_map: &HashMap<std::ops::Range<usize>, String>,
471) -> Vec<FunctionContext> {
472 let mut contexts = Vec::new();
473 let mut class_stack = Vec::new();
474
475 let recursion_limits = sqry_core::config::RecursionLimits::load_or_default()
477 .expect("Failed to load recursion limits");
478 let file_ops_depth = recursion_limits
479 .effective_file_ops_depth()
480 .expect("Invalid file_ops_depth configuration");
481 let mut guard = sqry_core::query::security::RecursionGuard::new(file_ops_depth)
482 .expect("Failed to create recursion guard");
483
484 if let Err(e) = extract_contexts_recursive(
485 node,
486 content,
487 namespace_map,
488 &mut contexts,
489 &mut class_stack,
490 0, &mut guard,
492 ) {
493 eprintln!("Warning: C++ AST traversal hit recursion limit: {e}");
494 }
495
496 contexts
497}
498
499fn extract_contexts_recursive(
504 node: Node,
505 content: &[u8],
506 namespace_map: &HashMap<std::ops::Range<usize>, String>,
507 contexts: &mut Vec<FunctionContext>,
508 class_stack: &mut Vec<String>,
509 depth: usize,
510 guard: &mut sqry_core::query::security::RecursionGuard,
511) -> Result<(), sqry_core::query::security::RecursionError> {
512 guard.enter()?;
513
514 match node.kind() {
515 "class_specifier" | "struct_specifier" => {
516 if let Some(name_node) = node.child_by_field_name("name") {
518 let class_name = extract_identifier(name_node, content);
519 class_stack.push(class_name);
520
521 if let Some(body) = node.child_by_field_name("body") {
523 let mut cursor = body.walk();
524 for child in body.children(&mut cursor) {
525 extract_contexts_recursive(
526 child,
527 content,
528 namespace_map,
529 contexts,
530 class_stack,
531 depth + 1,
532 guard,
533 )?;
534 }
535 }
536
537 class_stack.pop();
538 }
539 }
540
541 "function_definition" => {
542 if let Some(declarator) = node.child_by_field_name("declarator") {
544 let (func_name, class_prefix) =
545 extract_function_name_with_class(declarator, content);
546
547 let namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
549 let namespace_stack: Vec<String> = if namespace.is_empty() {
550 Vec::new()
551 } else {
552 namespace
553 .trim_end_matches("::")
554 .split("::")
555 .map(String::from)
556 .collect()
557 };
558
559 let effective_class_stack: Vec<String> = if !class_stack.is_empty() {
563 class_stack.clone()
564 } else if let Some(ref prefix) = class_prefix {
565 vec![prefix.clone()]
566 } else {
567 Vec::new()
568 };
569
570 let qualified_name =
572 build_qualified_name(&namespace_stack, &effective_class_stack, &func_name);
573
574 let is_static = is_static_function(node, content);
576 let is_virtual = is_virtual_function(node, content);
577 let is_inline = is_inline_function(node, content);
578
579 let return_type = node
581 .child_by_field_name("type")
582 .and_then(|type_node| type_node.utf8_text(content).ok())
583 .map(std::string::ToString::to_string);
584
585 let span = (node.start_byte(), node.end_byte());
587
588 contexts.push(FunctionContext {
589 qualified_name,
590 span,
591 depth,
592 is_static,
593 is_virtual,
594 is_inline,
595 namespace_stack,
596 class_stack: effective_class_stack,
597 return_type,
598 });
599 }
600
601 }
603
604 _ => {
605 let mut cursor = node.walk();
607 for child in node.children(&mut cursor) {
608 extract_contexts_recursive(
609 child,
610 content,
611 namespace_map,
612 contexts,
613 class_stack,
614 depth,
615 guard,
616 )?;
617 }
618 }
619 }
620
621 guard.exit();
622 Ok(())
623}
624
625fn build_qualified_name(namespace_stack: &[String], class_stack: &[String], name: &str) -> String {
630 let mut parts = Vec::new();
631
632 parts.extend(namespace_stack.iter().cloned());
634
635 for class_name in class_stack {
637 parts.push(class_name.clone());
638 }
639
640 parts.push(name.to_string());
642
643 parts.join("::")
644}
645
646fn extract_function_name_with_class(declarator: Node, content: &[u8]) -> (String, Option<String>) {
651 match declarator.kind() {
659 "function_declarator" => {
660 if let Some(declarator_inner) = declarator.child_by_field_name("declarator") {
662 extract_function_name_with_class(declarator_inner, content)
663 } else {
664 (extract_identifier(declarator, content), None)
665 }
666 }
667 "qualified_identifier" => {
668 let name = if let Some(name_node) = declarator.child_by_field_name("name") {
670 extract_identifier(name_node, content)
671 } else {
672 extract_identifier(declarator, content)
673 };
674
675 let class_prefix = declarator
677 .child_by_field_name("scope")
678 .map(|scope_node| extract_identifier(scope_node, content));
679
680 (name, class_prefix)
681 }
682 "field_identifier" | "identifier" | "destructor_name" | "operator_name" => {
683 (extract_identifier(declarator, content), None)
684 }
685 _ => {
686 (extract_identifier(declarator, content), None)
688 }
689 }
690}
691
692#[allow(dead_code)]
694fn extract_function_name(declarator: Node, content: &[u8]) -> String {
695 extract_function_name_with_class(declarator, content).0
696}
697
698fn is_static_function(node: Node, content: &[u8]) -> bool {
700 has_specifier(node, "static", content)
701}
702
703fn is_virtual_function(node: Node, content: &[u8]) -> bool {
705 has_specifier(node, "virtual", content)
706}
707
708fn is_inline_function(node: Node, content: &[u8]) -> bool {
710 has_specifier(node, "inline", content)
711}
712
713fn has_specifier(node: Node, specifier: &str, content: &[u8]) -> bool {
715 let mut cursor = node.walk();
717 for child in node.children(&mut cursor) {
718 if (child.kind() == "storage_class_specifier"
719 || child.kind() == "type_qualifier"
720 || child.kind() == "virtual"
721 || child.kind() == "inline")
722 && let Ok(text) = child.utf8_text(content)
723 && text == specifier
724 {
725 return true;
726 }
727 }
728 false
729}
730
731fn extract_field_and_type_info(
741 node: Node,
742 content: &[u8],
743 namespace_map: &HashMap<std::ops::Range<usize>, String>,
744) -> (QualifiedNameMap, QualifiedNameMap) {
745 let mut field_types = HashMap::new();
746 let mut type_map = HashMap::new();
747 let mut class_stack = Vec::new();
748
749 extract_fields_recursive(
750 node,
751 content,
752 namespace_map,
753 &mut field_types,
754 &mut type_map,
755 &mut class_stack,
756 );
757
758 (field_types, type_map)
759}
760
761fn extract_fields_recursive(
763 node: Node,
764 content: &[u8],
765 namespace_map: &HashMap<std::ops::Range<usize>, String>,
766 field_types: &mut HashMap<(String, String), String>,
767 type_map: &mut HashMap<(String, String), String>,
768 class_stack: &mut Vec<String>,
769) {
770 match node.kind() {
771 "class_specifier" | "struct_specifier" => {
772 if let Some(name_node) = node.child_by_field_name("name") {
774 let class_name = extract_identifier(name_node, content);
775 let namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
776
777 let class_fqn = if class_stack.is_empty() {
779 if namespace.is_empty() {
781 class_name.clone()
782 } else {
783 format!("{}::{}", namespace.trim_end_matches("::"), class_name)
784 }
785 } else {
786 format!("{}::{}", class_stack.last().unwrap(), class_name)
788 };
789
790 class_stack.push(class_fqn.clone());
791
792 let mut cursor = node.walk();
794 for child in node.children(&mut cursor) {
795 extract_fields_recursive(
796 child,
797 content,
798 namespace_map,
799 field_types,
800 type_map,
801 class_stack,
802 );
803 }
804
805 class_stack.pop();
806 }
807 }
808
809 "field_declaration" => {
810 if let Some(class_fqn) = class_stack.last() {
812 extract_field_declaration(
813 node,
814 content,
815 class_fqn,
816 namespace_map,
817 field_types,
818 type_map,
819 );
820 }
821 }
822
823 "using_directive" => {
824 extract_using_directive(node, content, namespace_map, type_map);
826 }
827
828 "using_declaration" => {
829 extract_using_declaration(node, content, namespace_map, type_map);
831 }
832
833 _ => {
834 let mut cursor = node.walk();
836 for child in node.children(&mut cursor) {
837 extract_fields_recursive(
838 child,
839 content,
840 namespace_map,
841 field_types,
842 type_map,
843 class_stack,
844 );
845 }
846 }
847 }
848}
849
850fn extract_field_declaration(
852 node: Node,
853 content: &[u8],
854 class_fqn: &str,
855 namespace_map: &HashMap<std::ops::Range<usize>, String>,
856 field_types: &mut HashMap<(String, String), String>,
857 type_map: &HashMap<(String, String), String>,
858) {
859 let mut field_type = None;
864 let mut field_names = Vec::new();
865
866 let mut cursor = node.walk();
867 for child in node.children(&mut cursor) {
868 match child.kind() {
869 "type_identifier" | "primitive_type" | "qualified_identifier" | "template_type" => {
870 field_type = Some(extract_type_name(child, content));
871 }
872 "field_identifier" => {
873 field_names.push(extract_identifier(child, content));
875 }
876 "field_declarator"
877 | "init_declarator"
878 | "pointer_declarator"
879 | "reference_declarator"
880 | "array_declarator" => {
881 if let Some(name) = extract_field_name(child, content) {
883 field_names.push(name);
884 }
885 }
886 _ => {}
887 }
888 }
889
890 if let Some(ftype) = field_type {
892 let namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
893 let field_type_fqn = resolve_type_to_fqn(&ftype, &namespace, type_map);
894
895 for fname in field_names {
897 field_types.insert((class_fqn.to_string(), fname), field_type_fqn.clone());
898 }
899 }
900}
901
902fn extract_type_name(type_node: Node, content: &[u8]) -> String {
904 match type_node.kind() {
905 "type_identifier" | "primitive_type" => extract_identifier(type_node, content),
906 "qualified_identifier" => {
907 extract_identifier(type_node, content)
909 }
910 "template_type" => {
911 if let Some(name) = type_node.child_by_field_name("name") {
913 extract_identifier(name, content)
914 } else {
915 extract_identifier(type_node, content)
916 }
917 }
918 _ => {
919 extract_identifier(type_node, content)
921 }
922 }
923}
924
925fn extract_field_name(declarator: Node, content: &[u8]) -> Option<String> {
927 match declarator.kind() {
928 "field_declarator" => {
929 if let Some(declarator_inner) = declarator.child_by_field_name("declarator") {
931 extract_field_name(declarator_inner, content)
932 } else {
933 Some(extract_identifier(declarator, content))
934 }
935 }
936 "field_identifier" | "identifier" => Some(extract_identifier(declarator, content)),
937 "pointer_declarator" | "reference_declarator" | "array_declarator" => {
938 if let Some(declarator_inner) = declarator.child_by_field_name("declarator") {
940 extract_field_name(declarator_inner, content)
941 } else {
942 None
943 }
944 }
945 "init_declarator" => {
946 if let Some(declarator_inner) = declarator.child_by_field_name("declarator") {
948 extract_field_name(declarator_inner, content)
949 } else {
950 None
951 }
952 }
953 _ => None,
954 }
955}
956
957fn resolve_type_to_fqn(
959 type_name: &str,
960 namespace: &str,
961 type_map: &HashMap<(String, String), String>,
962) -> String {
963 if type_name.contains("::") {
965 return type_name.to_string();
966 }
967
968 let namespace_key = namespace.trim_end_matches("::").to_string();
970 if let Some(fqn) = type_map.get(&(namespace_key.clone(), type_name.to_string())) {
971 return fqn.clone();
972 }
973
974 if let Some(fqn) = type_map.get(&(String::new(), type_name.to_string())) {
976 return fqn.clone();
977 }
978
979 type_name.to_string()
981}
982
983fn extract_using_directive(
985 node: Node,
986 content: &[u8],
987 namespace_map: &HashMap<std::ops::Range<usize>, String>,
988 _type_map: &mut HashMap<(String, String), String>,
989) {
990 let _namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
994
995 if let Some(name_node) = node.child_by_field_name("name") {
997 let _using_ns = extract_identifier(name_node, content);
998 }
1002}
1003
1004fn extract_using_declaration(
1009 node: Node,
1010 content: &[u8],
1011 namespace_map: &HashMap<std::ops::Range<usize>, String>,
1012 type_map: &mut HashMap<(String, String), String>,
1013) {
1014 let namespace = find_namespace_for_offset(node.start_byte(), namespace_map);
1015 let namespace_key = namespace.trim_end_matches("::").to_string();
1016
1017 let mut cursor = node.walk();
1019 for child in node.children(&mut cursor) {
1020 if child.kind() == "qualified_identifier" || child.kind() == "identifier" {
1021 let fqn = extract_identifier(child, content);
1022
1023 if let Some(simple_name) = fqn.split("::").last() {
1025 type_map.insert((namespace_key, simple_name.to_string()), fqn);
1027 }
1028 break;
1029 }
1030 }
1031}
1032
1033fn resolve_callee_name(
1045 callee_name: &str,
1046 caller_ctx: &FunctionContext,
1047 _ast_graph: &ASTGraph,
1048) -> String {
1049 if callee_name.starts_with("::") {
1051 return callee_name.trim_start_matches("::").to_string();
1052 }
1053
1054 if callee_name.contains("::") {
1056 if !caller_ctx.namespace_stack.is_empty() {
1058 let namespace_prefix = caller_ctx.namespace_stack.join("::");
1059 return format!("{namespace_prefix}::{callee_name}");
1060 }
1061 return callee_name.to_string();
1062 }
1063
1064 let mut parts = Vec::new();
1066
1067 if !caller_ctx.namespace_stack.is_empty() {
1069 parts.extend(caller_ctx.namespace_stack.iter().cloned());
1070 }
1071
1072 parts.push(callee_name.to_string());
1078
1079 parts.join("::")
1080}
1081
1082fn strip_type_qualifiers(type_text: &str) -> String {
1089 let mut result = type_text.trim().to_string();
1090
1091 result = result.replace("const ", "");
1093 result = result.replace("volatile ", "");
1094 result = result.replace("mutable ", "");
1095 result = result.replace("constexpr ", "");
1096
1097 result = result.replace(" const", "");
1099 result = result.replace(" volatile", "");
1100 result = result.replace(" mutable", "");
1101 result = result.replace(" constexpr", "");
1102
1103 result = result.replace(['*', '&'], "");
1105
1106 result = result.trim().to_string();
1108
1109 if let Some(last_part) = result.split("::").last() {
1111 result = last_part.to_string();
1112 }
1113
1114 if let Some(open_bracket) = result.find('<') {
1116 result = result[..open_bracket].to_string();
1117 }
1118
1119 result.trim().to_string()
1120}
1121
1122#[allow(clippy::unnecessary_wraps)]
1124fn process_field_declaration(
1125 node: Node,
1126 content: &[u8],
1127 class_qualified_name: &str,
1128 visibility: &str,
1129 helper: &mut GraphBuildHelper,
1130) -> GraphResult<()> {
1131 let mut field_type_text = None;
1133 let mut field_names = Vec::new();
1134
1135 let mut cursor = node.walk();
1136 for child in node.children(&mut cursor) {
1137 match child.kind() {
1138 "type_identifier" | "primitive_type" => {
1139 if let Ok(text) = child.utf8_text(content) {
1140 field_type_text = Some(text.to_string());
1141 }
1142 }
1143 "qualified_identifier" => {
1144 if let Ok(text) = child.utf8_text(content) {
1146 field_type_text = Some(text.to_string());
1147 }
1148 }
1149 "template_type" => {
1150 if let Ok(text) = child.utf8_text(content) {
1152 field_type_text = Some(text.to_string());
1153 }
1154 }
1155 "sized_type_specifier" => {
1156 if let Ok(text) = child.utf8_text(content) {
1158 field_type_text = Some(text.to_string());
1159 }
1160 }
1161 "type_qualifier" => {
1162 if field_type_text.is_none()
1164 && let Ok(text) = child.utf8_text(content)
1165 {
1166 field_type_text = Some(text.to_string());
1167 }
1168 }
1169 "auto" => {
1170 field_type_text = Some("auto".to_string());
1172 }
1173 "decltype" => {
1174 if let Ok(text) = child.utf8_text(content) {
1176 field_type_text = Some(text.to_string());
1177 }
1178 }
1179 "struct_specifier" | "class_specifier" | "enum_specifier" | "union_specifier" => {
1180 if let Ok(text) = child.utf8_text(content) {
1182 field_type_text = Some(text.to_string());
1183 }
1184 }
1185 "field_identifier" => {
1186 if let Ok(name) = child.utf8_text(content) {
1187 field_names.push(name.trim().to_string());
1188 }
1189 }
1190 "field_declarator"
1191 | "pointer_declarator"
1192 | "reference_declarator"
1193 | "init_declarator" => {
1194 if let Some(name) = extract_field_name(child, content) {
1196 field_names.push(name);
1197 }
1198 }
1199 _ => {}
1200 }
1201 }
1202
1203 if let Some(type_text) = field_type_text {
1205 let base_type = strip_type_qualifiers(&type_text);
1206
1207 for field_name in field_names {
1208 let field_qualified = format!("{class_qualified_name}::{field_name}");
1209 let span = span_from_node(node);
1210
1211 let var_id = helper.add_node_with_visibility(
1213 &field_qualified,
1214 Some(span),
1215 sqry_core::graph::unified::node::NodeKind::Variable,
1216 Some(visibility),
1217 );
1218
1219 let type_id = helper.add_type(&base_type, None);
1221
1222 helper.add_typeof_edge(var_id, type_id);
1224
1225 helper.add_reference_edge(var_id, type_id);
1227 }
1228 }
1229
1230 Ok(())
1231}
1232
1233#[allow(clippy::unnecessary_wraps)]
1235fn process_global_variable_declaration(
1236 node: Node,
1237 content: &[u8],
1238 namespace_stack: &[String],
1239 helper: &mut GraphBuildHelper,
1240) -> GraphResult<()> {
1241 if node.kind() != "declaration" {
1243 return Ok(());
1244 }
1245
1246 let mut cursor_check = node.walk();
1249 for child in node.children(&mut cursor_check) {
1250 if child.kind() == "function_declarator" {
1251 return Ok(());
1252 }
1253 }
1254
1255 let mut type_text = None;
1257 let mut var_names = Vec::new();
1258
1259 let mut cursor = node.walk();
1260 for child in node.children(&mut cursor) {
1261 match child.kind() {
1262 "type_identifier" | "primitive_type" | "qualified_identifier" | "template_type" => {
1263 if let Ok(text) = child.utf8_text(content) {
1264 type_text = Some(text.to_string());
1265 }
1266 }
1267 "init_declarator" => {
1268 if let Some(declarator) = child.child_by_field_name("declarator")
1270 && let Some(name) = extract_declarator_name(declarator, content)
1271 {
1272 var_names.push(name);
1273 }
1274 }
1275 "pointer_declarator" | "reference_declarator" => {
1276 if let Some(name) = extract_declarator_name(child, content) {
1277 var_names.push(name);
1278 }
1279 }
1280 "identifier" => {
1281 if let Ok(name) = child.utf8_text(content) {
1283 var_names.push(name.to_string());
1284 }
1285 }
1286 _ => {}
1287 }
1288 }
1289
1290 if let Some(type_text) = type_text {
1291 let base_type = strip_type_qualifiers(&type_text);
1292
1293 for var_name in var_names {
1294 let qualified = if namespace_stack.is_empty() {
1296 var_name.clone()
1297 } else {
1298 format!("{}::{}", namespace_stack.join("::"), var_name)
1299 };
1300
1301 let span = span_from_node(node);
1302
1303 let var_id = helper.add_node_with_visibility(
1305 &qualified,
1306 Some(span),
1307 sqry_core::graph::unified::node::NodeKind::Variable,
1308 Some("public"),
1309 );
1310
1311 let type_id = helper.add_type(&base_type, None);
1313
1314 helper.add_typeof_edge(var_id, type_id);
1316 helper.add_reference_edge(var_id, type_id);
1317 }
1318 }
1319
1320 Ok(())
1321}
1322
1323fn extract_declarator_name(node: Node, content: &[u8]) -> Option<String> {
1325 match node.kind() {
1326 "identifier" => {
1327 if let Ok(name) = node.utf8_text(content) {
1328 Some(name.to_string())
1329 } else {
1330 None
1331 }
1332 }
1333 "pointer_declarator" | "reference_declarator" | "array_declarator" => {
1334 if let Some(inner) = node.child_by_field_name("declarator") {
1336 extract_declarator_name(inner, content)
1337 } else {
1338 let mut cursor = node.walk();
1340 for child in node.children(&mut cursor) {
1341 if child.kind() == "identifier"
1342 && let Ok(name) = child.utf8_text(content)
1343 {
1344 return Some(name.to_string());
1345 }
1346 }
1347 None
1348 }
1349 }
1350 "init_declarator" => {
1351 if let Some(inner) = node.child_by_field_name("declarator") {
1353 extract_declarator_name(inner, content)
1354 } else {
1355 None
1356 }
1357 }
1358 "field_declarator" => {
1359 if let Some(inner) = node.child_by_field_name("declarator") {
1361 extract_declarator_name(inner, content)
1362 } else {
1363 if let Ok(name) = node.utf8_text(content) {
1365 Some(name.to_string())
1366 } else {
1367 None
1368 }
1369 }
1370 }
1371 _ => None,
1372 }
1373}
1374
1375#[allow(clippy::too_many_arguments)]
1377fn walk_class_body(
1378 body_node: Node,
1379 content: &[u8],
1380 class_qualified_name: &str,
1381 is_struct: bool,
1382 ast_graph: &ASTGraph,
1383 helper: &mut GraphBuildHelper,
1384 seen_includes: &mut HashSet<String>,
1385 namespace_stack: &mut Vec<String>,
1386 class_stack: &mut Vec<String>,
1387 ffi_registry: &FfiRegistry,
1388 pure_virtual_registry: &PureVirtualRegistry,
1389) -> GraphResult<()> {
1390 let mut current_visibility = if is_struct { "public" } else { "private" };
1392
1393 let mut cursor = body_node.walk();
1394 for child in body_node.children(&mut cursor) {
1395 match child.kind() {
1396 "access_specifier" => {
1397 if let Ok(text) = child.utf8_text(content) {
1399 let spec = text.trim().trim_end_matches(':').trim();
1400 current_visibility = spec;
1401 }
1402 }
1403 "field_declaration" => {
1404 process_field_declaration(
1406 child,
1407 content,
1408 class_qualified_name,
1409 current_visibility,
1410 helper,
1411 )?;
1412 }
1413 "function_definition" => {
1414 if let Some(context) = ast_graph
1417 .contexts
1418 .iter()
1419 .find(|ctx| ctx.span.0 == child.start_byte())
1420 {
1421 let span = span_from_node(child);
1422 helper.add_method_with_signature(
1423 &context.qualified_name,
1424 Some(span),
1425 false, context.is_static,
1427 Some(current_visibility),
1428 context.return_type.as_deref(),
1429 );
1430 }
1431 walk_tree_for_graph(
1433 child,
1434 content,
1435 ast_graph,
1436 helper,
1437 seen_includes,
1438 namespace_stack,
1439 class_stack,
1440 ffi_registry,
1441 pure_virtual_registry,
1442 )?;
1443 }
1444 _ => {
1445 walk_tree_for_graph(
1447 child,
1448 content,
1449 ast_graph,
1450 helper,
1451 seen_includes,
1452 namespace_stack,
1453 class_stack,
1454 ffi_registry,
1455 pure_virtual_registry,
1456 )?;
1457 }
1458 }
1459 }
1460
1461 Ok(())
1462}
1463
1464#[allow(clippy::too_many_arguments)]
1466#[allow(clippy::too_many_lines)] fn walk_tree_for_graph(
1468 node: Node,
1469 content: &[u8],
1470 ast_graph: &ASTGraph,
1471 helper: &mut GraphBuildHelper,
1472 seen_includes: &mut HashSet<String>,
1473 namespace_stack: &mut Vec<String>,
1474 class_stack: &mut Vec<String>,
1475 ffi_registry: &FfiRegistry,
1476 pure_virtual_registry: &PureVirtualRegistry,
1477) -> GraphResult<()> {
1478 match node.kind() {
1479 "preproc_include" => {
1480 build_import_edge(node, content, helper, seen_includes)?;
1482 }
1483 "linkage_specification" => {
1484 build_ffi_block_for_staging(node, content, helper, namespace_stack);
1486 }
1487 "namespace_definition" => {
1488 if let Some(name_node) = node.child_by_field_name("name")
1490 && let Ok(ns_name) = name_node.utf8_text(content)
1491 {
1492 namespace_stack.push(ns_name.trim().to_string());
1493
1494 let mut cursor = node.walk();
1496 for child in node.children(&mut cursor) {
1497 walk_tree_for_graph(
1498 child,
1499 content,
1500 ast_graph,
1501 helper,
1502 seen_includes,
1503 namespace_stack,
1504 class_stack,
1505 ffi_registry,
1506 pure_virtual_registry,
1507 )?;
1508 }
1509
1510 namespace_stack.pop();
1511 return Ok(());
1512 }
1513 }
1514 "class_specifier" | "struct_specifier" => {
1515 if let Some(name_node) = node.child_by_field_name("name")
1517 && let Ok(class_name) = name_node.utf8_text(content)
1518 {
1519 let class_name = class_name.trim();
1520 let span = span_from_node(node);
1521 let is_struct = node.kind() == "struct_specifier";
1522
1523 let qualified_class =
1525 build_qualified_name(namespace_stack, class_stack, class_name);
1526
1527 let visibility = "public";
1529 let class_id = if is_struct {
1530 helper.add_struct_with_visibility(
1531 &qualified_class,
1532 Some(span),
1533 Some(visibility),
1534 )
1535 } else {
1536 helper.add_class_with_visibility(&qualified_class, Some(span), Some(visibility))
1537 };
1538
1539 build_inheritance_and_implements_edges(
1542 node,
1543 content,
1544 &qualified_class,
1545 class_id,
1546 helper,
1547 namespace_stack,
1548 pure_virtual_registry,
1549 )?;
1550
1551 if class_stack.is_empty() {
1554 let module_id = helper.add_module(FILE_MODULE_NAME, None);
1555 helper.add_export_edge(module_id, class_id);
1556 }
1557
1558 class_stack.push(class_name.to_string());
1560
1561 if let Some(body) = node.child_by_field_name("body") {
1564 walk_class_body(
1565 body,
1566 content,
1567 &qualified_class,
1568 is_struct,
1569 ast_graph,
1570 helper,
1571 seen_includes,
1572 namespace_stack,
1573 class_stack,
1574 ffi_registry,
1575 pure_virtual_registry,
1576 )?;
1577 }
1578
1579 class_stack.pop();
1580 return Ok(());
1581 }
1582 }
1583 "enum_specifier" => {
1584 if let Some(name_node) = node.child_by_field_name("name")
1585 && let Ok(enum_name) = name_node.utf8_text(content)
1586 {
1587 let enum_name = enum_name.trim();
1588 let span = span_from_node(node);
1589 let qualified_enum = build_qualified_name(namespace_stack, class_stack, enum_name);
1590 let enum_id = helper.add_enum(&qualified_enum, Some(span));
1591
1592 if class_stack.is_empty() {
1593 let module_id = helper.add_module(FILE_MODULE_NAME, None);
1594 helper.add_export_edge(module_id, enum_id);
1595 }
1596 }
1597 }
1598 "function_definition" => {
1599 if !class_stack.is_empty() {
1603 let mut cursor = node.walk();
1606 for child in node.children(&mut cursor) {
1607 walk_tree_for_graph(
1608 child,
1609 content,
1610 ast_graph,
1611 helper,
1612 seen_includes,
1613 namespace_stack,
1614 class_stack,
1615 ffi_registry,
1616 pure_virtual_registry,
1617 )?;
1618 }
1619 return Ok(());
1620 }
1621
1622 if let Some(context) = ast_graph
1624 .contexts
1625 .iter()
1626 .find(|ctx| ctx.span.0 == node.start_byte())
1627 {
1628 let span = span_from_node(node);
1629
1630 if context.class_stack.is_empty() {
1632 let visibility = if context.is_static {
1635 "private"
1636 } else {
1637 "public"
1638 };
1639 let fn_id = helper.add_function_with_signature(
1640 &context.qualified_name,
1641 Some(span),
1642 false, false, Some(visibility),
1645 context.return_type.as_deref(),
1646 );
1647
1648 if !context.is_static {
1650 let module_id = helper.add_module(FILE_MODULE_NAME, None);
1651 helper.add_export_edge(module_id, fn_id);
1652 }
1653 } else {
1654 helper.add_method_with_signature(
1659 &context.qualified_name,
1660 Some(span),
1661 false, context.is_static,
1663 Some("public"), context.return_type.as_deref(),
1665 );
1666 }
1667 }
1668 }
1669 "call_expression" => {
1670 if let Ok(Some((caller_qname, callee_qname, argument_count, span))) =
1672 build_call_for_staging(ast_graph, node, content)
1673 {
1674 let caller_function_id = helper.ensure_function(&caller_qname, None, false, false);
1676 let argument_count = u8::try_from(argument_count).unwrap_or(u8::MAX);
1677
1678 let is_unqualified = !callee_qname.contains("::");
1681 if is_unqualified {
1682 if let Some((ffi_qualified, ffi_convention)) = ffi_registry.get(&callee_qname) {
1683 let ffi_target_id =
1685 helper.ensure_function(ffi_qualified, None, false, true);
1686 helper.add_ffi_edge(caller_function_id, ffi_target_id, *ffi_convention);
1687 } else {
1688 let target_function_id =
1690 helper.ensure_function(&callee_qname, None, false, false);
1691 helper.add_call_edge_full_with_span(
1692 caller_function_id,
1693 target_function_id,
1694 argument_count,
1695 false,
1696 vec![span],
1697 );
1698 }
1699 } else {
1700 let target_function_id =
1702 helper.ensure_function(&callee_qname, None, false, false);
1703 helper.add_call_edge_full_with_span(
1704 caller_function_id,
1705 target_function_id,
1706 argument_count,
1707 false,
1708 vec![span],
1709 );
1710 }
1711 }
1712 }
1713 "declaration" => {
1714 if class_stack.is_empty() {
1717 process_global_variable_declaration(node, content, namespace_stack, helper)?;
1718 }
1719 }
1720 _ => {}
1721 }
1722
1723 let mut cursor = node.walk();
1725 for child in node.children(&mut cursor) {
1726 walk_tree_for_graph(
1727 child,
1728 content,
1729 ast_graph,
1730 helper,
1731 seen_includes,
1732 namespace_stack,
1733 class_stack,
1734 ffi_registry,
1735 pure_virtual_registry,
1736 )?;
1737 }
1738
1739 Ok(())
1740}
1741
1742fn build_call_for_staging(
1744 ast_graph: &ASTGraph,
1745 call_node: Node<'_>,
1746 content: &[u8],
1747) -> GraphResult<Option<(String, String, usize, Span)>> {
1748 let call_context = ast_graph.find_enclosing(call_node.start_byte());
1750 let caller_qualified_name = if let Some(ctx) = call_context {
1751 ctx.qualified_name.clone()
1752 } else {
1753 return Ok(None);
1755 };
1756
1757 let Some(function_node) = call_node.child_by_field_name("function") else {
1758 return Ok(None);
1759 };
1760
1761 let callee_text = function_node
1762 .utf8_text(content)
1763 .map_err(|_| GraphBuilderError::ParseError {
1764 span: span_from_node(call_node),
1765 reason: "failed to read call expression".to_string(),
1766 })?
1767 .trim();
1768
1769 if callee_text.is_empty() {
1770 return Ok(None);
1771 }
1772
1773 let target_qualified_name = if let Some(ctx) = call_context {
1775 resolve_callee_name(callee_text, ctx, ast_graph)
1776 } else {
1777 callee_text.to_string()
1778 };
1779
1780 let span = span_from_node(call_node);
1781 let argument_count = count_arguments(call_node);
1782
1783 Ok(Some((
1784 caller_qualified_name,
1785 target_qualified_name,
1786 argument_count,
1787 span,
1788 )))
1789}
1790
1791fn build_import_edge(
1798 include_node: Node<'_>,
1799 content: &[u8],
1800 helper: &mut GraphBuildHelper,
1801 seen_includes: &mut HashSet<String>,
1802) -> GraphResult<()> {
1803 let path_node = include_node.child_by_field_name("path").or_else(|| {
1805 let mut cursor = include_node.walk();
1807 include_node.children(&mut cursor).find(|child| {
1808 matches!(
1809 child.kind(),
1810 "system_lib_string" | "string_literal" | "string_content"
1811 )
1812 })
1813 });
1814
1815 let Some(path_node) = path_node else {
1816 return Ok(());
1817 };
1818
1819 let include_path = path_node
1820 .utf8_text(content)
1821 .map_err(|_| GraphBuilderError::ParseError {
1822 span: span_from_node(include_node),
1823 reason: "failed to read include path".to_string(),
1824 })?
1825 .trim();
1826
1827 if include_path.is_empty() {
1828 return Ok(());
1829 }
1830
1831 let is_system_include = include_path.starts_with('<') && include_path.ends_with('>');
1833 let cleaned_path = if is_system_include {
1834 include_path.trim_start_matches('<').trim_end_matches('>')
1836 } else {
1837 include_path.trim_start_matches('"').trim_end_matches('"')
1839 };
1840
1841 if cleaned_path.is_empty() {
1842 return Ok(());
1843 }
1844
1845 if !seen_includes.insert(cleaned_path.to_string()) {
1847 return Ok(()); }
1849
1850 let file_module_id = helper.add_module("<file>", None);
1852
1853 let span = span_from_node(include_node);
1855 let import_id = helper.add_import(cleaned_path, Some(span));
1856
1857 helper.add_import_edge(file_module_id, import_id);
1860
1861 Ok(())
1862}
1863
1864fn collect_ffi_declarations(node: Node<'_>, content: &[u8], ffi_registry: &mut FfiRegistry) {
1875 if node.kind() == "linkage_specification" {
1876 let abi = extract_ffi_abi(node, content);
1878 let convention = abi_to_convention(&abi);
1879
1880 if let Some(body_node) = node.child_by_field_name("body") {
1882 collect_ffi_from_body(body_node, content, &abi, convention, ffi_registry);
1883 }
1884 }
1885
1886 let mut cursor = node.walk();
1888 for child in node.children(&mut cursor) {
1889 collect_ffi_declarations(child, content, ffi_registry);
1890 }
1891}
1892
1893fn collect_ffi_from_body(
1895 body_node: Node<'_>,
1896 content: &[u8],
1897 abi: &str,
1898 convention: FfiConvention,
1899 ffi_registry: &mut FfiRegistry,
1900) {
1901 match body_node.kind() {
1902 "declaration_list" => {
1903 let mut cursor = body_node.walk();
1905 for decl in body_node.children(&mut cursor) {
1906 if decl.kind() == "declaration"
1907 && let Some(fn_name) = extract_ffi_function_name(decl, content)
1908 {
1909 let qualified = format!("extern::{abi}::{fn_name}");
1910 ffi_registry.insert(fn_name, (qualified, convention));
1911 }
1912 }
1913 }
1914 "declaration" => {
1915 if let Some(fn_name) = extract_ffi_function_name(body_node, content) {
1917 let qualified = format!("extern::{abi}::{fn_name}");
1918 ffi_registry.insert(fn_name, (qualified, convention));
1919 }
1920 }
1921 _ => {}
1922 }
1923}
1924
1925fn extract_ffi_function_name(decl_node: Node<'_>, content: &[u8]) -> Option<String> {
1927 if let Some(declarator_node) = decl_node.child_by_field_name("declarator") {
1929 return extract_function_name_from_declarator(declarator_node, content);
1930 }
1931 None
1932}
1933
1934fn extract_function_name_from_declarator(node: Node<'_>, content: &[u8]) -> Option<String> {
1936 match node.kind() {
1937 "function_declarator" => {
1938 if let Some(inner) = node.child_by_field_name("declarator") {
1940 return extract_function_name_from_declarator(inner, content);
1941 }
1942 }
1943 "identifier" => {
1944 if let Ok(name) = node.utf8_text(content) {
1946 let name = name.trim();
1947 if !name.is_empty() {
1948 return Some(name.to_string());
1949 }
1950 }
1951 }
1952 "pointer_declarator" | "reference_declarator" => {
1953 if let Some(inner) = node.child_by_field_name("declarator") {
1955 return extract_function_name_from_declarator(inner, content);
1956 }
1957 }
1958 "parenthesized_declarator" => {
1959 let mut cursor = node.walk();
1961 for child in node.children(&mut cursor) {
1962 if let Some(name) = extract_function_name_from_declarator(child, content) {
1963 return Some(name);
1964 }
1965 }
1966 }
1967 _ => {}
1968 }
1969 None
1970}
1971
1972fn extract_ffi_abi(node: Node<'_>, content: &[u8]) -> String {
1976 if let Some(value_node) = node.child_by_field_name("value")
1978 && value_node.kind() == "string_literal"
1979 {
1980 let mut cursor = value_node.walk();
1982 for child in value_node.children(&mut cursor) {
1983 if child.kind() == "string_content"
1984 && let Ok(text) = child.utf8_text(content)
1985 {
1986 let trimmed = text.trim();
1987 if !trimmed.is_empty() {
1988 return trimmed.to_string();
1989 }
1990 }
1991 }
1992 }
1993 "C".to_string()
1995}
1996
1997fn abi_to_convention(abi: &str) -> FfiConvention {
1999 match abi.to_lowercase().as_str() {
2000 "system" => FfiConvention::System,
2001 "stdcall" => FfiConvention::Stdcall,
2002 "fastcall" => FfiConvention::Fastcall,
2003 "cdecl" => FfiConvention::Cdecl,
2004 _ => FfiConvention::C, }
2006}
2007
2008fn build_ffi_block_for_staging(
2012 node: Node<'_>,
2013 content: &[u8],
2014 helper: &mut GraphBuildHelper,
2015 namespace_stack: &[String],
2016) {
2017 let abi = extract_ffi_abi(node, content);
2019
2020 if let Some(body_node) = node.child_by_field_name("body") {
2022 build_ffi_from_body(body_node, content, &abi, helper, namespace_stack);
2023 }
2024}
2025
2026fn build_ffi_from_body(
2028 body_node: Node<'_>,
2029 content: &[u8],
2030 abi: &str,
2031 helper: &mut GraphBuildHelper,
2032 namespace_stack: &[String],
2033) {
2034 match body_node.kind() {
2035 "declaration_list" => {
2036 let mut cursor = body_node.walk();
2038 for decl in body_node.children(&mut cursor) {
2039 if decl.kind() == "declaration"
2040 && let Some(fn_name) = extract_ffi_function_name(decl, content)
2041 {
2042 let span = span_from_node(decl);
2043 let qualified = if namespace_stack.is_empty() {
2045 format!("extern::{abi}::{fn_name}")
2046 } else {
2047 format!("{}::extern::{abi}::{fn_name}", namespace_stack.join("::"))
2048 };
2049 helper.add_function(
2051 &qualified,
2052 Some(span),
2053 false, true, );
2056 }
2057 }
2058 }
2059 "declaration" => {
2060 if let Some(fn_name) = extract_ffi_function_name(body_node, content) {
2062 let span = span_from_node(body_node);
2063 let qualified = if namespace_stack.is_empty() {
2064 format!("extern::{abi}::{fn_name}")
2065 } else {
2066 format!("{}::extern::{abi}::{fn_name}", namespace_stack.join("::"))
2067 };
2068 helper.add_function(&qualified, Some(span), false, true);
2069 }
2070 }
2071 _ => {}
2072 }
2073}
2074
2075fn collect_pure_virtual_interfaces(
2085 node: Node<'_>,
2086 content: &[u8],
2087 registry: &mut PureVirtualRegistry,
2088) {
2089 if matches!(node.kind(), "class_specifier" | "struct_specifier")
2090 && let Some(name_node) = node.child_by_field_name("name")
2091 && let Ok(class_name) = name_node.utf8_text(content)
2092 {
2093 let class_name = class_name.trim();
2094 if !class_name.is_empty() && has_pure_virtual_methods(node, content) {
2095 registry.insert(class_name.to_string());
2096 }
2097 }
2098
2099 let mut cursor = node.walk();
2101 for child in node.children(&mut cursor) {
2102 collect_pure_virtual_interfaces(child, content, registry);
2103 }
2104}
2105
2106fn has_pure_virtual_methods(class_node: Node<'_>, content: &[u8]) -> bool {
2110 if let Some(body) = class_node.child_by_field_name("body") {
2111 let mut cursor = body.walk();
2112 for child in body.children(&mut cursor) {
2113 if child.kind() == "field_declaration" && is_pure_virtual_declaration(child, content) {
2115 return true;
2116 }
2117 }
2118 }
2119 false
2120}
2121
2122fn is_pure_virtual_declaration(decl_node: Node<'_>, content: &[u8]) -> bool {
2124 let mut has_virtual = false;
2125 let mut has_pure_specifier = false;
2126
2127 let mut cursor = decl_node.walk();
2129 for child in decl_node.children(&mut cursor) {
2130 match child.kind() {
2131 "virtual" => {
2132 has_virtual = true;
2133 }
2134 "number_literal" => {
2135 if let Ok(text) = child.utf8_text(content)
2138 && text.trim() == "0"
2139 {
2140 has_pure_specifier = true;
2141 }
2142 }
2143 _ => {}
2144 }
2145 }
2146
2147 has_virtual && has_pure_specifier
2148}
2149
2150fn build_inheritance_and_implements_edges(
2156 class_node: Node<'_>,
2157 content: &[u8],
2158 _qualified_class_name: &str,
2159 child_id: sqry_core::graph::unified::node::NodeId,
2160 helper: &mut GraphBuildHelper,
2161 namespace_stack: &[String],
2162 pure_virtual_registry: &PureVirtualRegistry,
2163) -> GraphResult<()> {
2164 let mut cursor = class_node.walk();
2166 let base_clause = class_node
2167 .children(&mut cursor)
2168 .find(|child| child.kind() == "base_class_clause");
2169
2170 let Some(base_clause) = base_clause else {
2171 return Ok(()); };
2173
2174 let mut clause_cursor = base_clause.walk();
2176 for child in base_clause.children(&mut clause_cursor) {
2177 match child.kind() {
2178 "type_identifier" => {
2179 let base_name = child
2180 .utf8_text(content)
2181 .map_err(|_| GraphBuilderError::ParseError {
2182 span: span_from_node(child),
2183 reason: "failed to read base class name".to_string(),
2184 })?
2185 .trim();
2186
2187 if !base_name.is_empty() {
2188 let qualified_base = if namespace_stack.is_empty() {
2190 base_name.to_string()
2191 } else {
2192 format!("{}::{}", namespace_stack.join("::"), base_name)
2193 };
2194
2195 if pure_virtual_registry.contains(base_name) {
2197 let interface_id = helper.add_interface(&qualified_base, None);
2199 helper.add_implements_edge(child_id, interface_id);
2200 } else {
2201 let parent_id = helper.add_class(&qualified_base, None);
2203 helper.add_inherits_edge(child_id, parent_id);
2204 }
2205 }
2206 }
2207 "qualified_identifier" => {
2208 let base_name = child
2210 .utf8_text(content)
2211 .map_err(|_| GraphBuilderError::ParseError {
2212 span: span_from_node(child),
2213 reason: "failed to read base class name".to_string(),
2214 })?
2215 .trim();
2216
2217 if !base_name.is_empty() {
2218 let simple_name = base_name.rsplit("::").next().unwrap_or(base_name);
2220
2221 if pure_virtual_registry.contains(simple_name) {
2222 let interface_id = helper.add_interface(base_name, None);
2223 helper.add_implements_edge(child_id, interface_id);
2224 } else {
2225 let parent_id = helper.add_class(base_name, None);
2226 helper.add_inherits_edge(child_id, parent_id);
2227 }
2228 }
2229 }
2230 "template_type" => {
2231 if let Some(template_name_node) = child.child_by_field_name("name")
2233 && let Ok(base_name) = template_name_node.utf8_text(content)
2234 {
2235 let base_name = base_name.trim();
2236 if !base_name.is_empty() {
2237 let qualified_base =
2238 if base_name.contains("::") || namespace_stack.is_empty() {
2239 base_name.to_string()
2240 } else {
2241 format!("{}::{}", namespace_stack.join("::"), base_name)
2242 };
2243
2244 if pure_virtual_registry.contains(base_name) {
2247 let interface_id = helper.add_interface(&qualified_base, None);
2248 helper.add_implements_edge(child_id, interface_id);
2249 } else {
2250 let parent_id = helper.add_class(&qualified_base, None);
2251 helper.add_inherits_edge(child_id, parent_id);
2252 }
2253 }
2254 }
2255 }
2256 _ => {
2257 }
2259 }
2260 }
2261
2262 Ok(())
2263}
2264
2265fn span_from_node(node: Node<'_>) -> Span {
2266 let start = node.start_position();
2267 let end = node.end_position();
2268 Span::new(
2269 sqry_core::graph::node::Position::new(start.row, start.column),
2270 sqry_core::graph::node::Position::new(end.row, end.column),
2271 )
2272}
2273
2274fn count_arguments(node: Node<'_>) -> usize {
2275 node.child_by_field_name("arguments").map_or(0, |args| {
2276 let mut count = 0;
2277 let mut cursor = args.walk();
2278 for child in args.children(&mut cursor) {
2279 if !matches!(child.kind(), "(" | ")" | ",") {
2280 count += 1;
2281 }
2282 }
2283 count
2284 })
2285}
2286
2287#[cfg(test)]
2288mod tests {
2289 use super::*;
2290 use sqry_core::graph::unified::build::test_helpers::{
2291 assert_has_node, assert_has_node_with_kind, collect_call_edges,
2292 };
2293 use sqry_core::graph::unified::node::NodeKind;
2294 use tree_sitter::Parser;
2295
2296 fn parse_cpp(source: &str) -> Tree {
2297 let mut parser = Parser::new();
2298 parser
2299 .set_language(&tree_sitter_cpp::LANGUAGE.into())
2300 .expect("Failed to set Cpp language");
2301 parser
2302 .parse(source.as_bytes(), None)
2303 .expect("Failed to parse Cpp source")
2304 }
2305
2306 #[test]
2307 fn test_extract_class() {
2308 let source = "class User { }";
2309 let tree = parse_cpp(source);
2310 let mut staging = StagingGraph::new();
2311 let builder = CppGraphBuilder::new();
2312
2313 let result = builder.build_graph(
2314 &tree,
2315 source.as_bytes(),
2316 Path::new("test.cpp"),
2317 &mut staging,
2318 );
2319
2320 assert!(result.is_ok());
2321 assert_has_node_with_kind(&staging, "User", NodeKind::Class);
2322 }
2323
2324 #[test]
2325 fn test_extract_template_class() {
2326 let source = r"
2327 template <typename T>
2328 class Person {
2329 public:
2330 T name;
2331 T age;
2332 };
2333 ";
2334 let tree = parse_cpp(source);
2335 let mut staging = StagingGraph::new();
2336 let builder = CppGraphBuilder::new();
2337
2338 let result = builder.build_graph(
2339 &tree,
2340 source.as_bytes(),
2341 Path::new("test.cpp"),
2342 &mut staging,
2343 );
2344
2345 assert!(result.is_ok());
2346 assert_has_node_with_kind(&staging, "Person", NodeKind::Class);
2347 }
2348
2349 #[test]
2350 fn test_extract_function() {
2351 let source = r#"
2352 #include <cstdio>
2353 void hello() {
2354 std::printf("Hello");
2355 }
2356 "#;
2357 let tree = parse_cpp(source);
2358 let mut staging = StagingGraph::new();
2359 let builder = CppGraphBuilder::new();
2360
2361 let result = builder.build_graph(
2362 &tree,
2363 source.as_bytes(),
2364 Path::new("test.cpp"),
2365 &mut staging,
2366 );
2367
2368 assert!(result.is_ok());
2369 assert_has_node_with_kind(&staging, "hello", NodeKind::Function);
2370 }
2371
2372 #[test]
2373 fn test_extract_virtual_function() {
2374 let source = r"
2375 class Service {
2376 public:
2377 virtual void fetchData() {}
2378 };
2379 ";
2380 let tree = parse_cpp(source);
2381 let mut staging = StagingGraph::new();
2382 let builder = CppGraphBuilder::new();
2383
2384 let result = builder.build_graph(
2385 &tree,
2386 source.as_bytes(),
2387 Path::new("test.cpp"),
2388 &mut staging,
2389 );
2390
2391 assert!(result.is_ok());
2392 assert_has_node(&staging, "fetchData");
2393 }
2394
2395 #[test]
2396 fn test_extract_call_edge() {
2397 let source = r"
2398 void greet() {}
2399
2400 int main() {
2401 greet();
2402 return 0;
2403 }
2404 ";
2405 let tree = parse_cpp(source);
2406 let mut staging = StagingGraph::new();
2407 let builder = CppGraphBuilder::new();
2408
2409 let result = builder.build_graph(
2410 &tree,
2411 source.as_bytes(),
2412 Path::new("test.cpp"),
2413 &mut staging,
2414 );
2415
2416 assert!(result.is_ok());
2417 assert_has_node(&staging, "main");
2418 assert_has_node(&staging, "greet");
2419 let calls = collect_call_edges(&staging);
2420 assert!(!calls.is_empty());
2421 }
2422
2423 #[test]
2424 fn test_extract_member_call_edge() {
2425 let source = r"
2426 class Service {
2427 public:
2428 void helper() {}
2429 };
2430
2431 int main() {
2432 Service svc;
2433 svc.helper();
2434 return 0;
2435 }
2436 ";
2437 let tree = parse_cpp(source);
2438 let mut staging = StagingGraph::new();
2439 let builder = CppGraphBuilder::new();
2440
2441 let result = builder.build_graph(
2442 &tree,
2443 source.as_bytes(),
2444 Path::new("member.cpp"),
2445 &mut staging,
2446 );
2447
2448 assert!(result.is_ok());
2449 assert_has_node(&staging, "main");
2450 assert_has_node(&staging, "helper");
2451 let calls = collect_call_edges(&staging);
2452 assert!(!calls.is_empty());
2453 }
2454
2455 #[test]
2456 fn test_extract_namespace_map_simple() {
2457 let source = r"
2458 namespace demo {
2459 void func() {}
2460 }
2461 ";
2462 let tree = parse_cpp(source);
2463 let namespace_map = extract_namespace_map(tree.root_node(), source.as_bytes());
2464
2465 assert_eq!(namespace_map.len(), 1);
2467
2468 let (_, ns_prefix) = namespace_map.iter().next().unwrap();
2470 assert_eq!(ns_prefix, "demo::");
2471 }
2472
2473 #[test]
2474 fn test_extract_namespace_map_nested() {
2475 let source = r"
2476 namespace outer {
2477 namespace inner {
2478 void func() {}
2479 }
2480 }
2481 ";
2482 let tree = parse_cpp(source);
2483 let namespace_map = extract_namespace_map(tree.root_node(), source.as_bytes());
2484
2485 assert!(namespace_map.len() >= 2);
2487
2488 let ns_values: Vec<&String> = namespace_map.values().collect();
2490 assert!(ns_values.iter().any(|v| v.as_str() == "outer::"));
2491 assert!(ns_values.iter().any(|v| v.as_str() == "outer::inner::"));
2492 }
2493
2494 #[test]
2495 fn test_extract_namespace_map_multiple() {
2496 let source = r"
2497 namespace first {
2498 void func1() {}
2499 }
2500 namespace second {
2501 void func2() {}
2502 }
2503 ";
2504 let tree = parse_cpp(source);
2505 let namespace_map = extract_namespace_map(tree.root_node(), source.as_bytes());
2506
2507 assert_eq!(namespace_map.len(), 2);
2509
2510 let ns_values: Vec<&String> = namespace_map.values().collect();
2511 assert!(ns_values.iter().any(|v| v.as_str() == "first::"));
2512 assert!(ns_values.iter().any(|v| v.as_str() == "second::"));
2513 }
2514
2515 #[test]
2516 fn test_find_namespace_for_offset() {
2517 let source = r"
2518 namespace demo {
2519 void func() {}
2520 }
2521 ";
2522 let tree = parse_cpp(source);
2523 let namespace_map = extract_namespace_map(tree.root_node(), source.as_bytes());
2524
2525 let func_offset = source.find("func").unwrap();
2527 let ns = find_namespace_for_offset(func_offset, &namespace_map);
2528 assert_eq!(ns, "demo::");
2529
2530 let ns = find_namespace_for_offset(0, &namespace_map);
2532 assert_eq!(ns, "");
2533 }
2534
2535 #[test]
2536 fn test_extract_cpp_contexts_free_function() {
2537 let source = r"
2538 void helper() {}
2539 ";
2540 let tree = parse_cpp(source);
2541 let namespace_map = extract_namespace_map(tree.root_node(), source.as_bytes());
2542 let contexts = extract_cpp_contexts(tree.root_node(), source.as_bytes(), &namespace_map);
2543
2544 assert_eq!(contexts.len(), 1);
2545 assert_eq!(contexts[0].qualified_name, "helper");
2546 assert!(!contexts[0].is_static);
2547 assert!(!contexts[0].is_virtual);
2548 }
2549
2550 #[test]
2551 fn test_extract_cpp_contexts_namespace_function() {
2552 let source = r"
2553 namespace demo {
2554 void helper() {}
2555 }
2556 ";
2557 let tree = parse_cpp(source);
2558 let namespace_map = extract_namespace_map(tree.root_node(), source.as_bytes());
2559 let contexts = extract_cpp_contexts(tree.root_node(), source.as_bytes(), &namespace_map);
2560
2561 assert_eq!(contexts.len(), 1);
2562 assert_eq!(contexts[0].qualified_name, "demo::helper");
2563 assert_eq!(contexts[0].namespace_stack, vec!["demo"]);
2564 }
2565
2566 #[test]
2567 fn test_extract_cpp_contexts_class_method() {
2568 let source = r"
2569 class Service {
2570 public:
2571 void process() {}
2572 };
2573 ";
2574 let tree = parse_cpp(source);
2575 let namespace_map = extract_namespace_map(tree.root_node(), source.as_bytes());
2576 let contexts = extract_cpp_contexts(tree.root_node(), source.as_bytes(), &namespace_map);
2577
2578 assert_eq!(contexts.len(), 1);
2579 assert_eq!(contexts[0].qualified_name, "Service::process");
2580 assert_eq!(contexts[0].class_stack, vec!["Service"]);
2581 }
2582
2583 #[test]
2584 fn test_extract_cpp_contexts_namespace_and_class() {
2585 let source = r"
2586 namespace demo {
2587 class Service {
2588 public:
2589 void process() {}
2590 };
2591 }
2592 ";
2593 let tree = parse_cpp(source);
2594 let namespace_map = extract_namespace_map(tree.root_node(), source.as_bytes());
2595 let contexts = extract_cpp_contexts(tree.root_node(), source.as_bytes(), &namespace_map);
2596
2597 assert_eq!(contexts.len(), 1);
2598 assert_eq!(contexts[0].qualified_name, "demo::Service::process");
2599 assert_eq!(contexts[0].namespace_stack, vec!["demo"]);
2600 assert_eq!(contexts[0].class_stack, vec!["Service"]);
2601 }
2602
2603 #[test]
2604 fn test_extract_cpp_contexts_static_method() {
2605 let source = r"
2606 class Repository {
2607 public:
2608 static void save() {}
2609 };
2610 ";
2611 let tree = parse_cpp(source);
2612 let namespace_map = extract_namespace_map(tree.root_node(), source.as_bytes());
2613 let contexts = extract_cpp_contexts(tree.root_node(), source.as_bytes(), &namespace_map);
2614
2615 assert_eq!(contexts.len(), 1);
2616 assert_eq!(contexts[0].qualified_name, "Repository::save");
2617 assert!(contexts[0].is_static);
2618 }
2619
2620 #[test]
2621 fn test_extract_cpp_contexts_virtual_method() {
2622 let source = r"
2623 class Base {
2624 public:
2625 virtual void render() {}
2626 };
2627 ";
2628 let tree = parse_cpp(source);
2629 let namespace_map = extract_namespace_map(tree.root_node(), source.as_bytes());
2630 let contexts = extract_cpp_contexts(tree.root_node(), source.as_bytes(), &namespace_map);
2631
2632 assert_eq!(contexts.len(), 1);
2633 assert_eq!(contexts[0].qualified_name, "Base::render");
2634 assert!(contexts[0].is_virtual);
2635 }
2636
2637 #[test]
2638 fn test_extract_cpp_contexts_inline_function() {
2639 let source = r"
2640 inline void helper() {}
2641 ";
2642 let tree = parse_cpp(source);
2643 let namespace_map = extract_namespace_map(tree.root_node(), source.as_bytes());
2644 let contexts = extract_cpp_contexts(tree.root_node(), source.as_bytes(), &namespace_map);
2645
2646 assert_eq!(contexts.len(), 1);
2647 assert_eq!(contexts[0].qualified_name, "helper");
2648 assert!(contexts[0].is_inline);
2649 }
2650
2651 #[test]
2652 fn test_extract_cpp_contexts_out_of_line_definition() {
2653 let source = r"
2654 namespace demo {
2655 class Service {
2656 public:
2657 int process(int v);
2658 };
2659
2660 inline int Service::process(int v) {
2661 return v;
2662 }
2663 }
2664 ";
2665 let tree = parse_cpp(source);
2666 let namespace_map = extract_namespace_map(tree.root_node(), source.as_bytes());
2667 let contexts = extract_cpp_contexts(tree.root_node(), source.as_bytes(), &namespace_map);
2668
2669 assert_eq!(contexts.len(), 1);
2671 assert_eq!(contexts[0].qualified_name, "demo::Service::process");
2672 assert!(contexts[0].is_inline);
2673 }
2674
2675 #[test]
2676 fn test_extract_field_types_simple() {
2677 let source = r"
2678 class Service {
2679 public:
2680 Repository repo;
2681 };
2682 ";
2683 let tree = parse_cpp(source);
2684 let namespace_map = extract_namespace_map(tree.root_node(), source.as_bytes());
2685 let (field_types, _type_map) =
2686 extract_field_and_type_info(tree.root_node(), source.as_bytes(), &namespace_map);
2687
2688 assert_eq!(field_types.len(), 1);
2690 assert_eq!(
2691 field_types.get(&("Service".to_string(), "repo".to_string())),
2692 Some(&"Repository".to_string())
2693 );
2694 }
2695
2696 #[test]
2697 fn test_extract_field_types_namespace() {
2698 let source = r"
2699 namespace demo {
2700 class Service {
2701 public:
2702 Repository repo;
2703 };
2704 }
2705 ";
2706 let tree = parse_cpp(source);
2707 let namespace_map = extract_namespace_map(tree.root_node(), source.as_bytes());
2708 let (field_types, _type_map) =
2709 extract_field_and_type_info(tree.root_node(), source.as_bytes(), &namespace_map);
2710
2711 assert_eq!(field_types.len(), 1);
2713 assert_eq!(
2714 field_types.get(&("demo::Service".to_string(), "repo".to_string())),
2715 Some(&"Repository".to_string())
2716 );
2717 }
2718
2719 #[test]
2720 fn test_extract_field_types_no_collision() {
2721 let source = r"
2722 class ServiceA {
2723 public:
2724 Repository repo;
2725 };
2726
2727 class ServiceB {
2728 public:
2729 Repository repo;
2730 };
2731 ";
2732 let tree = parse_cpp(source);
2733 let namespace_map = extract_namespace_map(tree.root_node(), source.as_bytes());
2734 let (field_types, _type_map) =
2735 extract_field_and_type_info(tree.root_node(), source.as_bytes(), &namespace_map);
2736
2737 assert_eq!(field_types.len(), 2);
2739 assert_eq!(
2740 field_types.get(&("ServiceA".to_string(), "repo".to_string())),
2741 Some(&"Repository".to_string())
2742 );
2743 assert_eq!(
2744 field_types.get(&("ServiceB".to_string(), "repo".to_string())),
2745 Some(&"Repository".to_string())
2746 );
2747 }
2748
2749 #[test]
2750 fn test_extract_using_declaration() {
2751 let source = r"
2752 using std::vector;
2753
2754 class Service {
2755 public:
2756 vector data;
2757 };
2758 ";
2759 let tree = parse_cpp(source);
2760 let namespace_map = extract_namespace_map(tree.root_node(), source.as_bytes());
2761 let (field_types, type_map) =
2762 extract_field_and_type_info(tree.root_node(), source.as_bytes(), &namespace_map);
2763
2764 assert_eq!(field_types.len(), 1);
2766 assert_eq!(
2767 field_types.get(&("Service".to_string(), "data".to_string())),
2768 Some(&"std::vector".to_string()),
2769 "Field type should resolve 'vector' to 'std::vector' via using declaration"
2770 );
2771
2772 assert_eq!(
2774 type_map.get(&(String::new(), "vector".to_string())),
2775 Some(&"std::vector".to_string()),
2776 "Using declaration should map 'vector' to 'std::vector' in type_map"
2777 );
2778 }
2779
2780 #[test]
2781 fn test_extract_field_types_pointer() {
2782 let source = r"
2783 class Service {
2784 public:
2785 Repository* repo;
2786 };
2787 ";
2788 let tree = parse_cpp(source);
2789 let namespace_map = extract_namespace_map(tree.root_node(), source.as_bytes());
2790 let (field_types, _type_map) =
2791 extract_field_and_type_info(tree.root_node(), source.as_bytes(), &namespace_map);
2792
2793 assert_eq!(field_types.len(), 1);
2795 assert_eq!(
2796 field_types.get(&("Service".to_string(), "repo".to_string())),
2797 Some(&"Repository".to_string())
2798 );
2799 }
2800
2801 #[test]
2802 fn test_extract_field_types_multiple_declarators() {
2803 let source = r"
2804 class Service {
2805 public:
2806 Repository repo_a, repo_b, repo_c;
2807 };
2808 ";
2809 let tree = parse_cpp(source);
2810 let namespace_map = extract_namespace_map(tree.root_node(), source.as_bytes());
2811 let (field_types, _type_map) =
2812 extract_field_and_type_info(tree.root_node(), source.as_bytes(), &namespace_map);
2813
2814 assert_eq!(field_types.len(), 3);
2816 assert_eq!(
2817 field_types.get(&("Service".to_string(), "repo_a".to_string())),
2818 Some(&"Repository".to_string())
2819 );
2820 assert_eq!(
2821 field_types.get(&("Service".to_string(), "repo_b".to_string())),
2822 Some(&"Repository".to_string())
2823 );
2824 assert_eq!(
2825 field_types.get(&("Service".to_string(), "repo_c".to_string())),
2826 Some(&"Repository".to_string())
2827 );
2828 }
2829
2830 #[test]
2831 fn test_extract_field_types_nested_struct_with_parent_field() {
2832 let source = r"
2835 namespace demo {
2836 struct Outer {
2837 int outer_field;
2838 struct Inner {
2839 int inner_field;
2840 };
2841 Inner nested_instance;
2842 };
2843 }
2844 ";
2845 let tree = parse_cpp(source);
2846 let namespace_map = extract_namespace_map(tree.root_node(), source.as_bytes());
2847 let (field_types, _type_map) =
2848 extract_field_and_type_info(tree.root_node(), source.as_bytes(), &namespace_map);
2849
2850 assert!(
2853 field_types.len() >= 2,
2854 "Expected at least outer_field and nested_instance"
2855 );
2856
2857 assert_eq!(
2859 field_types.get(&("demo::Outer".to_string(), "outer_field".to_string())),
2860 Some(&"int".to_string())
2861 );
2862
2863 assert_eq!(
2865 field_types.get(&("demo::Outer".to_string(), "nested_instance".to_string())),
2866 Some(&"Inner".to_string())
2867 );
2868
2869 if field_types.contains_key(&("demo::Outer::Inner".to_string(), "inner_field".to_string()))
2871 {
2872 assert_eq!(
2874 field_types.get(&("demo::Outer::Inner".to_string(), "inner_field".to_string())),
2875 Some(&"int".to_string()),
2876 "Inner class fields must use parent-qualified FQN 'demo::Outer::Inner'"
2877 );
2878 }
2879 }
2880}