1use std::{
26 collections::{HashMap, HashSet},
27 path::Path,
28};
29
30use sqry_core::graph::unified::edge::kind::TypeOfContext;
31use sqry_core::graph::unified::{GraphBuildHelper, NodeId, StagingGraph};
32use sqry_core::graph::{GraphBuilder, GraphBuilderError, GraphResult, Language, Span};
33use tree_sitter::{Node, Tree};
34
35use crate::relations::type_extractor::extract_type_names_from_zig_type;
36
37const FILE_MODULE_NAME: &str = "<file_module>";
39
40#[derive(Debug, Clone, Copy)]
42pub struct ZigGraphBuilder {
43 max_scope_depth: usize,
44}
45
46impl Default for ZigGraphBuilder {
47 fn default() -> Self {
48 Self {
49 max_scope_depth: 4, }
51 }
52}
53
54impl ZigGraphBuilder {
55 #[must_use]
56 pub fn new(max_scope_depth: usize) -> Self {
57 Self { max_scope_depth }
58 }
59}
60
61impl GraphBuilder for ZigGraphBuilder {
62 fn build_graph(
63 &self,
64 tree: &Tree,
65 content: &[u8],
66 file: &Path,
67 staging: &mut StagingGraph,
68 ) -> GraphResult<()> {
69 let mut helper = GraphBuildHelper::new(staging, file, Language::Zig);
70
71 let ast_graph = ASTGraph::from_tree(tree, content, self.max_scope_depth).map_err(|e| {
73 GraphBuilderError::ParseError {
74 span: Span::default(),
75 reason: e,
76 }
77 })?;
78
79 for context in ast_graph.contexts() {
81 let qualified = context.qualified_name();
82 let span = Span::from_bytes(context.span.0, context.span.1);
83 let visibility = if context.is_pub {
84 Some("public")
85 } else {
86 Some("private")
87 };
88 helper.add_function_with_visibility(&qualified, Some(span), false, false, visibility);
89 }
90
91 for decl in ast_graph.decl_nodes() {
93 helper.add_type(&decl.name, Some(Span::from_bytes(decl.span.0, decl.span.1)));
94 }
95
96 let module_id = helper.add_module(FILE_MODULE_NAME, None);
98
99 for context in ast_graph.contexts() {
101 if context.is_pub
103 && !context.qualified_name.contains('.')
104 && let Some(exported_id) = helper.get_node(&context.qualified_name)
105 {
106 helper.add_export_edge(module_id, exported_id);
107 }
108 }
109
110 for decl in ast_graph.decl_nodes() {
112 if decl.is_pub
113 && let Some(exported_id) = helper.get_node(&decl.name)
114 {
115 helper.add_export_edge(module_id, exported_id);
116 }
117 }
118
119 let mut stack = vec![tree.root_node()];
121 let mut visited = HashSet::new();
122
123 while let Some(node) = stack.pop() {
124 let node_id = node.id();
125
126 if !visited.insert(node_id) {
128 continue;
129 }
130
131 match node.kind() {
133 "comment" | "line_comment" | "doc_comment" | "string" | "char" | "integer"
134 | "float" => {
135 continue;
136 }
137 _ => {}
138 }
139
140 if node.kind() == "builtin_function"
142 && is_import_builtin(node, content)
143 && let Some(module_name) = extract_import_module_name(node, content)
144 {
145 let importer_id = if let Some(ctx) = ast_graph.get_callable_context(node.id()) {
147 helper.get_node(&ctx.qualified_name()).unwrap_or_else(|| {
148 let span = Span::from_bytes(ctx.span.0, ctx.span.1);
149 helper.add_function(&ctx.qualified_name(), Some(span), false, false)
150 })
151 } else {
152 module_id
153 };
154
155 let span = Span::from_bytes(node.start_byte(), node.end_byte());
157 let import_node_id = helper.add_import(&module_name, Some(span));
158 helper.add_import_edge(importer_id, import_node_id);
159 }
160 else if (node.kind() == "call_expression"
162 || (node.kind() == "builtin_function" && !is_import_builtin(node, content)))
163 && let Some((caller_id, callee_id, argument_count)) =
164 build_call_edge_ids(&ast_graph, node, content, &mut helper)
165 {
166 let call_span = Span::from_bytes(node.start_byte(), node.end_byte());
167 helper.add_call_edge_full_with_span(
168 caller_id,
169 callee_id,
170 argument_count,
171 false,
172 vec![call_span],
173 );
174 }
175
176 let mut cursor = node.walk();
178 for child in node.children(&mut cursor) {
179 stack.push(child);
180 }
181 }
182
183 process_typeof_edges(tree.root_node(), content, &mut helper)?;
185
186 Ok(())
187 }
188
189 fn language(&self) -> Language {
190 Language::Zig
191 }
192}
193
194fn build_call_edge_ids(
200 ast_graph: &ASTGraph,
201 call_node: Node<'_>,
202 content: &[u8],
203 helper: &mut GraphBuildHelper,
204) -> Option<(NodeId, NodeId, u8)> {
205 let module_context;
207 let call_context = if let Some(ctx) = ast_graph.get_callable_context(call_node.id()) {
208 ctx
209 } else {
210 module_context = CallContext {
212 qualified_name: "<module>".to_string(),
213 span: (0, content.len()),
214 is_pub: false,
215 };
216 &module_context
217 };
218
219 let (callee_name, arg_count) = extract_call_info(call_node, content);
221
222 if callee_name.is_empty() {
224 return None;
225 }
226
227 let source_id = if helper.has_node(&call_context.qualified_name()) {
229 helper.get_node(&call_context.qualified_name()).unwrap()
230 } else {
231 let span = Span::from_bytes(call_context.span.0, call_context.span.1);
232 helper.add_function(&call_context.qualified_name(), Some(span), false, false)
233 };
234
235 let target_id = helper.add_function(&callee_name, None, false, false);
237
238 let argument_count = u8::try_from(arg_count).unwrap_or(u8::MAX);
239 Some((source_id, target_id, argument_count))
240}
241
242fn extract_call_info(call_node: Node<'_>, content: &[u8]) -> (String, usize) {
262 let mut function_name = String::new();
263 let mut arg_count = 0;
264 let mut in_arguments = false;
265 let mut found_function_name = false;
266
267 let mut cursor = call_node.walk();
268 for child in call_node.children(&mut cursor) {
269 match child.kind() {
270 "builtin_identifier" => {
271 if !found_function_name {
273 function_name = child.utf8_text(content).unwrap_or("").to_string();
274 found_function_name = true;
275 }
276 }
277 "identifier" | "field_expression" | "field_access" => {
278 if !found_function_name {
280 function_name = child.utf8_text(content).unwrap_or("").to_string();
281 found_function_name = true;
282 } else if in_arguments {
283 arg_count += 1;
285 }
286 }
287 "arguments" => {
288 arg_count = count_arguments_in_node(child);
290 }
291 "(" => {
292 in_arguments = true;
294 }
295 ")" => {
296 in_arguments = false;
298 }
299 "," => {
300 }
302 _ => {
303 if in_arguments {
305 arg_count += 1;
306 }
307 }
308 }
309 }
310
311 (function_name, arg_count)
312}
313
314fn count_arguments_in_node(args_node: Node<'_>) -> usize {
316 let mut count = 0;
317 let mut cursor = args_node.walk();
318
319 for child in args_node.children(&mut cursor) {
320 match child.kind() {
321 "(" | ")" | "," => {
322 }
324 _ => {
325 count += 1;
327 }
328 }
329 }
330
331 count
332}
333
334fn is_import_builtin(node: Node<'_>, content: &[u8]) -> bool {
337 if node.kind() != "builtin_function" {
338 return false;
339 }
340
341 let mut cursor = node.walk();
342 for child in node.children(&mut cursor) {
343 if child.kind() == "builtin_identifier"
344 && let Ok(text) = child.utf8_text(content)
345 && text == "@import"
346 {
347 return true;
348 }
349 }
350
351 false
352}
353
354fn extract_import_module_name(node: Node<'_>, content: &[u8]) -> Option<String> {
363 if node.kind() != "builtin_function" {
364 return None;
365 }
366
367 let mut cursor = node.walk();
368 for child in node.children(&mut cursor) {
369 if child.kind() == "arguments" {
370 let mut args_cursor = child.walk();
372 for arg_child in child.children(&mut args_cursor) {
373 if arg_child.kind() == "string"
374 && let Ok(text) = arg_child.utf8_text(content)
375 {
376 let trimmed = text.trim().trim_matches('"');
378 if !trimmed.is_empty() {
379 return Some(trimmed.to_string());
380 }
381 }
382 }
383 }
384 }
385
386 None
387}
388
389#[derive(Debug)]
394struct ASTGraph {
395 contexts: Vec<CallContext>,
396 node_to_context: HashMap<usize, usize>,
397 decl_nodes: Vec<DeclNode>,
398}
399
400#[derive(Debug, Clone)]
401struct DeclNode {
402 name: String,
403 span: (usize, usize),
404 is_pub: bool,
405}
406
407impl ASTGraph {
408 fn from_tree(tree: &Tree, content: &[u8], _max_depth: usize) -> Result<Self, String> {
409 let mut contexts = Vec::new();
410 let mut node_to_context = HashMap::new();
411 let mut decl_nodes = Vec::new();
412
413 let root = tree.root_node();
415 extract_functions_recursive(root, content, &mut contexts, &mut node_to_context, None)?;
416 extract_declarations_recursive(root, content, &mut decl_nodes, None)?;
417
418 Ok(Self {
419 contexts,
420 node_to_context,
421 decl_nodes,
422 })
423 }
424
425 fn contexts(&self) -> &[CallContext] {
426 &self.contexts
427 }
428
429 fn decl_nodes(&self) -> &[DeclNode] {
430 &self.decl_nodes
431 }
432
433 fn get_callable_context(&self, node_id: usize) -> Option<&CallContext> {
434 self.node_to_context
435 .get(&node_id)
436 .and_then(|idx| self.contexts.get(*idx))
437 }
438}
439
440fn extract_functions_recursive(
442 node: Node<'_>,
443 content: &[u8],
444 contexts: &mut Vec<CallContext>,
445 node_to_context: &mut HashMap<usize, usize>,
446 parent_name: Option<&str>,
447) -> Result<(), String> {
448 if node.kind() == "function_declaration"
450 && let Some(name) = extract_function_name(node, content)
451 {
452 let is_pub = has_pub_modifier(node);
453
454 let qualified_name = if let Some(parent) = parent_name {
456 format!("{parent}.{name}")
457 } else {
458 name.clone()
459 };
460
461 let context_idx = contexts.len();
462 contexts.push(CallContext {
463 qualified_name: qualified_name.clone(),
464 span: (node.start_byte(), node.end_byte()),
465 is_pub,
466 });
467
468 map_descendants_to_context(&node, context_idx, node_to_context);
470
471 let mut cursor = node.walk();
473 for child in node.children(&mut cursor) {
474 extract_functions_recursive(
475 child,
476 content,
477 contexts,
478 node_to_context,
479 Some(&qualified_name),
480 )?;
481 }
482
483 return Ok(());
485 }
486
487 if node.kind() == "struct_declaration"
490 || node.kind() == "union_declaration"
491 || node.kind() == "enum_declaration"
492 {
493 let container_name = node.parent().and_then(|parent| {
495 if parent.kind() == "variable_declaration" {
496 extract_container_name_from_var_decl(parent, content)
497 } else {
498 None
499 }
500 });
501
502 let qualified_container = if let Some(name) = container_name {
504 if let Some(parent) = parent_name {
505 format!("{parent}.{name}")
506 } else {
507 name
508 }
509 } else {
510 parent_name.map(String::from).unwrap_or_default()
512 };
513
514 let mut cursor = node.walk();
516 for child in node.children(&mut cursor) {
517 let child_parent = if qualified_container.is_empty() {
518 parent_name
519 } else {
520 Some(qualified_container.as_str())
521 };
522 extract_functions_recursive(child, content, contexts, node_to_context, child_parent)?;
523 }
524
525 return Ok(());
527 }
528
529 let mut cursor = node.walk();
531 for child in node.children(&mut cursor) {
532 extract_functions_recursive(child, content, contexts, node_to_context, parent_name)?;
533 }
534
535 Ok(())
536}
537
538fn extract_declarations_recursive(
540 node: Node<'_>,
541 content: &[u8],
542 decl_nodes: &mut Vec<DeclNode>,
543 parent_name: Option<&str>,
544) -> Result<(), String> {
545 if parent_name.is_none()
548 && node.kind() == "variable_declaration"
549 && let Some((name, is_pub)) = extract_var_decl_info(node, content)
550 && is_pub
551 {
552 decl_nodes.push(DeclNode {
553 name,
554 span: (node.start_byte(), node.end_byte()),
555 is_pub: true,
556 });
557 }
558
559 let mut cursor = node.walk();
561 for child in node.children(&mut cursor) {
562 extract_declarations_recursive(child, content, decl_nodes, parent_name)?;
563 }
564
565 Ok(())
566}
567
568fn extract_var_decl_info(node: Node<'_>, content: &[u8]) -> Option<(String, bool)> {
570 let is_pub = has_pub_modifier(node);
571
572 let mut cursor = node.walk();
573 for child in node.children(&mut cursor) {
574 if child.kind() == "identifier"
575 && let Ok(name) = child.utf8_text(content)
576 {
577 return Some((name.to_string(), is_pub));
578 }
579 }
580 None
581}
582
583fn extract_container_name_from_var_decl(node: Node<'_>, content: &[u8]) -> Option<String> {
586 let mut cursor = node.walk();
587 for child in node.children(&mut cursor) {
588 if child.kind() == "identifier"
589 && let Ok(name) = child.utf8_text(content)
590 {
591 return Some(name.to_string());
592 }
593 }
594 None
595}
596
597fn extract_function_name(node: Node<'_>, content: &[u8]) -> Option<String> {
599 let mut cursor = node.walk();
600 for child in node.children(&mut cursor) {
601 if child.kind() == "identifier"
602 && let Ok(name) = child.utf8_text(content)
603 {
604 return Some(name.to_string());
605 }
606 }
607 None
608}
609
610fn has_pub_modifier(node: Node<'_>) -> bool {
612 let mut cursor = node.walk();
613 for child in node.children(&mut cursor) {
614 if child.kind() == "pub" {
615 return true;
616 }
617 }
618 false
619}
620
621fn map_descendants_to_context(node: &Node, context_idx: usize, map: &mut HashMap<usize, usize>) {
623 map.insert(node.id(), context_idx);
624
625 let mut cursor = node.walk();
626 for child in node.children(&mut cursor) {
627 map_descendants_to_context(&child, context_idx, map);
628 }
629}
630
631#[derive(Debug, Clone)]
632struct CallContext {
633 qualified_name: String,
634 span: (usize, usize),
635 #[allow(dead_code)] is_pub: bool,
637}
638
639impl CallContext {
640 fn qualified_name(&self) -> String {
641 self.qualified_name.clone()
642 }
643}
644
645fn process_typeof_edges(
657 root: Node,
658 content: &[u8],
659 helper: &mut GraphBuildHelper,
660) -> GraphResult<()> {
661 let mut stack = vec![root];
662 let mut visited = HashSet::new();
663
664 while let Some(node) = stack.pop() {
665 let node_id = node.id();
666
667 if !visited.insert(node_id) {
668 continue;
669 }
670
671 match node.kind() {
672 "variable_declaration" => {
673 handle_variable_declaration(node, content, helper)?;
674 }
675 "function_declaration" => {
676 handle_function_typeof_edges(node, content, helper)?;
677 }
678 "struct_declaration" | "union_declaration" | "enum_declaration" => {
679 handle_container_fields(node, content, helper)?;
680 }
681 _ => {}
682 }
683
684 let mut cursor = node.walk();
686 for child in node.children(&mut cursor) {
687 stack.push(child);
688 }
689 }
690
691 Ok(())
692}
693
694#[allow(clippy::unnecessary_wraps)]
700fn handle_variable_declaration(
701 node: Node,
702 content: &[u8],
703 helper: &mut GraphBuildHelper,
704) -> GraphResult<()> {
705 let var_name = extract_variable_name(node, content);
707
708 if let Some(name) = var_name {
709 let type_node =
712 find_type_annotation_in_var_decl(node).or_else(|| find_type_alias_expression(node));
713
714 if let Some(type_node) = type_node {
715 let var_id = if let Some(id) = helper.get_node(&name) {
717 id
718 } else {
719 let span = Span::from_bytes(node.start_byte(), node.end_byte());
721 helper.add_variable(&name, Some(span))
722 };
723
724 if let Ok(type_str) = type_node.utf8_text(content) {
726 let type_id = helper.add_type(type_str.trim(), None);
727 helper.add_typeof_edge_with_context(
728 var_id,
729 type_id,
730 Some(TypeOfContext::Variable),
731 None,
732 Some(&name),
733 );
734 }
735
736 let type_names = extract_type_names_from_zig_type(type_node, content);
738 for type_name in type_names {
739 let type_id = helper.add_type(&type_name, None);
740 helper.add_reference_edge(var_id, type_id);
741 }
742 }
743 }
744
745 Ok(())
746}
747
748fn handle_function_typeof_edges(
750 node: Node,
751 content: &[u8],
752 helper: &mut GraphBuildHelper,
753) -> GraphResult<()> {
754 let fn_name = extract_function_name(node, content);
756
757 if let Some(name) = fn_name {
758 if let Some(fn_id) = helper.get_node(&name) {
760 if let Some(params_node) = find_parameters_node(node) {
762 let mut param_index = 0;
763 let mut cursor = params_node.walk();
764
765 for child in params_node.children(&mut cursor) {
766 if child.kind() == "parameter" {
767 handle_function_parameter(child, content, helper, fn_id, param_index)?;
768 param_index += 1;
769 }
770 }
771 }
772
773 if let Some(return_type_node) = find_function_return_type(node) {
775 if let Ok(type_str) = return_type_node.utf8_text(content) {
777 let type_id = helper.add_type(type_str.trim(), None);
778 helper.add_typeof_edge_with_context(
779 fn_id,
780 type_id,
781 Some(TypeOfContext::Return),
782 None,
783 None,
784 );
785 }
786
787 let type_names = extract_type_names_from_zig_type(return_type_node, content);
789 for type_name in type_names {
790 let type_id = helper.add_type(&type_name, None);
791 helper.add_reference_edge(fn_id, type_id);
792 }
793 }
794 }
795 }
796
797 Ok(())
798}
799
800#[allow(clippy::cast_possible_truncation)]
802#[allow(clippy::unnecessary_wraps)]
803fn handle_function_parameter(
804 param_node: Node,
805 content: &[u8],
806 helper: &mut GraphBuildHelper,
807 fn_id: NodeId,
808 param_index: usize,
809) -> GraphResult<()> {
810 let param_name = extract_parameter_name(param_node, content);
812 let type_node = find_parameter_type_node(param_node);
813
814 if let Some(type_node) = type_node {
815 if let Ok(type_str) = type_node.utf8_text(content) {
817 let type_id = helper.add_type(type_str.trim(), None);
818
819 helper.add_typeof_edge_with_context(
820 fn_id,
821 type_id,
822 Some(TypeOfContext::Parameter),
823 Some(param_index as u16),
824 param_name.as_deref(),
825 );
826 }
827
828 let type_names = extract_type_names_from_zig_type(type_node, content);
830 for type_name in type_names {
831 let type_id = helper.add_type(&type_name, None);
832 helper.add_reference_edge(fn_id, type_id);
833 }
834 }
835
836 Ok(())
837}
838
839fn handle_container_fields(
841 container_node: Node,
842 content: &[u8],
843 helper: &mut GraphBuildHelper,
844) -> GraphResult<()> {
845 let container_name = container_node.parent().and_then(|parent| {
847 if parent.kind() == "variable_declaration" {
848 extract_container_name_from_var_decl(parent, content)
849 } else {
850 None
851 }
852 });
853
854 if let Some(container_name) = container_name {
855 let mut cursor = container_node.walk();
857 for child in container_node.children(&mut cursor) {
858 if child.kind() == "container_field" {
859 handle_container_field(child, content, helper, &container_name)?;
860 }
861 }
862 }
863
864 Ok(())
865}
866
867#[allow(clippy::unnecessary_wraps)]
869fn handle_container_field(
870 field_node: Node,
871 content: &[u8],
872 helper: &mut GraphBuildHelper,
873 container_name: &str,
874) -> GraphResult<()> {
875 let field_name = extract_field_name(field_node, content);
877 let type_node = find_field_type_node(field_node);
878
879 if let (Some(name), Some(type_node)) = (field_name, type_node) {
880 let qualified_name = format!("{container_name}.{name}");
881
882 let field_id = if let Some(id) = helper.get_node(&qualified_name) {
884 id
885 } else {
886 let span = Span::from_bytes(field_node.start_byte(), field_node.end_byte());
887 helper.add_variable(&qualified_name, Some(span))
888 };
889
890 if let Ok(type_str) = type_node.utf8_text(content) {
892 let type_id = helper.add_type(type_str.trim(), None);
893 helper.add_typeof_edge_with_context(
894 field_id,
895 type_id,
896 Some(TypeOfContext::Field),
897 None,
898 Some(&name),
899 );
900 }
901
902 let type_names = extract_type_names_from_zig_type(type_node, content);
904 for type_name in type_names {
905 let type_id = helper.add_type(&type_name, None);
906 helper.add_reference_edge(field_id, type_id);
907 }
908 }
909
910 Ok(())
911}
912
913fn extract_variable_name(node: Node, content: &[u8]) -> Option<String> {
919 let mut cursor = node.walk();
920 for child in node.children(&mut cursor) {
921 if child.kind() == "identifier" {
922 return child.utf8_text(content).ok().map(String::from);
923 }
924 }
925 None
926}
927
928fn find_type_annotation_in_var_decl(node: Node) -> Option<Node> {
932 let mut found_colon = false;
933 let mut cursor = node.walk();
934
935 for child in node.children(&mut cursor) {
936 if child.kind() == ":" {
937 found_colon = true;
938 continue;
939 }
940
941 if found_colon && is_type_like_node(child.kind()) {
943 return Some(child);
944 }
945 }
946
947 None
948}
949
950fn find_type_alias_expression(node: Node) -> Option<Node> {
956 let mut found_equals = false;
957 let mut cursor = node.walk();
958
959 for child in node.children(&mut cursor) {
960 if child.kind() == "=" {
961 found_equals = true;
962 continue;
963 }
964
965 if found_equals && is_type_like_node(child.kind()) {
969 return Some(child);
970 }
971 }
972
973 None
974}
975
976fn find_parameters_node(node: Node) -> Option<Node> {
978 let mut cursor = node.walk();
979 node.children(&mut cursor)
980 .find(|child| child.kind() == "parameters")
981}
982
983fn find_function_return_type(node: Node) -> Option<Node> {
987 let mut found_params = false;
988 let mut cursor = node.walk();
989
990 for child in node.children(&mut cursor) {
991 if child.kind() == "parameters" || child.kind() == ")" {
993 found_params = true;
994 continue;
995 }
996
997 if found_params && is_type_like_node(child.kind()) && child.kind() != "block" {
1000 return Some(child);
1001 }
1002 }
1003
1004 None
1005}
1006
1007fn extract_parameter_name(node: Node, content: &[u8]) -> Option<String> {
1009 let mut cursor = node.walk();
1010 for child in node.children(&mut cursor) {
1011 if child.kind() == "identifier" {
1012 return child.utf8_text(content).ok().map(String::from);
1013 }
1014 }
1015 None
1016}
1017
1018fn find_parameter_type_node(node: Node) -> Option<Node> {
1022 let mut found_colon = false;
1023 let mut cursor = node.walk();
1024
1025 for child in node.children(&mut cursor) {
1026 if child.kind() == ":" {
1027 found_colon = true;
1028 continue;
1029 }
1030
1031 if found_colon && is_type_like_node(child.kind()) {
1033 return Some(child);
1034 }
1035 }
1036
1037 None
1038}
1039
1040fn extract_field_name(node: Node, content: &[u8]) -> Option<String> {
1042 let mut cursor = node.walk();
1043 for child in node.children(&mut cursor) {
1044 if child.kind() == "identifier" {
1045 return child.utf8_text(content).ok().map(String::from);
1046 }
1047 }
1048 None
1049}
1050
1051fn find_field_type_node(node: Node) -> Option<Node> {
1055 let mut found_colon = false;
1056 let mut cursor = node.walk();
1057
1058 for child in node.children(&mut cursor) {
1059 if child.kind() == ":" {
1060 found_colon = true;
1061 continue;
1062 }
1063
1064 if found_colon && is_type_like_node(child.kind()) {
1066 return Some(child);
1067 }
1068 }
1069
1070 None
1071}
1072
1073fn is_type_like_node(kind: &str) -> bool {
1075 matches!(
1076 kind,
1077 "builtin_type"
1078 | "identifier"
1079 | "pointer_type"
1080 | "slice_type"
1081 | "array_type"
1082 | "optional_type"
1083 | "nullable_type"
1084 | "error_union_type"
1085 | "function_type"
1086 | "FnProto"
1087 | "fn_proto"
1088 | "struct_declaration"
1089 | "enum_declaration"
1090 | "union_declaration"
1091 | "call_expression" | "field_expression" | "field_access" )
1095}
1096
1097#[cfg(test)]
1098mod tests {
1099 use super::*;
1100 use sqry_core::graph::unified::build::StagingOp;
1101 use sqry_core::graph::unified::build::test_helpers::*;
1102 use sqry_core::graph::unified::edge::EdgeKind;
1103 use sqry_core::graph::unified::node::NodeKind;
1104 use std::path::Path;
1105
1106 fn parse_zig(source: &str) -> (Tree, Vec<u8>) {
1107 let mut parser = tree_sitter::Parser::new();
1108 parser
1109 .set_language(&tree_sitter_zig::LANGUAGE.into())
1110 .expect("Failed to load Zig grammar");
1111
1112 let content = source.as_bytes().to_vec();
1113 let tree = parser.parse(&content, None).expect("Failed to parse");
1114 (tree, content)
1115 }
1116
1117 fn has_display_name(
1118 staging: &StagingGraph,
1119 canonical_name: &str,
1120 expected_display_name: &str,
1121 ) -> bool {
1122 staging.operations().iter().any(|op| {
1123 if let StagingOp::AddNode { entry, .. } = op {
1124 staging.resolve_node_canonical_name(entry) == Some(canonical_name)
1125 && staging
1126 .resolve_node_display_name(Language::Zig, entry)
1127 .as_deref()
1128 == Some(expected_display_name)
1129 } else {
1130 false
1131 }
1132 })
1133 }
1134
1135 fn has_display_edge(
1136 staging: &StagingGraph,
1137 kind_matches: impl Fn(&EdgeKind) -> bool,
1138 expected_source: &str,
1139 expected_target: &str,
1140 ) -> bool {
1141 staging.operations().iter().any(|op| {
1142 if let StagingOp::AddEdge {
1143 source,
1144 target,
1145 kind,
1146 ..
1147 } = op
1148 {
1149 if !kind_matches(kind) {
1150 return false;
1151 }
1152
1153 let source_display = staging.operations().iter().find_map(|candidate| {
1154 if let StagingOp::AddNode {
1155 entry,
1156 expected_id: Some(node_id),
1157 } = candidate
1158 && *node_id == *source
1159 {
1160 staging.resolve_node_display_name(Language::Zig, entry)
1161 } else {
1162 None
1163 }
1164 });
1165 let target_display = staging.operations().iter().find_map(|candidate| {
1166 if let StagingOp::AddNode {
1167 entry,
1168 expected_id: Some(node_id),
1169 } = candidate
1170 && *node_id == *target
1171 {
1172 staging.resolve_node_display_name(Language::Zig, entry)
1173 } else {
1174 None
1175 }
1176 });
1177
1178 source_display.as_deref() == Some(expected_source)
1179 && target_display.as_deref() == Some(expected_target)
1180 } else {
1181 false
1182 }
1183 })
1184 }
1185
1186 fn assert_has_display_call_edge(staging: &StagingGraph, source: &str, target: &str) {
1187 assert!(
1188 has_display_edge(
1189 staging,
1190 |kind| matches!(kind, EdgeKind::Calls { .. }),
1191 source,
1192 target,
1193 ),
1194 "Expected Zig native display call edge {source} -> {target}"
1195 );
1196 }
1197
1198 fn assert_has_display_import_edge(staging: &StagingGraph, source: &str, target: &str) {
1199 assert!(
1200 has_display_edge(
1201 staging,
1202 |kind| matches!(
1203 kind,
1204 EdgeKind::Imports {
1205 alias: _,
1206 is_wildcard: _,
1207 }
1208 ),
1209 source,
1210 target,
1211 ),
1212 "Expected Zig native display import edge {source} -> {target}"
1213 );
1214 }
1215
1216 #[test]
1217 fn test_extract_top_level_function() {
1218 let source = r"
1219pub fn add(a: i32, b: i32) i32 {
1220 return a + b;
1221}
1222 ";
1223
1224 let (tree, content) = parse_zig(source);
1225 let mut staging = StagingGraph::new();
1226 let builder = ZigGraphBuilder::default();
1227
1228 builder
1229 .build_graph(&tree, &content, Path::new("test.zig"), &mut staging)
1230 .unwrap();
1231
1232 assert_has_node_with_kind(&staging, "add", NodeKind::Function);
1234
1235 assert_has_export_edge(&staging, FILE_MODULE_NAME, "add");
1237 }
1238
1239 #[test]
1240 fn test_simple_function_call() {
1241 let source = r"
1242fn helper() void {
1243 return;
1244}
1245
1246fn main() void {
1247 helper();
1248}
1249 ";
1250
1251 let (tree, content) = parse_zig(source);
1252 let mut staging = StagingGraph::new();
1253 let builder = ZigGraphBuilder::default();
1254
1255 builder
1256 .build_graph(&tree, &content, Path::new("test.zig"), &mut staging)
1257 .unwrap();
1258
1259 assert_has_node_with_kind(&staging, "helper", NodeKind::Function);
1261 assert_has_node_with_kind(&staging, "main", NodeKind::Function);
1262
1263 assert_has_call_edge(&staging, "main", "helper");
1265 }
1266
1267 #[test]
1268 fn test_qualified_std_call() {
1269 let source = r#"
1270const std = @import("std");
1271
1272fn process(data: []const u8) void {
1273 std.debug.print("Data: {any}\n", .{data});
1274}
1275 "#;
1276
1277 let (tree, content) = parse_zig(source);
1278 let mut staging = StagingGraph::new();
1279 let builder = ZigGraphBuilder::default();
1280
1281 builder
1282 .build_graph(&tree, &content, Path::new("test.zig"), &mut staging)
1283 .unwrap();
1284
1285 assert_has_import_edge(&staging, FILE_MODULE_NAME, "std");
1287
1288 assert_has_node_with_kind(&staging, "process", NodeKind::Function);
1290
1291 assert_has_call_edge(&staging, "process", "std::debug::print");
1293 assert_has_display_call_edge(&staging, "process", "std.debug.print");
1294 }
1295
1296 #[test]
1297 fn test_argument_counting_zero_args() {
1298 let source = r"
1299fn getValue() i32 {
1300 return 42;
1301}
1302
1303fn main() void {
1304 const x = getValue();
1305}
1306 ";
1307
1308 let (tree, content) = parse_zig(source);
1309 let mut staging = StagingGraph::new();
1310 let builder = ZigGraphBuilder::default();
1311
1312 builder
1313 .build_graph(&tree, &content, Path::new("test.zig"), &mut staging)
1314 .unwrap();
1315
1316 assert_has_call_edge(&staging, "main", "getValue");
1318
1319 let call_edges = collect_call_edges(&staging);
1321 assert_eq!(call_edges.len(), 1, "Expected exactly one call edge");
1322 }
1323
1324 #[test]
1325 fn test_argument_counting_multiple_args() {
1326 let source = r"
1327fn calculate(a: i32, b: i32, c: i32) i32 {
1328 return a + b + c;
1329}
1330
1331fn main() void {
1332 const result = calculate(1, 2, 3);
1333}
1334 ";
1335
1336 let (tree, content) = parse_zig(source);
1337 let mut staging = StagingGraph::new();
1338 let builder = ZigGraphBuilder::default();
1339
1340 builder
1341 .build_graph(&tree, &content, Path::new("test.zig"), &mut staging)
1342 .unwrap();
1343
1344 assert_has_call_edge(&staging, "main", "calculate");
1346
1347 let call_edges = collect_call_edges(&staging);
1349 assert_eq!(call_edges.len(), 1, "Expected exactly one call edge");
1350 }
1351
1352 #[test]
1353 fn test_nested_function() {
1354 let source = r"
1360fn outer() void {
1361 fn inner() void {
1362 return;
1363 }
1364
1365 inner();
1366}
1367 ";
1368
1369 let (tree, content) = parse_zig(source);
1370 let mut staging = StagingGraph::new();
1371 let builder = ZigGraphBuilder::default();
1372
1373 builder
1374 .build_graph(&tree, &content, Path::new("test.zig"), &mut staging)
1375 .unwrap();
1376
1377 assert_has_node_with_kind(&staging, "outer", NodeKind::Function);
1379
1380 }
1383
1384 #[test]
1385 fn test_method_call_as_qualified() {
1386 let source = r"
1387const ArrayList = struct {
1388 fn append(self: *ArrayList, item: i32) void {
1389 // implementation
1390 }
1391};
1392
1393fn main() void {
1394 var list: ArrayList = undefined;
1395 list.append(42);
1396}
1397 ";
1398
1399 let (tree, content) = parse_zig(source);
1400 let mut staging = StagingGraph::new();
1401 let builder = ZigGraphBuilder::default();
1402
1403 builder
1404 .build_graph(&tree, &content, Path::new("test.zig"), &mut staging)
1405 .unwrap();
1406
1407 assert_has_node_with_kind_exact(&staging, "ArrayList::append", NodeKind::Function);
1409 assert!(
1410 has_display_name(&staging, "ArrayList::append", "ArrayList.append"),
1411 "Struct methods should display with Zig native dot syntax"
1412 );
1413
1414 assert_has_node_with_kind(&staging, "main", NodeKind::Function);
1416
1417 assert_has_call_edge(&staging, "main", "list::append");
1419 assert_has_display_call_edge(&staging, "main", "list.append");
1420 }
1421
1422 #[test]
1423 fn test_stdlib_qualified_call() {
1424 let source = r#"
1425const std = @import("std");
1426
1427fn copyData(dest: []u8, src: []const u8) void {
1428 std.mem.copy(u8, dest, src);
1429}
1430 "#;
1431
1432 let (tree, content) = parse_zig(source);
1433 let mut staging = StagingGraph::new();
1434 let builder = ZigGraphBuilder::default();
1435
1436 builder
1437 .build_graph(&tree, &content, Path::new("test.zig"), &mut staging)
1438 .unwrap();
1439
1440 assert_has_import_edge(&staging, FILE_MODULE_NAME, "std");
1442
1443 assert_has_node_with_kind(&staging, "copyData", NodeKind::Function);
1445
1446 assert_has_call_edge(&staging, "copyData", "std::mem::copy");
1448 assert_has_display_call_edge(&staging, "copyData", "std.mem.copy");
1449 }
1450
1451 #[test]
1452 fn test_private_function_visibility() {
1453 let source = r"
1454fn privateHelper() void {
1455 return;
1456}
1457
1458pub fn publicFunction() void {
1459 privateHelper();
1460}
1461 ";
1462
1463 let (tree, content) = parse_zig(source);
1464 let mut staging = StagingGraph::new();
1465 let builder = ZigGraphBuilder::default();
1466
1467 builder
1468 .build_graph(&tree, &content, Path::new("test.zig"), &mut staging)
1469 .unwrap();
1470
1471 assert_has_node_with_kind(&staging, "privateHelper", NodeKind::Function);
1473 assert_has_node_with_kind(&staging, "publicFunction", NodeKind::Function);
1474
1475 assert_has_export_edge(&staging, FILE_MODULE_NAME, "publicFunction");
1477
1478 let export_edges = collect_export_edges(&staging);
1480 assert_eq!(export_edges.len(), 1, "Expected only one export edge");
1481 }
1482
1483 #[test]
1484 fn test_multiple_calls_in_function() {
1485 let source = r"
1486fn helper1() void {}
1487fn helper2() void {}
1488fn helper3() void {}
1489
1490fn main() void {
1491 helper1();
1492 helper2();
1493 helper3();
1494}
1495 ";
1496
1497 let (tree, content) = parse_zig(source);
1498 let mut staging = StagingGraph::new();
1499 let builder = ZigGraphBuilder::default();
1500
1501 builder
1502 .build_graph(&tree, &content, Path::new("test.zig"), &mut staging)
1503 .unwrap();
1504
1505 assert_has_node_with_kind(&staging, "helper1", NodeKind::Function);
1507 assert_has_node_with_kind(&staging, "helper2", NodeKind::Function);
1508 assert_has_node_with_kind(&staging, "helper3", NodeKind::Function);
1509 assert_has_node_with_kind(&staging, "main", NodeKind::Function);
1510
1511 assert_has_call_edge(&staging, "main", "helper1");
1513 assert_has_call_edge(&staging, "main", "helper2");
1514 assert_has_call_edge(&staging, "main", "helper3");
1515
1516 let call_edges = collect_call_edges(&staging);
1518 assert_eq!(call_edges.len(), 3, "Expected exactly three call edges");
1519 }
1520
1521 #[test]
1522 fn test_builtin_function_calls() {
1523 let source = r#"
1524const std = @import("std");
1525
1526fn useBuiltins(dest: []u8, src: []const u8) void {
1527 @memcpy(dest.ptr, src.ptr, src.len);
1528 const info = @typeInfo(@TypeOf(dest));
1529}
1530 "#;
1531
1532 let (tree, content) = parse_zig(source);
1533 let mut staging = StagingGraph::new();
1534 let builder = ZigGraphBuilder::default();
1535
1536 builder
1537 .build_graph(&tree, &content, Path::new("test.zig"), &mut staging)
1538 .unwrap();
1539
1540 assert_has_import_edge(&staging, FILE_MODULE_NAME, "std");
1542
1543 assert_has_node_with_kind(&staging, "useBuiltins", NodeKind::Function);
1545
1546 assert_has_call_edge(&staging, "useBuiltins", "@memcpy");
1548 assert_has_call_edge(&staging, "useBuiltins", "@typeInfo");
1549 }
1550
1551 #[test]
1552 fn test_struct_methods_with_same_name() {
1553 let source = r"
1555const ArrayList = struct {
1556 fn init() ArrayList {
1557 return undefined;
1558 }
1559
1560 fn deinit(self: *ArrayList) void {
1561 // cleanup
1562 }
1563
1564 fn append(self: *ArrayList, item: i32) void {
1565 // add item
1566 }
1567};
1568
1569const HashMap = struct {
1570 fn init() HashMap {
1571 return undefined;
1572 }
1573
1574 fn deinit(self: *HashMap) void {
1575 // cleanup
1576 }
1577};
1578
1579fn main() void {
1580 var list = ArrayList.init();
1581 list.append(42);
1582 list.deinit();
1583
1584 var map = HashMap.init();
1585 map.deinit();
1586}
1587 ";
1588
1589 let (tree, content) = parse_zig(source);
1590 let mut staging = StagingGraph::new();
1591 let builder = ZigGraphBuilder::default();
1592
1593 builder
1594 .build_graph(&tree, &content, Path::new("test.zig"), &mut staging)
1595 .unwrap();
1596
1597 assert_has_node_with_kind_exact(&staging, "ArrayList::init", NodeKind::Function);
1599 assert_has_node_with_kind_exact(&staging, "ArrayList::deinit", NodeKind::Function);
1600 assert_has_node_with_kind_exact(&staging, "ArrayList::append", NodeKind::Function);
1601 assert_has_node_with_kind_exact(&staging, "HashMap::init", NodeKind::Function);
1602 assert_has_node_with_kind_exact(&staging, "HashMap::deinit", NodeKind::Function);
1603 assert_has_node_with_kind(&staging, "main", NodeKind::Function);
1604 assert!(has_display_name(
1605 &staging,
1606 "ArrayList::init",
1607 "ArrayList.init"
1608 ));
1609 assert!(has_display_name(
1610 &staging,
1611 "ArrayList::deinit",
1612 "ArrayList.deinit"
1613 ));
1614 assert!(has_display_name(
1615 &staging,
1616 "ArrayList::append",
1617 "ArrayList.append"
1618 ));
1619 assert!(has_display_name(&staging, "HashMap::init", "HashMap.init"));
1620 assert!(has_display_name(
1621 &staging,
1622 "HashMap::deinit",
1623 "HashMap.deinit"
1624 ));
1625
1626 let func_count = count_nodes_by_kind(&staging, NodeKind::Function);
1630 assert!(
1631 func_count >= 6,
1632 "Expected at least 6 functions (5 methods + main), got {func_count}"
1633 );
1634 }
1635
1636 #[test]
1637 fn test_method_call_normalization() {
1638 let source = r"
1641const ArrayList = struct {
1642 fn init() ArrayList {
1643 return undefined;
1644 }
1645
1646 fn deinit(self: *ArrayList) void {
1647 // cleanup
1648 }
1649};
1650
1651fn main() void {
1652 var list = ArrayList.init();
1653 list.deinit(); // This should resolve to ArrayList.deinit, not list.deinit
1654}
1655 ";
1656
1657 let (tree, content) = parse_zig(source);
1658 let mut staging = StagingGraph::new();
1659 let builder = ZigGraphBuilder::default();
1660
1661 builder
1662 .build_graph(&tree, &content, Path::new("test.zig"), &mut staging)
1663 .unwrap();
1664
1665 assert_has_node_with_kind_exact(&staging, "ArrayList::init", NodeKind::Function);
1667 assert_has_node_with_kind_exact(&staging, "ArrayList::deinit", NodeKind::Function);
1668 assert!(has_display_name(
1669 &staging,
1670 "ArrayList::init",
1671 "ArrayList.init"
1672 ));
1673 assert!(has_display_name(
1674 &staging,
1675 "ArrayList::deinit",
1676 "ArrayList.deinit"
1677 ));
1678
1679 assert_has_node_with_kind(&staging, "main", NodeKind::Function);
1681
1682 assert_has_call_edge(&staging, "main", "ArrayList::init");
1684 assert_has_call_edge(&staging, "main", "list::deinit");
1685 assert_has_display_call_edge(&staging, "main", "ArrayList.init");
1686 assert_has_display_call_edge(&staging, "main", "list.deinit");
1687 }
1688
1689 #[test]
1690 fn test_language_is_zig() {
1691 let source = r"
1692fn test_function() void {
1693 return;
1694}
1695 ";
1696
1697 let (tree, content) = parse_zig(source);
1698 let mut staging = StagingGraph::new();
1699 let builder = ZigGraphBuilder::default();
1700
1701 builder
1702 .build_graph(&tree, &content, Path::new("test.zig"), &mut staging)
1703 .unwrap();
1704
1705 assert_has_node_with_kind(&staging, "test_function", NodeKind::Function);
1707
1708 assert_eq!(builder.language(), Language::Zig);
1710 }
1711
1712 #[test]
1713 fn test_import_builtin_detection() {
1714 let source = r#"
1715const std = @import("std");
1716const other = @import("other.zig");
1717
1718fn main() void {
1719 std.debug.print("Hello\n", .{});
1720}
1721 "#;
1722
1723 let (tree, content) = parse_zig(source);
1724 let mut staging = StagingGraph::new();
1725 let builder = ZigGraphBuilder::default();
1726
1727 builder
1728 .build_graph(&tree, &content, Path::new("test.zig"), &mut staging)
1729 .unwrap();
1730
1731 assert_has_node_with_kind(&staging, "std", NodeKind::Import);
1733 assert_has_node_with_kind_exact(&staging, "other::zig", NodeKind::Import);
1734 assert_has_import_edge(&staging, FILE_MODULE_NAME, "std");
1735 assert_has_import_edge(&staging, FILE_MODULE_NAME, "other::zig");
1736 assert!(has_display_name(&staging, "other::zig", "other.zig"));
1737 assert_has_display_import_edge(&staging, FILE_MODULE_NAME, "other.zig");
1738
1739 assert_has_node_with_kind(&staging, "main", NodeKind::Function);
1741 assert_has_call_edge(&staging, "main", "std::debug::print");
1742 assert_has_display_call_edge(&staging, "main", "std.debug.print");
1743 }
1744
1745 #[test]
1746 fn test_import_in_function() {
1747 let source = r#"
1749fn loadModule() void {
1750 const module = @import("dynamic.zig");
1751 module.init();
1752}
1753 "#;
1754
1755 let (tree, content) = parse_zig(source);
1756 let mut staging = StagingGraph::new();
1757 let builder = ZigGraphBuilder::default();
1758
1759 builder
1760 .build_graph(&tree, &content, Path::new("test.zig"), &mut staging)
1761 .unwrap();
1762
1763 assert_has_node_with_kind_exact(&staging, "dynamic::zig", NodeKind::Import);
1765 assert_has_import_edge(&staging, "loadModule", "dynamic::zig");
1766 assert!(has_display_name(&staging, "dynamic::zig", "dynamic.zig"));
1767 assert_has_display_import_edge(&staging, "loadModule", "dynamic.zig");
1768
1769 assert_has_node_with_kind(&staging, "loadModule", NodeKind::Function);
1771 assert_has_call_edge(&staging, "loadModule", "module::init");
1772 assert_has_display_call_edge(&staging, "loadModule", "module.init");
1773 }
1774
1775 #[test]
1776 fn test_builtin_non_import_still_creates_call() {
1777 let source = r#"
1779fn copyMemory(dest: []u8, src: []const u8) void {
1780 @memcpy(dest.ptr, src.ptr, src.len);
1781}
1782 "#;
1783
1784 let (tree, content) = parse_zig(source);
1785 let mut staging = StagingGraph::new();
1786 let builder = ZigGraphBuilder::default();
1787
1788 builder
1789 .build_graph(&tree, &content, Path::new("test.zig"), &mut staging)
1790 .unwrap();
1791
1792 assert_has_node_with_kind(&staging, "copyMemory", NodeKind::Function);
1794
1795 assert_has_call_edge(&staging, "copyMemory", "@memcpy");
1797
1798 let import_edges = collect_import_edges(&staging);
1800 assert_eq!(
1801 import_edges.len(),
1802 0,
1803 "Non-import builtins should not create import edges"
1804 );
1805 }
1806
1807 #[test]
1808 fn test_export_pub_function() {
1809 let source = r"
1810pub fn add(a: i32, b: i32) i32 {
1811 return a + b;
1812}
1813
1814fn privateHelper() i32 {
1815 return 42;
1816}
1817 ";
1818
1819 let (tree, content) = parse_zig(source);
1820 let mut staging = StagingGraph::new();
1821 let builder = ZigGraphBuilder::default();
1822
1823 builder
1824 .build_graph(&tree, &content, Path::new("test.zig"), &mut staging)
1825 .unwrap();
1826
1827 assert_has_node_with_kind(&staging, "add", NodeKind::Function);
1829 assert_has_node_with_kind(&staging, "privateHelper", NodeKind::Function);
1830
1831 assert_has_export_edge(&staging, FILE_MODULE_NAME, "add");
1833
1834 let export_edges = collect_export_edges(&staging);
1836 assert_eq!(export_edges.len(), 1, "Expected only one export edge");
1837 }
1838
1839 #[test]
1840 fn test_export_pub_const_type() {
1841 let source = r#"
1842pub const Point = struct {
1843 x: f32,
1844 y: f32,
1845
1846 pub fn distance(self: Point) f32 {
1847 return @sqrt(self.x * self.x + self.y * self.y);
1848 }
1849};
1850
1851const PrivateType = struct {
1852 value: i32,
1853};
1854
1855pub const API_VERSION = "1.0.0";
1856 "#;
1857
1858 let (tree, content) = parse_zig(source);
1859 let mut staging = StagingGraph::new();
1860 let builder = ZigGraphBuilder::default();
1861
1862 builder
1863 .build_graph(&tree, &content, Path::new("test.zig"), &mut staging)
1864 .unwrap();
1865
1866 assert_has_node_with_kind(&staging, "Point", NodeKind::Type);
1868 assert_has_node_with_kind(&staging, "API_VERSION", NodeKind::Type);
1869
1870 assert_has_export_edge(&staging, FILE_MODULE_NAME, "Point");
1872 assert_has_export_edge(&staging, FILE_MODULE_NAME, "API_VERSION");
1873
1874 let export_edges = collect_export_edges(&staging);
1876 assert!(
1880 export_edges.len() >= 2,
1881 "Expected at least two export edges (Point and API_VERSION)"
1882 );
1883 }
1884
1885 #[test]
1886 fn test_export_nested_pub_in_private_container() {
1887 let source = r"
1888const PrivateContainer = struct {
1889 pub fn publicMethod() i32 {
1890 return 42;
1891 }
1892
1893 pub const PUBLIC_CONST: i32 = 100;
1894};
1895
1896pub const PublicContainer = struct {
1897 fn privateMethod() i32 {
1898 return 42;
1899 }
1900};
1901 ";
1902
1903 let (tree, content) = parse_zig(source);
1904 let mut staging = StagingGraph::new();
1905 let builder = ZigGraphBuilder::default();
1906
1907 builder
1908 .build_graph(&tree, &content, Path::new("test.zig"), &mut staging)
1909 .unwrap();
1910
1911 assert_has_export_edge(&staging, FILE_MODULE_NAME, "PublicContainer");
1913
1914 let export_edges = collect_export_edges(&staging);
1920 assert!(
1921 !export_edges.is_empty(),
1922 "Expected at least one export edge (PublicContainer)"
1923 );
1924 }
1925}