1#![allow(clippy::similar_names)] use std::collections::HashMap;
42use std::path::Path;
43
44use super::super::edge::kind::{LifetimeConstraintKind, MacroExpansionKind, TypeOfContext};
45use super::super::resolution::canonicalize_graph_qualified_name;
46use super::staging::StagingGraph;
47use crate::graph::node::{Language, Span};
48use crate::graph::unified::edge::{EdgeKind, ExportKind, FfiConvention, HttpMethod, TableWriteOp};
49use crate::graph::unified::file::FileId;
50use crate::graph::unified::node::{NodeId, NodeKind};
51use crate::graph::unified::storage::NodeEntry;
52use crate::graph::unified::string::StringId;
53
54#[derive(Debug)]
61pub struct GraphBuildHelper<'a> {
62 staging: &'a mut StagingGraph,
64 language: Language,
66 file_id: FileId,
68 file_path: String,
70 string_cache: HashMap<String, StringId>,
72 next_string_id: u32,
74 node_cache: HashMap<(String, NodeKind), NodeId>,
82}
83
84impl<'a> GraphBuildHelper<'a> {
85 pub fn new(staging: &'a mut StagingGraph, file: &Path, language: Language) -> Self {
90 Self {
91 staging,
92 language,
93 file_id: FileId::new(0), file_path: file.display().to_string(),
95 string_cache: HashMap::new(),
96 next_string_id: 0,
97 node_cache: HashMap::new(),
98 }
99 }
100
101 pub fn with_file_id(
103 staging: &'a mut StagingGraph,
104 file: &Path,
105 language: Language,
106 file_id: FileId,
107 ) -> Self {
108 Self {
109 staging,
110 language,
111 file_id,
112 file_path: file.display().to_string(),
113 string_cache: HashMap::new(),
114 next_string_id: 0,
115 node_cache: HashMap::new(),
116 }
117 }
118
119 #[must_use]
121 pub fn language(&self) -> Language {
122 self.language
123 }
124
125 #[must_use]
127 pub fn file_id(&self) -> FileId {
128 self.file_id
129 }
130
131 #[must_use]
133 pub fn file_path(&self) -> &str {
134 &self.file_path
135 }
136
137 pub fn intern(&mut self, s: &str) -> StringId {
143 if let Some(&id) = self.string_cache.get(s) {
144 return id;
145 }
146
147 let id = StringId::new_local(self.next_string_id);
148 self.next_string_id += 1;
149 self.string_cache.insert(s.to_string(), id);
150 self.staging.intern_string(id, s.to_string());
152 id
153 }
154
155 #[must_use]
157 pub fn has_node(&self, qualified_name: &str) -> bool {
158 self.node_cache
159 .keys()
160 .any(|(name, _)| name == qualified_name)
161 }
162
163 #[must_use]
165 pub fn get_node(&self, qualified_name: &str) -> Option<NodeId> {
166 self.node_cache
167 .iter()
168 .find_map(|((name, _), id)| (name == qualified_name).then_some(*id))
169 }
170
171 #[must_use]
173 pub fn has_node_with_kind(&self, qualified_name: &str, kind: NodeKind) -> bool {
174 self.node_cache
175 .contains_key(&(qualified_name.to_string(), kind))
176 }
177
178 #[must_use]
180 pub fn get_node_with_kind(&self, qualified_name: &str, kind: NodeKind) -> Option<NodeId> {
181 self.node_cache
182 .get(&(qualified_name.to_string(), kind))
183 .copied()
184 }
185
186 pub fn add_function(
190 &mut self,
191 qualified_name: &str,
192 span: Option<Span>,
193 is_async: bool,
194 is_unsafe: bool,
195 ) -> NodeId {
196 self.add_node_internal(
197 qualified_name,
198 span,
199 NodeKind::Function,
200 &[("async", is_async), ("unsafe", is_unsafe)],
201 None,
202 None,
203 )
204 }
205
206 pub fn add_function_with_visibility(
210 &mut self,
211 qualified_name: &str,
212 span: Option<Span>,
213 is_async: bool,
214 is_unsafe: bool,
215 visibility: Option<&str>,
216 ) -> NodeId {
217 self.add_node_internal(
218 qualified_name,
219 span,
220 NodeKind::Function,
221 &[("async", is_async), ("unsafe", is_unsafe)],
222 visibility,
223 None,
224 )
225 }
226
227 pub fn add_function_with_signature(
232 &mut self,
233 qualified_name: &str,
234 span: Option<Span>,
235 is_async: bool,
236 is_unsafe: bool,
237 visibility: Option<&str>,
238 signature: Option<&str>,
239 ) -> NodeId {
240 self.add_node_internal(
241 qualified_name,
242 span,
243 NodeKind::Function,
244 &[("async", is_async), ("unsafe", is_unsafe)],
245 visibility,
246 signature,
247 )
248 }
249
250 pub fn add_method(
252 &mut self,
253 qualified_name: &str,
254 span: Option<Span>,
255 is_async: bool,
256 is_static: bool,
257 ) -> NodeId {
258 self.add_node_internal(
259 qualified_name,
260 span,
261 NodeKind::Method,
262 &[("async", is_async), ("static", is_static)],
263 None,
264 None,
265 )
266 }
267
268 pub fn add_method_with_visibility(
270 &mut self,
271 qualified_name: &str,
272 span: Option<Span>,
273 is_async: bool,
274 is_static: bool,
275 visibility: Option<&str>,
276 ) -> NodeId {
277 self.add_node_internal(
278 qualified_name,
279 span,
280 NodeKind::Method,
281 &[("async", is_async), ("static", is_static)],
282 visibility,
283 None,
284 )
285 }
286
287 pub fn add_method_with_signature(
292 &mut self,
293 qualified_name: &str,
294 span: Option<Span>,
295 is_async: bool,
296 is_static: bool,
297 visibility: Option<&str>,
298 signature: Option<&str>,
299 ) -> NodeId {
300 self.add_node_internal(
301 qualified_name,
302 span,
303 NodeKind::Method,
304 &[("async", is_async), ("static", is_static)],
305 visibility,
306 signature,
307 )
308 }
309
310 pub fn add_class(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
312 self.add_node_internal(qualified_name, span, NodeKind::Class, &[], None, None)
313 }
314
315 pub fn add_class_with_visibility(
317 &mut self,
318 qualified_name: &str,
319 span: Option<Span>,
320 visibility: Option<&str>,
321 ) -> NodeId {
322 self.add_node_internal(qualified_name, span, NodeKind::Class, &[], visibility, None)
323 }
324
325 pub fn add_struct(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
327 self.add_node_internal(qualified_name, span, NodeKind::Struct, &[], None, None)
328 }
329
330 pub fn add_struct_with_visibility(
332 &mut self,
333 qualified_name: &str,
334 span: Option<Span>,
335 visibility: Option<&str>,
336 ) -> NodeId {
337 self.add_node_internal(
338 qualified_name,
339 span,
340 NodeKind::Struct,
341 &[],
342 visibility,
343 None,
344 )
345 }
346
347 pub fn add_module(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
349 self.add_node_internal(qualified_name, span, NodeKind::Module, &[], None, None)
350 }
351
352 pub fn add_resource(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
354 self.add_node_internal(qualified_name, span, NodeKind::Resource, &[], None, None)
355 }
356
357 pub fn add_endpoint(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
365 self.add_node_internal(qualified_name, span, NodeKind::Endpoint, &[], None, None)
366 }
367
368 pub fn add_import(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
370 self.add_node_internal(qualified_name, span, NodeKind::Import, &[], None, None)
371 }
372
373 pub fn add_verbatim_import(&mut self, name: &str, span: Option<Span>) -> NodeId {
379 self.add_node_verbatim(name, span, NodeKind::Import, &[], None, None)
380 }
381
382 pub fn add_variable(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
384 self.add_node_internal(qualified_name, span, NodeKind::Variable, &[], None, None)
385 }
386
387 pub fn add_verbatim_variable(&mut self, name: &str, span: Option<Span>) -> NodeId {
392 self.add_node_verbatim(name, span, NodeKind::Variable, &[], None, None)
393 }
394
395 pub fn add_constant(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
397 self.add_node_internal(qualified_name, span, NodeKind::Constant, &[], None, None)
398 }
399
400 pub fn add_constant_with_visibility(
402 &mut self,
403 qualified_name: &str,
404 span: Option<Span>,
405 visibility: Option<&str>,
406 ) -> NodeId {
407 self.add_node_internal(
408 qualified_name,
409 span,
410 NodeKind::Constant,
411 &[],
412 visibility,
413 None,
414 )
415 }
416
417 pub fn add_constant_with_static_and_visibility(
419 &mut self,
420 qualified_name: &str,
421 span: Option<Span>,
422 is_static: bool,
423 visibility: Option<&str>,
424 ) -> NodeId {
425 let attrs: &[(&str, bool)] = if is_static { &[("static", true)] } else { &[] };
426 self.add_node_internal(
427 qualified_name,
428 span,
429 NodeKind::Constant,
430 attrs,
431 visibility,
432 None,
433 )
434 }
435
436 pub fn add_property_with_static_and_visibility(
438 &mut self,
439 qualified_name: &str,
440 span: Option<Span>,
441 is_static: bool,
442 visibility: Option<&str>,
443 ) -> NodeId {
444 let attrs: &[(&str, bool)] = if is_static { &[("static", true)] } else { &[] };
445 self.add_node_internal(
446 qualified_name,
447 span,
448 NodeKind::Property,
449 attrs,
450 visibility,
451 None,
452 )
453 }
454
455 pub fn add_enum(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
457 self.add_node_internal(qualified_name, span, NodeKind::Enum, &[], None, None)
458 }
459
460 pub fn add_enum_with_visibility(
462 &mut self,
463 qualified_name: &str,
464 span: Option<Span>,
465 visibility: Option<&str>,
466 ) -> NodeId {
467 self.add_node_internal(qualified_name, span, NodeKind::Enum, &[], visibility, None)
468 }
469
470 pub fn add_interface(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
472 self.add_node_internal(qualified_name, span, NodeKind::Interface, &[], None, None)
473 }
474
475 pub fn add_interface_with_visibility(
477 &mut self,
478 qualified_name: &str,
479 span: Option<Span>,
480 visibility: Option<&str>,
481 ) -> NodeId {
482 self.add_node_internal(
483 qualified_name,
484 span,
485 NodeKind::Interface,
486 &[],
487 visibility,
488 None,
489 )
490 }
491
492 pub fn add_type(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
494 self.add_node_internal(qualified_name, span, NodeKind::Type, &[], None, None)
495 }
496
497 pub fn add_type_with_visibility(
499 &mut self,
500 qualified_name: &str,
501 span: Option<Span>,
502 visibility: Option<&str>,
503 ) -> NodeId {
504 self.add_node_internal(qualified_name, span, NodeKind::Type, &[], visibility, None)
505 }
506
507 pub fn add_lifetime(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
509 self.add_node_internal(qualified_name, span, NodeKind::Lifetime, &[], None, None)
510 }
511
512 pub fn add_lifetime_constraint_edge(
514 &mut self,
515 source: NodeId,
516 target: NodeId,
517 constraint_kind: LifetimeConstraintKind,
518 ) {
519 self.staging.add_edge(
520 source,
521 target,
522 EdgeKind::LifetimeConstraint { constraint_kind },
523 self.file_id,
524 );
525 }
526
527 pub fn add_trait_method_binding_edge(
532 &mut self,
533 caller: NodeId,
534 callee: NodeId,
535 trait_name: &str,
536 impl_type: &str,
537 is_ambiguous: bool,
538 ) {
539 let trait_name_id = self.intern(trait_name);
540 let impl_type_id = self.intern(impl_type);
541 self.staging.add_edge(
542 caller,
543 callee,
544 EdgeKind::TraitMethodBinding {
545 trait_name: trait_name_id,
546 impl_type: impl_type_id,
547 is_ambiguous,
548 },
549 self.file_id,
550 );
551 }
552
553 pub fn add_macro_expansion_edge(
579 &mut self,
580 invocation: NodeId,
581 expansion: NodeId,
582 expansion_kind: MacroExpansionKind,
583 is_verified: bool,
584 ) {
585 self.staging.add_edge(
586 invocation,
587 expansion,
588 EdgeKind::MacroExpansion {
589 expansion_kind,
590 is_verified,
591 },
592 self.file_id,
593 );
594 }
595
596 pub fn add_node(&mut self, qualified_name: &str, span: Option<Span>, kind: NodeKind) -> NodeId {
598 self.add_node_internal(qualified_name, span, kind, &[], None, None)
599 }
600
601 pub fn add_node_with_visibility(
603 &mut self,
604 qualified_name: &str,
605 span: Option<Span>,
606 kind: NodeKind,
607 visibility: Option<&str>,
608 ) -> NodeId {
609 self.add_node_internal(qualified_name, span, kind, &[], visibility, None)
610 }
611
612 fn add_node_internal(
622 &mut self,
623 qualified_name: &str,
624 span: Option<Span>,
625 kind: NodeKind,
626 attributes: &[(&str, bool)],
627 visibility: Option<&str>,
628 signature: Option<&str>,
629 ) -> NodeId {
630 let canonical_qualified_name =
631 canonicalize_graph_qualified_name(self.language, qualified_name);
632 let semantic_name = semantic_name_for_node_input(qualified_name, &canonical_qualified_name);
633 let mut is_async = false;
634 let mut is_static = false;
635 let mut is_unsafe = false;
636 for &(key, value) in attributes {
637 match key {
638 "async" => is_async |= value,
639 "static" => is_static |= value,
640 "unsafe" => is_unsafe |= value,
641 _ => {}
642 }
643 }
644
645 if let Some(&id) = self
647 .node_cache
648 .get(&(canonical_qualified_name.clone(), kind))
649 {
650 let visibility_id = visibility.map(|vis| self.intern(vis));
651 let signature_id = signature.map(|sig| self.intern(sig));
652 self.staging.update_node_entry(
653 id,
654 span,
655 is_async,
656 is_static,
657 is_unsafe,
658 visibility_id,
659 signature_id,
660 );
661 return id;
662 }
663
664 let name_id = self.intern(&semantic_name);
665
666 let mut entry = NodeEntry::new(kind, name_id, self.file_id);
668 if semantic_name != canonical_qualified_name {
669 let qualified_name_id = self.intern(&canonical_qualified_name);
670 entry = entry.with_qualified_name(qualified_name_id);
671 }
672
673 if let Some(s) = span {
675 let start_line = u32::try_from(s.start.line.saturating_add(1)).unwrap_or(u32::MAX);
676 let start_column = u32::try_from(s.start.column).unwrap_or(u32::MAX);
677 let end_line = u32::try_from(s.end.line.saturating_add(1)).unwrap_or(u32::MAX);
678 let end_column = u32::try_from(s.end.column).unwrap_or(u32::MAX);
679 entry = entry.with_location(start_line, start_column, end_line, end_column);
680 }
681
682 if is_async {
684 entry = entry.with_async(true);
685 }
686 if is_static {
687 entry = entry.with_static(true);
688 }
689 if is_unsafe {
690 entry = entry.with_unsafe(true);
691 }
692
693 if let Some(vis) = visibility {
695 let vis_id = self.intern(vis);
696 entry = entry.with_visibility(vis_id);
697 }
698
699 if let Some(sig) = signature {
701 let sig_id = self.intern(sig);
702 entry = entry.with_signature(sig_id);
703 }
704
705 let node_id = self.staging.add_node(entry);
707
708 self.node_cache
710 .insert((canonical_qualified_name, kind), node_id);
711
712 node_id
713 }
714
715 fn add_node_verbatim(
716 &mut self,
717 name: &str,
718 span: Option<Span>,
719 kind: NodeKind,
720 attributes: &[(&str, bool)],
721 visibility: Option<&str>,
722 signature: Option<&str>,
723 ) -> NodeId {
724 let mut is_async = false;
725 let mut is_static = false;
726 let mut is_unsafe = false;
727 for &(key, value) in attributes {
728 match key {
729 "async" => is_async |= value,
730 "static" => is_static |= value,
731 "unsafe" => is_unsafe |= value,
732 _ => {}
733 }
734 }
735
736 if let Some(&id) = self.node_cache.get(&(name.to_string(), kind)) {
737 let visibility_id = visibility.map(|vis| self.intern(vis));
738 let signature_id = signature.map(|sig| self.intern(sig));
739 self.staging.update_node_entry(
740 id,
741 span,
742 is_async,
743 is_static,
744 is_unsafe,
745 visibility_id,
746 signature_id,
747 );
748 return id;
749 }
750
751 let name_id = self.intern(name);
752 let mut entry = NodeEntry::new(kind, name_id, self.file_id);
753
754 if let Some(s) = span {
755 let start_line = u32::try_from(s.start.line.saturating_add(1)).unwrap_or(u32::MAX);
756 let start_column = u32::try_from(s.start.column).unwrap_or(u32::MAX);
757 let end_line = u32::try_from(s.end.line.saturating_add(1)).unwrap_or(u32::MAX);
758 let end_column = u32::try_from(s.end.column).unwrap_or(u32::MAX);
759 entry = entry.with_location(start_line, start_column, end_line, end_column);
760 }
761
762 if is_async {
763 entry = entry.with_async(true);
764 }
765 if is_static {
766 entry = entry.with_static(true);
767 }
768 if is_unsafe {
769 entry = entry.with_unsafe(true);
770 }
771
772 if let Some(vis) = visibility {
773 let vis_id = self.intern(vis);
774 entry = entry.with_visibility(vis_id);
775 }
776 if let Some(sig) = signature {
777 let sig_id = self.intern(sig);
778 entry = entry.with_signature(sig_id);
779 }
780
781 let node_id = self.staging.add_node(entry);
782 self.node_cache.insert((name.to_string(), kind), node_id);
783 node_id
784 }
785
786 pub fn add_call_edge(&mut self, caller: NodeId, callee: NodeId) {
788 self.add_call_edge_with_span(caller, callee, Vec::new());
789 }
790
791 pub fn add_call_edge_with_span(
801 &mut self,
802 caller: NodeId,
803 callee: NodeId,
804 spans: Vec<crate::graph::node::Span>,
805 ) {
806 self.staging.add_edge_with_spans(
807 caller,
808 callee,
809 EdgeKind::Calls {
810 argument_count: 255,
811 is_async: false,
812 },
813 self.file_id,
814 spans,
815 );
816 }
817
818 pub fn add_call_edge_full(
849 &mut self,
850 caller: NodeId,
851 callee: NodeId,
852 argument_count: u8,
853 is_async: bool,
854 ) {
855 self.staging.add_edge(
856 caller,
857 callee,
858 EdgeKind::Calls {
859 argument_count,
860 is_async,
861 },
862 self.file_id,
863 );
864 }
865
866 pub fn add_call_edge_full_with_span(
871 &mut self,
872 caller: NodeId,
873 callee: NodeId,
874 argument_count: u8,
875 is_async: bool,
876 spans: Vec<crate::graph::node::Span>,
877 ) {
878 self.staging.add_edge_with_spans(
879 caller,
880 callee,
881 EdgeKind::Calls {
882 argument_count,
883 is_async,
884 },
885 self.file_id,
886 spans,
887 );
888 }
889
890 pub fn add_table_read_edge_with_span(
892 &mut self,
893 reader: NodeId,
894 table: NodeId,
895 table_name: &str,
896 schema: Option<&str>,
897 spans: Vec<crate::graph::node::Span>,
898 ) {
899 let table_name_id = self.intern(table_name);
900 let schema_id = schema.map(|s| self.intern(s));
901 self.staging.add_edge_with_spans(
902 reader,
903 table,
904 EdgeKind::TableRead {
905 table_name: table_name_id,
906 schema: schema_id,
907 },
908 self.file_id,
909 spans,
910 );
911 }
912
913 pub fn add_table_write_edge_with_span(
915 &mut self,
916 writer: NodeId,
917 table: NodeId,
918 table_name: &str,
919 schema: Option<&str>,
920 operation: TableWriteOp,
921 spans: Vec<crate::graph::node::Span>,
922 ) {
923 let table_name_id = self.intern(table_name);
924 let schema_id = schema.map(|s| self.intern(s));
925 self.staging.add_edge_with_spans(
926 writer,
927 table,
928 EdgeKind::TableWrite {
929 table_name: table_name_id,
930 schema: schema_id,
931 operation,
932 },
933 self.file_id,
934 spans,
935 );
936 }
937
938 pub fn add_triggered_by_edge_with_span(
942 &mut self,
943 trigger: NodeId,
944 table: NodeId,
945 trigger_name: &str,
946 schema: Option<&str>,
947 spans: Vec<crate::graph::node::Span>,
948 ) {
949 let trigger_name_id = self.intern(trigger_name);
950 let schema_id = schema.map(|s| self.intern(s));
951 self.staging.add_edge_with_spans(
952 trigger,
953 table,
954 EdgeKind::TriggeredBy {
955 trigger_name: trigger_name_id,
956 schema: schema_id,
957 },
958 self.file_id,
959 spans,
960 );
961 }
962
963 pub fn add_import_edge(&mut self, importer: NodeId, imported: NodeId) {
969 self.staging.add_edge(
970 importer,
971 imported,
972 EdgeKind::Imports {
973 alias: None,
974 is_wildcard: false,
975 },
976 self.file_id,
977 );
978 }
979
980 pub fn add_import_edge_full(
1012 &mut self,
1013 importer: NodeId,
1014 imported: NodeId,
1015 alias: Option<&str>,
1016 is_wildcard: bool,
1017 ) {
1018 let alias_id = alias.map(|s| self.intern(s));
1019 self.staging.add_edge(
1020 importer,
1021 imported,
1022 EdgeKind::Imports {
1023 alias: alias_id,
1024 is_wildcard,
1025 },
1026 self.file_id,
1027 );
1028 }
1029
1030 pub fn add_export_edge(&mut self, module: NodeId, exported: NodeId) {
1036 self.staging.add_edge(
1037 module,
1038 exported,
1039 EdgeKind::Exports {
1040 kind: ExportKind::Direct,
1041 alias: None,
1042 },
1043 self.file_id,
1044 );
1045 }
1046
1047 pub fn add_export_edge_full(
1089 &mut self,
1090 module: NodeId,
1091 exported: NodeId,
1092 kind: ExportKind,
1093 alias: Option<&str>,
1094 ) {
1095 let alias_id = alias.map(|s| self.intern(s));
1096 self.staging.add_edge(
1097 module,
1098 exported,
1099 EdgeKind::Exports {
1100 kind,
1101 alias: alias_id,
1102 },
1103 self.file_id,
1104 );
1105 }
1106
1107 pub fn add_reference_edge(&mut self, from: NodeId, to: NodeId) {
1109 self.staging
1110 .add_edge(from, to, EdgeKind::References, self.file_id);
1111 }
1112
1113 pub fn add_defines_edge(&mut self, parent: NodeId, child: NodeId) {
1115 self.staging
1116 .add_edge(parent, child, EdgeKind::Defines, self.file_id);
1117 }
1118
1119 pub fn add_typeof_edge(&mut self, source: NodeId, target: NodeId) {
1124 self.add_typeof_edge_with_context(source, target, None, None, None);
1125 }
1126
1127 pub fn add_typeof_edge_with_context(
1166 &mut self,
1167 source: NodeId,
1168 target: NodeId,
1169 context: Option<TypeOfContext>,
1170 index: Option<u16>,
1171 name: Option<&str>,
1172 ) {
1173 let name_id = name.map(|n| self.intern(n));
1174 self.staging.add_edge(
1175 source,
1176 target,
1177 EdgeKind::TypeOf {
1178 context,
1179 index,
1180 name: name_id,
1181 },
1182 self.file_id,
1183 );
1184 }
1185
1186 pub fn add_implements_edge(&mut self, implementor: NodeId, interface: NodeId) {
1188 self.staging
1189 .add_edge(implementor, interface, EdgeKind::Implements, self.file_id);
1190 }
1191
1192 pub fn add_inherits_edge(&mut self, child: NodeId, parent: NodeId) {
1194 self.staging
1195 .add_edge(child, parent, EdgeKind::Inherits, self.file_id);
1196 }
1197
1198 pub fn add_contains_edge(&mut self, parent: NodeId, child: NodeId) {
1200 self.staging
1201 .add_edge(parent, child, EdgeKind::Contains, self.file_id);
1202 }
1203
1204 pub fn add_webassembly_edge(&mut self, caller: NodeId, wasm_target: NodeId) {
1211 self.staging
1212 .add_edge(caller, wasm_target, EdgeKind::WebAssemblyCall, self.file_id);
1213 }
1214
1215 pub fn add_ffi_edge(&mut self, caller: NodeId, ffi_target: NodeId, convention: FfiConvention) {
1223 self.staging.add_edge(
1224 caller,
1225 ffi_target,
1226 EdgeKind::FfiCall { convention },
1227 self.file_id,
1228 );
1229 }
1230
1231 pub fn add_http_request_edge(
1235 &mut self,
1236 caller: NodeId,
1237 target: NodeId,
1238 method: HttpMethod,
1239 url: Option<&str>,
1240 ) {
1241 let url_id = url.map(|value| self.intern(value));
1242 self.staging.add_edge(
1243 caller,
1244 target,
1245 EdgeKind::HttpRequest {
1246 method,
1247 url: url_id,
1248 },
1249 self.file_id,
1250 );
1251 }
1252
1253 pub fn ensure_function(
1257 &mut self,
1258 qualified_name: &str,
1259 span: Option<Span>,
1260 is_async: bool,
1261 is_unsafe: bool,
1262 ) -> NodeId {
1263 self.add_function(qualified_name, span, is_async, is_unsafe)
1264 }
1265
1266 pub fn ensure_method(
1268 &mut self,
1269 qualified_name: &str,
1270 span: Option<Span>,
1271 is_async: bool,
1272 is_static: bool,
1273 ) -> NodeId {
1274 self.add_method(qualified_name, span, is_async, is_static)
1275 }
1276
1277 #[must_use]
1279 pub fn stats(&self) -> HelperStats {
1280 let staging_stats = self.staging.stats();
1281 HelperStats {
1282 strings_interned: self.string_cache.len(),
1283 nodes_created: self.node_cache.len(),
1284 nodes_staged: staging_stats.nodes_staged,
1285 edges_staged: staging_stats.edges_staged,
1286 }
1287 }
1288}
1289
1290fn semantic_name_for_node_input(original: &str, canonical: &str) -> String {
1291 if original.contains('/') {
1292 return original.to_string();
1293 }
1294
1295 canonical
1296 .rsplit("::")
1297 .next()
1298 .map_or_else(|| original.to_string(), ToString::to_string)
1299}
1300
1301#[derive(Debug, Clone, Default)]
1303pub struct HelperStats {
1304 pub strings_interned: usize,
1306 pub nodes_created: usize,
1308 pub nodes_staged: usize,
1310 pub edges_staged: usize,
1312}
1313
1314#[cfg(test)]
1315mod tests {
1316 use super::*;
1317 use crate::graph::node::Position;
1318 use crate::graph::unified::build::staging::StagingOp;
1319 use std::path::PathBuf;
1320
1321 #[test]
1322 fn test_helper_add_function() {
1323 let mut staging = StagingGraph::new();
1324 let file = PathBuf::from("test.rs");
1325 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
1326
1327 let node_id = helper.add_function("main", None, false, false);
1328 assert!(!node_id.is_invalid());
1329 assert_eq!(helper.stats().nodes_created, 1);
1330 }
1331
1332 #[test]
1333 fn test_helper_deduplication() {
1334 let mut staging = StagingGraph::new();
1335 let file = PathBuf::from("test.rs");
1336 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
1337
1338 let id1 = helper.add_function("main", None, false, false);
1339 let id2 = helper.add_function("main", None, false, false);
1340
1341 assert_eq!(id1, id2, "Same function should return same NodeId");
1342 assert_eq!(
1343 helper.stats().nodes_created,
1344 1,
1345 "Should only create one node"
1346 );
1347 }
1348
1349 #[test]
1350 fn test_helper_string_interning() {
1351 let mut staging = StagingGraph::new();
1352 let file = PathBuf::from("test.rs");
1353 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
1354
1355 let s1 = helper.intern("hello");
1356 let s2 = helper.intern("world");
1357 let s3 = helper.intern("hello"); assert_ne!(s1, s2, "Different strings should have different IDs");
1360 assert_eq!(s1, s3, "Same string should return same ID");
1361 assert_eq!(helper.stats().strings_interned, 2);
1362 }
1363
1364 #[test]
1365 fn test_helper_add_call_edge() {
1366 let mut staging = StagingGraph::new();
1367 let file = PathBuf::from("test.rs");
1368 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
1369
1370 let main_id = helper.add_function("main", None, false, false);
1371 let helper_id = helper.add_function("helper", None, false, false);
1372
1373 helper.add_call_edge(main_id, helper_id);
1374
1375 assert_eq!(helper.stats().edges_staged, 1);
1376 let edge_kind = staging.operations().iter().find_map(|op| {
1377 if let StagingOp::AddEdge { kind, .. } = op {
1378 Some(kind)
1379 } else {
1380 None
1381 }
1382 });
1383 match edge_kind {
1384 Some(EdgeKind::Calls {
1385 argument_count,
1386 is_async,
1387 }) => {
1388 assert_eq!(*argument_count, 255);
1389 assert!(!*is_async);
1390 }
1391 _ => panic!("Expected Calls edge"),
1392 }
1393 }
1394
1395 #[test]
1396 fn test_helper_multiple_node_kinds() {
1397 let mut staging = StagingGraph::new();
1398 let file = PathBuf::from("test.py");
1399 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Python);
1400
1401 let _class_id = helper.add_class("MyClass", None);
1402 let _method_id = helper.add_method("MyClass.my_method", None, false, false);
1403 let _func_id = helper.add_function("standalone_func", None, true, false);
1404
1405 assert_eq!(helper.stats().nodes_created, 3);
1406 }
1407
1408 #[test]
1409 fn test_helper_canonicalizes_language_native_qualified_names() {
1410 let mut staging = StagingGraph::new();
1411 let file = PathBuf::from("test.py");
1412 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Python);
1413
1414 let _method_id = helper.add_method("pkg.module.run", None, false, false);
1415
1416 let add_node_op = staging
1417 .operations()
1418 .iter()
1419 .find(|op| matches!(op, StagingOp::AddNode { .. }))
1420 .expect("Expected AddNode operation");
1421
1422 if let StagingOp::AddNode { entry, .. } = add_node_op {
1423 assert_eq!(staging.resolve_local_string(entry.name), Some("run"));
1424 assert_eq!(
1425 staging.resolve_node_name(entry),
1426 Some("pkg::module::run"),
1427 "expected GraphBuildHelper to canonicalize Python dotted qualified names"
1428 );
1429 }
1430 }
1431
1432 #[test]
1433 fn test_helper_preserves_path_qualified_names() {
1434 let mut staging = StagingGraph::new();
1435 let file = PathBuf::from("test.js");
1436 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::JavaScript);
1437
1438 let _func_id = helper.add_function("frontend/api.js::fetchUsers", None, false, false);
1439
1440 let add_node_op = staging
1441 .operations()
1442 .iter()
1443 .find(|op| matches!(op, StagingOp::AddNode { .. }))
1444 .expect("Expected AddNode operation");
1445
1446 if let StagingOp::AddNode { entry, .. } = add_node_op {
1447 assert_eq!(
1448 staging.resolve_local_string(entry.name),
1449 Some("frontend/api.js::fetchUsers")
1450 );
1451 assert_eq!(
1452 staging.resolve_node_name(entry),
1453 Some("frontend/api.js::fetchUsers"),
1454 "expected path-qualified names to remain unchanged"
1455 );
1456 }
1457 }
1458
1459 #[test]
1460 fn test_helper_verbatim_import_preserves_resource_name() {
1461 let mut staging = StagingGraph::new();
1462 let file = PathBuf::from("index.html");
1463 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Html);
1464
1465 let _import_id = helper.add_verbatim_import("styles.css", None);
1466
1467 let add_node_op = staging
1468 .operations()
1469 .iter()
1470 .find(|op| matches!(op, StagingOp::AddNode { .. }))
1471 .expect("Expected AddNode operation");
1472
1473 if let StagingOp::AddNode { entry, .. } = add_node_op {
1474 assert_eq!(staging.resolve_local_string(entry.name), Some("styles.css"));
1475 assert_eq!(entry.qualified_name, None);
1476 assert_eq!(
1477 staging.resolve_node_name(entry),
1478 Some("styles.css"),
1479 "expected verbatim resource imports to preserve their literal identity"
1480 );
1481 }
1482 }
1483
1484 #[test]
1485 fn test_helper_verbatim_variable_preserves_resource_name() {
1486 let mut staging = StagingGraph::new();
1487 let file = PathBuf::from("index.html");
1488 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Html);
1489
1490 let _variable_id = helper.add_verbatim_variable("/assets/logo.icon.png", None);
1491
1492 let add_node_op = staging
1493 .operations()
1494 .iter()
1495 .find(|op| matches!(op, StagingOp::AddNode { .. }))
1496 .expect("Expected AddNode operation");
1497
1498 if let StagingOp::AddNode { entry, .. } = add_node_op {
1499 assert_eq!(
1500 staging.resolve_local_string(entry.name),
1501 Some("/assets/logo.icon.png")
1502 );
1503 assert_eq!(entry.qualified_name, None);
1504 assert_eq!(
1505 staging.resolve_node_name(entry),
1506 Some("/assets/logo.icon.png"),
1507 "expected verbatim resource variables to preserve their literal identity"
1508 );
1509 }
1510 }
1511
1512 #[test]
1513 fn test_helper_ensure_function() {
1514 let mut staging = StagingGraph::new();
1515 let file = PathBuf::from("test.rs");
1516 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
1517
1518 let id1 = helper.ensure_function("foo", None, false, false);
1519 let id2 = helper.ensure_function("foo", None, true, false); assert_eq!(id1, id2, "ensure_function should be idempotent by name");
1522 }
1523
1524 #[test]
1525 fn test_helper_with_span() {
1526 let mut staging = StagingGraph::new();
1527 let file = PathBuf::from("test.rs");
1528 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
1529
1530 let span = Span {
1531 start: Position {
1532 line: 10,
1533 column: 0,
1534 },
1535 end: Position {
1536 line: 15,
1537 column: 1,
1538 },
1539 };
1540
1541 let node_id = helper.add_function("main", Some(span), false, false);
1542 assert!(!node_id.is_invalid());
1543 }
1544
1545 #[test]
1546 fn test_helper_add_call_edge_full() {
1547 let mut staging = StagingGraph::new();
1548 let file = PathBuf::from("test.rs");
1549 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
1550
1551 let caller_id = helper.add_function("caller", None, false, false);
1552 let callee_id = helper.add_function("callee", None, false, false);
1553
1554 helper.add_call_edge_full(caller_id, callee_id, 3, true);
1556
1557 assert_eq!(helper.stats().edges_staged, 1);
1558
1559 let edges = staging.operations();
1561 let call_edge = edges.iter().find(|op| {
1562 matches!(
1563 op,
1564 StagingOp::AddEdge {
1565 kind: EdgeKind::Calls { .. },
1566 ..
1567 }
1568 )
1569 });
1570
1571 assert!(call_edge.is_some());
1572 if let StagingOp::AddEdge {
1573 kind:
1574 EdgeKind::Calls {
1575 argument_count,
1576 is_async,
1577 },
1578 ..
1579 } = call_edge.unwrap()
1580 {
1581 assert_eq!(*argument_count, 3);
1582 assert!(*is_async);
1583 }
1584 }
1585
1586 #[test]
1587 fn test_helper_add_import_edge_full() {
1588 let mut staging = StagingGraph::new();
1589 let file = PathBuf::from("test.js");
1590 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::JavaScript);
1591
1592 let module_id = helper.add_module("app", None);
1593 let imported_id = helper.add_function("utils", None, false, false);
1594
1595 helper.add_import_edge_full(module_id, imported_id, Some("helpers"), false);
1597
1598 assert_eq!(helper.stats().edges_staged, 1);
1599
1600 let edges = staging.operations();
1602 let import_edge = edges.iter().find(|op| {
1603 matches!(
1604 op,
1605 StagingOp::AddEdge {
1606 kind: EdgeKind::Imports { .. },
1607 ..
1608 }
1609 )
1610 });
1611
1612 assert!(import_edge.is_some());
1613 if let StagingOp::AddEdge {
1614 kind: EdgeKind::Imports { alias, is_wildcard },
1615 ..
1616 } = import_edge.unwrap()
1617 {
1618 assert!(alias.is_some(), "Alias should be present");
1619 assert!(!*is_wildcard);
1620 }
1621 }
1622
1623 #[test]
1624 fn test_helper_add_import_edge_wildcard() {
1625 let mut staging = StagingGraph::new();
1626 let file = PathBuf::from("test.js");
1627 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::JavaScript);
1628
1629 let module_id = helper.add_module("app", None);
1630 let imported_id = helper.add_module("lodash", None);
1631
1632 helper.add_import_edge_full(module_id, imported_id, None, true);
1634
1635 let edges = staging.operations();
1636 let import_edge = edges.iter().find(|op| {
1637 matches!(
1638 op,
1639 StagingOp::AddEdge {
1640 kind: EdgeKind::Imports { .. },
1641 ..
1642 }
1643 )
1644 });
1645
1646 if let StagingOp::AddEdge {
1647 kind: EdgeKind::Imports { alias, is_wildcard },
1648 ..
1649 } = import_edge.unwrap()
1650 {
1651 assert!(alias.is_none());
1652 assert!(*is_wildcard);
1653 }
1654 }
1655
1656 #[test]
1657 fn test_helper_add_export_edge_full() {
1658 let mut staging = StagingGraph::new();
1659 let file = PathBuf::from("test.js");
1660 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::JavaScript);
1661
1662 let module_id = helper.add_module("app", None);
1663 let component_id = helper.add_class("MyComponent", None);
1664
1665 helper.add_export_edge_full(module_id, component_id, ExportKind::Default, None);
1667
1668 assert_eq!(helper.stats().edges_staged, 1);
1669
1670 let edges = staging.operations();
1671 let export_edge = edges.iter().find(|op| {
1672 matches!(
1673 op,
1674 StagingOp::AddEdge {
1675 kind: EdgeKind::Exports { .. },
1676 ..
1677 }
1678 )
1679 });
1680
1681 assert!(export_edge.is_some());
1682 if let StagingOp::AddEdge {
1683 kind: EdgeKind::Exports { kind, alias },
1684 ..
1685 } = export_edge.unwrap()
1686 {
1687 assert_eq!(*kind, ExportKind::Default);
1688 assert!(alias.is_none());
1689 }
1690 }
1691
1692 #[test]
1693 fn test_helper_add_export_edge_with_alias() {
1694 let mut staging = StagingGraph::new();
1695 let file = PathBuf::from("test.js");
1696 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::JavaScript);
1697
1698 let module_id = helper.add_module("app", None);
1699 let helper_fn_id = helper.add_function("internalHelper", None, false, false);
1700
1701 helper.add_export_edge_full(module_id, helper_fn_id, ExportKind::Direct, Some("helper"));
1703
1704 let edges = staging.operations();
1705 let export_edge = edges.iter().find(|op| {
1706 matches!(
1707 op,
1708 StagingOp::AddEdge {
1709 kind: EdgeKind::Exports { .. },
1710 ..
1711 }
1712 )
1713 });
1714
1715 if let StagingOp::AddEdge {
1716 kind: EdgeKind::Exports { kind, alias },
1717 ..
1718 } = export_edge.unwrap()
1719 {
1720 assert_eq!(*kind, ExportKind::Direct);
1721 assert!(alias.is_some(), "Alias should be present");
1722 }
1723 }
1724
1725 #[test]
1726 fn test_helper_add_export_edge_reexport() {
1727 let mut staging = StagingGraph::new();
1728 let file = PathBuf::from("index.js");
1729 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::JavaScript);
1730
1731 let module_id = helper.add_module("index", None);
1732 let utils_id = helper.add_module("utils", None);
1733
1734 helper.add_export_edge_full(module_id, utils_id, ExportKind::Namespace, Some("utils"));
1736
1737 let edges = staging.operations();
1738 let export_edge = edges.iter().find(|op| {
1739 matches!(
1740 op,
1741 StagingOp::AddEdge {
1742 kind: EdgeKind::Exports { .. },
1743 ..
1744 }
1745 )
1746 });
1747
1748 if let StagingOp::AddEdge {
1749 kind: EdgeKind::Exports { kind, alias },
1750 ..
1751 } = export_edge.unwrap()
1752 {
1753 assert_eq!(*kind, ExportKind::Namespace);
1754 assert!(alias.is_some());
1755 }
1756 }
1757
1758 #[test]
1759 fn test_helper_add_call_edge_full_with_span() {
1760 let mut staging = StagingGraph::new();
1761 let file = PathBuf::from("test.rs");
1762 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
1763
1764 let caller_id = helper.add_function("caller", None, false, false);
1765 let callee_id = helper.add_function("callee", None, false, false);
1766
1767 let span = Span {
1768 start: Position { line: 5, column: 4 },
1769 end: Position {
1770 line: 5,
1771 column: 20,
1772 },
1773 };
1774
1775 helper.add_call_edge_full_with_span(caller_id, callee_id, 2, false, vec![span]);
1776
1777 let edges = staging.operations();
1778 let call_edge = edges.iter().find(|op| {
1779 matches!(
1780 op,
1781 StagingOp::AddEdge {
1782 kind: EdgeKind::Calls { .. },
1783 ..
1784 }
1785 )
1786 });
1787
1788 if let StagingOp::AddEdge {
1789 kind:
1790 EdgeKind::Calls {
1791 argument_count,
1792 is_async,
1793 },
1794 spans: edge_spans,
1795 ..
1796 } = call_edge.unwrap()
1797 {
1798 assert_eq!(*argument_count, 2);
1799 assert!(!*is_async);
1800 assert!(!edge_spans.is_empty());
1801 }
1802 }
1803
1804 #[test]
1805 fn test_helper_add_function_with_async_attribute() {
1806 let mut staging = StagingGraph::new();
1807 let file = PathBuf::from("test.kt");
1808 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Kotlin);
1809
1810 let _func_id = helper.add_function("fetchData", None, true, false);
1812
1813 let ops = staging.operations();
1815 let add_node_op = ops
1816 .iter()
1817 .find(|op| matches!(op, StagingOp::AddNode { .. }));
1818
1819 assert!(add_node_op.is_some(), "Expected AddNode operation");
1820 if let StagingOp::AddNode { entry, .. } = add_node_op.unwrap() {
1821 assert!(
1822 entry.is_async,
1823 "Expected is_async=true for suspend function, got is_async=false"
1824 );
1825 }
1826 }
1827
1828 #[test]
1829 fn test_helper_add_method_with_static_attribute() {
1830 let mut staging = StagingGraph::new();
1831 let file = PathBuf::from("test.java");
1832 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Java);
1833
1834 let _method_id = helper.add_method("MyClass.staticMethod", None, false, true);
1836
1837 let ops = staging.operations();
1839 let add_node_op = ops
1840 .iter()
1841 .find(|op| matches!(op, StagingOp::AddNode { .. }));
1842
1843 assert!(add_node_op.is_some(), "Expected AddNode operation");
1844 if let StagingOp::AddNode { entry, .. } = add_node_op.unwrap() {
1845 assert!(
1846 entry.is_static,
1847 "Expected is_static=true for static method, got is_static=false"
1848 );
1849 }
1850 }
1851
1852 #[test]
1853 fn test_helper_add_function_without_attributes() {
1854 let mut staging = StagingGraph::new();
1855 let file = PathBuf::from("test.rs");
1856 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
1857
1858 let _func_id = helper.add_function("regular_function", None, false, false);
1860
1861 let ops = staging.operations();
1863 let add_node_op = ops
1864 .iter()
1865 .find(|op| matches!(op, StagingOp::AddNode { .. }));
1866
1867 assert!(add_node_op.is_some(), "Expected AddNode operation");
1868 if let StagingOp::AddNode { entry, .. } = add_node_op.unwrap() {
1869 assert!(
1870 !entry.is_async,
1871 "Expected is_async=false for regular function"
1872 );
1873 assert!(
1874 !entry.is_static,
1875 "Expected is_static=false for regular function"
1876 );
1877 }
1878 }
1879
1880 #[test]
1881 fn test_helper_add_method_with_both_attributes() {
1882 let mut staging = StagingGraph::new();
1883 let file = PathBuf::from("test.kt");
1884 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Kotlin);
1885
1886 let _method_id = helper.add_method("Service.asyncStaticMethod", None, true, true);
1888
1889 let ops = staging.operations();
1891 let add_node_op = ops
1892 .iter()
1893 .find(|op| matches!(op, StagingOp::AddNode { .. }));
1894
1895 assert!(add_node_op.is_some(), "Expected AddNode operation");
1896 if let StagingOp::AddNode { entry, .. } = add_node_op.unwrap() {
1897 assert!(entry.is_async, "Expected is_async=true for async method");
1898 assert!(entry.is_static, "Expected is_static=true for static method");
1899 }
1900 }
1901
1902 #[test]
1903 fn test_helper_add_function_with_unsafe_attribute() {
1904 let mut staging = StagingGraph::new();
1905 let file = PathBuf::from("test.rs");
1906 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
1907
1908 let _func_id = helper.add_function("unsafe_function", None, false, true);
1910
1911 let ops = staging.operations();
1913 let add_node_op = ops
1914 .iter()
1915 .find(|op| matches!(op, StagingOp::AddNode { .. }));
1916
1917 assert!(add_node_op.is_some(), "Expected AddNode operation");
1918 if let StagingOp::AddNode { entry, .. } = add_node_op.unwrap() {
1919 assert!(
1920 entry.is_unsafe,
1921 "Expected is_unsafe=true for unsafe function, got is_unsafe={}",
1922 entry.is_unsafe
1923 );
1924 }
1925 }
1926}