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]
137 pub fn lookup_node(&self, name: &str, kind: NodeKind) -> Option<NodeId> {
138 self.node_cache.get(&(name.to_string(), kind)).copied()
139 }
140
141 #[must_use]
143 pub fn file_path(&self) -> &str {
144 &self.file_path
145 }
146
147 pub fn attach_body_hashes(&mut self, content: &[u8]) {
155 self.staging.attach_body_hashes(content);
156 }
157
158 pub fn intern(&mut self, s: &str) -> StringId {
164 if let Some(&id) = self.string_cache.get(s) {
165 return id;
166 }
167
168 let id = StringId::new_local(self.next_string_id);
169 self.next_string_id += 1;
170 self.string_cache.insert(s.to_string(), id);
171 self.staging.intern_string(id, s.to_string());
173 id
174 }
175
176 #[must_use]
178 pub fn has_node(&self, qualified_name: &str) -> bool {
179 self.node_cache
180 .keys()
181 .any(|(name, _)| name == qualified_name)
182 }
183
184 #[must_use]
186 pub fn get_node(&self, qualified_name: &str) -> Option<NodeId> {
187 self.node_cache
188 .iter()
189 .find_map(|((name, _), id)| (name == qualified_name).then_some(*id))
190 }
191
192 #[must_use]
194 pub fn has_node_with_kind(&self, qualified_name: &str, kind: NodeKind) -> bool {
195 self.node_cache
196 .contains_key(&(qualified_name.to_string(), kind))
197 }
198
199 #[must_use]
201 pub fn get_node_with_kind(&self, qualified_name: &str, kind: NodeKind) -> Option<NodeId> {
202 self.node_cache
203 .get(&(qualified_name.to_string(), kind))
204 .copied()
205 }
206
207 pub fn add_function(
211 &mut self,
212 qualified_name: &str,
213 span: Option<Span>,
214 is_async: bool,
215 is_unsafe: bool,
216 ) -> NodeId {
217 self.add_node_internal(
218 qualified_name,
219 span,
220 NodeKind::Function,
221 &[("async", is_async), ("unsafe", is_unsafe)],
222 None,
223 None,
224 )
225 }
226
227 pub fn add_function_with_visibility(
231 &mut self,
232 qualified_name: &str,
233 span: Option<Span>,
234 is_async: bool,
235 is_unsafe: bool,
236 visibility: Option<&str>,
237 ) -> NodeId {
238 self.add_node_internal(
239 qualified_name,
240 span,
241 NodeKind::Function,
242 &[("async", is_async), ("unsafe", is_unsafe)],
243 visibility,
244 None,
245 )
246 }
247
248 pub fn add_function_with_signature(
253 &mut self,
254 qualified_name: &str,
255 span: Option<Span>,
256 is_async: bool,
257 is_unsafe: bool,
258 visibility: Option<&str>,
259 signature: Option<&str>,
260 ) -> NodeId {
261 self.add_node_internal(
262 qualified_name,
263 span,
264 NodeKind::Function,
265 &[("async", is_async), ("unsafe", is_unsafe)],
266 visibility,
267 signature,
268 )
269 }
270
271 pub fn add_method(
273 &mut self,
274 qualified_name: &str,
275 span: Option<Span>,
276 is_async: bool,
277 is_static: bool,
278 ) -> NodeId {
279 self.add_node_internal(
280 qualified_name,
281 span,
282 NodeKind::Method,
283 &[("async", is_async), ("static", is_static)],
284 None,
285 None,
286 )
287 }
288
289 pub fn add_method_with_visibility(
291 &mut self,
292 qualified_name: &str,
293 span: Option<Span>,
294 is_async: bool,
295 is_static: bool,
296 visibility: Option<&str>,
297 ) -> NodeId {
298 self.add_node_internal(
299 qualified_name,
300 span,
301 NodeKind::Method,
302 &[("async", is_async), ("static", is_static)],
303 visibility,
304 None,
305 )
306 }
307
308 pub fn add_method_with_signature(
313 &mut self,
314 qualified_name: &str,
315 span: Option<Span>,
316 is_async: bool,
317 is_static: bool,
318 visibility: Option<&str>,
319 signature: Option<&str>,
320 ) -> NodeId {
321 self.add_node_internal(
322 qualified_name,
323 span,
324 NodeKind::Method,
325 &[("async", is_async), ("static", is_static)],
326 visibility,
327 signature,
328 )
329 }
330
331 pub fn add_class(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
333 self.add_node_internal(qualified_name, span, NodeKind::Class, &[], None, None)
334 }
335
336 pub fn add_class_with_visibility(
338 &mut self,
339 qualified_name: &str,
340 span: Option<Span>,
341 visibility: Option<&str>,
342 ) -> NodeId {
343 self.add_node_internal(qualified_name, span, NodeKind::Class, &[], visibility, None)
344 }
345
346 pub fn add_struct(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
348 self.add_node_internal(qualified_name, span, NodeKind::Struct, &[], None, None)
349 }
350
351 pub fn add_struct_with_visibility(
353 &mut self,
354 qualified_name: &str,
355 span: Option<Span>,
356 visibility: Option<&str>,
357 ) -> NodeId {
358 self.add_node_internal(
359 qualified_name,
360 span,
361 NodeKind::Struct,
362 &[],
363 visibility,
364 None,
365 )
366 }
367
368 pub fn add_module(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
370 self.add_node_internal(qualified_name, span, NodeKind::Module, &[], None, None)
371 }
372
373 pub fn add_resource(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
375 self.add_node_internal(qualified_name, span, NodeKind::Resource, &[], None, None)
376 }
377
378 pub fn add_endpoint(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
386 self.add_node_internal(qualified_name, span, NodeKind::Endpoint, &[], None, None)
387 }
388
389 pub fn add_import(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
391 self.add_node_internal(qualified_name, span, NodeKind::Import, &[], None, None)
392 }
393
394 pub fn add_verbatim_import(&mut self, name: &str, span: Option<Span>) -> NodeId {
400 self.add_node_verbatim(name, span, NodeKind::Import, &[], None, None)
401 }
402
403 pub fn add_variable(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
405 self.add_node_internal(qualified_name, span, NodeKind::Variable, &[], None, None)
406 }
407
408 pub fn add_verbatim_variable(&mut self, name: &str, span: Option<Span>) -> NodeId {
413 self.add_node_verbatim(name, span, NodeKind::Variable, &[], None, None)
414 }
415
416 pub fn add_constant(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
418 self.add_node_internal(qualified_name, span, NodeKind::Constant, &[], None, None)
419 }
420
421 pub fn add_constant_with_visibility(
423 &mut self,
424 qualified_name: &str,
425 span: Option<Span>,
426 visibility: Option<&str>,
427 ) -> NodeId {
428 self.add_node_internal(
429 qualified_name,
430 span,
431 NodeKind::Constant,
432 &[],
433 visibility,
434 None,
435 )
436 }
437
438 pub fn add_constant_with_static_and_visibility(
440 &mut self,
441 qualified_name: &str,
442 span: Option<Span>,
443 is_static: bool,
444 visibility: Option<&str>,
445 ) -> NodeId {
446 let attrs: &[(&str, bool)] = if is_static { &[("static", true)] } else { &[] };
447 self.add_node_internal(
448 qualified_name,
449 span,
450 NodeKind::Constant,
451 attrs,
452 visibility,
453 None,
454 )
455 }
456
457 pub fn add_property_with_static_and_visibility(
459 &mut self,
460 qualified_name: &str,
461 span: Option<Span>,
462 is_static: bool,
463 visibility: Option<&str>,
464 ) -> NodeId {
465 let attrs: &[(&str, bool)] = if is_static { &[("static", true)] } else { &[] };
466 self.add_node_internal(
467 qualified_name,
468 span,
469 NodeKind::Property,
470 attrs,
471 visibility,
472 None,
473 )
474 }
475
476 pub fn add_enum(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
478 self.add_node_internal(qualified_name, span, NodeKind::Enum, &[], None, None)
479 }
480
481 pub fn add_enum_with_visibility(
483 &mut self,
484 qualified_name: &str,
485 span: Option<Span>,
486 visibility: Option<&str>,
487 ) -> NodeId {
488 self.add_node_internal(qualified_name, span, NodeKind::Enum, &[], visibility, None)
489 }
490
491 pub fn add_interface(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
493 self.add_node_internal(qualified_name, span, NodeKind::Interface, &[], None, None)
494 }
495
496 pub fn add_interface_with_visibility(
498 &mut self,
499 qualified_name: &str,
500 span: Option<Span>,
501 visibility: Option<&str>,
502 ) -> NodeId {
503 self.add_node_internal(
504 qualified_name,
505 span,
506 NodeKind::Interface,
507 &[],
508 visibility,
509 None,
510 )
511 }
512
513 pub fn add_type(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
515 self.add_node_internal(qualified_name, span, NodeKind::Type, &[], None, None)
516 }
517
518 pub fn add_type_with_visibility(
520 &mut self,
521 qualified_name: &str,
522 span: Option<Span>,
523 visibility: Option<&str>,
524 ) -> NodeId {
525 self.add_node_internal(qualified_name, span, NodeKind::Type, &[], visibility, None)
526 }
527
528 pub fn add_lifetime(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
530 self.add_node_internal(qualified_name, span, NodeKind::Lifetime, &[], None, None)
531 }
532
533 pub fn add_lifetime_constraint_edge(
535 &mut self,
536 source: NodeId,
537 target: NodeId,
538 constraint_kind: LifetimeConstraintKind,
539 ) {
540 self.staging.add_edge(
541 source,
542 target,
543 EdgeKind::LifetimeConstraint { constraint_kind },
544 self.file_id,
545 );
546 }
547
548 pub fn add_trait_method_binding_edge(
553 &mut self,
554 caller: NodeId,
555 callee: NodeId,
556 trait_name: &str,
557 impl_type: &str,
558 is_ambiguous: bool,
559 ) {
560 let trait_name_id = self.intern(trait_name);
561 let impl_type_id = self.intern(impl_type);
562 self.staging.add_edge(
563 caller,
564 callee,
565 EdgeKind::TraitMethodBinding {
566 trait_name: trait_name_id,
567 impl_type: impl_type_id,
568 is_ambiguous,
569 },
570 self.file_id,
571 );
572 }
573
574 pub fn add_macro_expansion_edge(
600 &mut self,
601 invocation: NodeId,
602 expansion: NodeId,
603 expansion_kind: MacroExpansionKind,
604 is_verified: bool,
605 ) {
606 self.staging.add_edge(
607 invocation,
608 expansion,
609 EdgeKind::MacroExpansion {
610 expansion_kind,
611 is_verified,
612 },
613 self.file_id,
614 );
615 }
616
617 pub fn add_node(&mut self, qualified_name: &str, span: Option<Span>, kind: NodeKind) -> NodeId {
619 self.add_node_internal(qualified_name, span, kind, &[], None, None)
620 }
621
622 pub fn add_node_with_visibility(
624 &mut self,
625 qualified_name: &str,
626 span: Option<Span>,
627 kind: NodeKind,
628 visibility: Option<&str>,
629 ) -> NodeId {
630 self.add_node_internal(qualified_name, span, kind, &[], visibility, None)
631 }
632
633 fn add_node_internal(
643 &mut self,
644 qualified_name: &str,
645 span: Option<Span>,
646 kind: NodeKind,
647 attributes: &[(&str, bool)],
648 visibility: Option<&str>,
649 signature: Option<&str>,
650 ) -> NodeId {
651 let canonical_qualified_name =
652 canonicalize_graph_qualified_name(self.language, qualified_name);
653 let semantic_name = semantic_name_for_node_input(qualified_name, &canonical_qualified_name);
654 let mut is_async = false;
655 let mut is_static = false;
656 let mut is_unsafe = false;
657 for &(key, value) in attributes {
658 match key {
659 "async" => is_async |= value,
660 "static" => is_static |= value,
661 "unsafe" => is_unsafe |= value,
662 _ => {}
663 }
664 }
665
666 if let Some(&id) = self
668 .node_cache
669 .get(&(canonical_qualified_name.clone(), kind))
670 {
671 let visibility_id = visibility.map(|vis| self.intern(vis));
672 let signature_id = signature.map(|sig| self.intern(sig));
673 self.staging.update_node_entry(
674 id,
675 span,
676 is_async,
677 is_static,
678 is_unsafe,
679 visibility_id,
680 signature_id,
681 );
682 return id;
683 }
684
685 let name_id = self.intern(&semantic_name);
686
687 let mut entry = NodeEntry::new(kind, name_id, self.file_id);
689 if semantic_name != canonical_qualified_name {
690 let qualified_name_id = self.intern(&canonical_qualified_name);
691 entry = entry.with_qualified_name(qualified_name_id);
692 }
693
694 if let Some(s) = span {
696 let start_line = u32::try_from(s.start.line.saturating_add(1)).unwrap_or(u32::MAX);
697 let start_column = u32::try_from(s.start.column).unwrap_or(u32::MAX);
698 let end_line = u32::try_from(s.end.line.saturating_add(1)).unwrap_or(u32::MAX);
699 let end_column = u32::try_from(s.end.column).unwrap_or(u32::MAX);
700 entry = entry.with_location(start_line, start_column, end_line, end_column);
701 }
702
703 if is_async {
705 entry = entry.with_async(true);
706 }
707 if is_static {
708 entry = entry.with_static(true);
709 }
710 if is_unsafe {
711 entry = entry.with_unsafe(true);
712 }
713
714 if let Some(vis) = visibility {
716 let vis_id = self.intern(vis);
717 entry = entry.with_visibility(vis_id);
718 }
719
720 if let Some(sig) = signature {
722 let sig_id = self.intern(sig);
723 entry = entry.with_signature(sig_id);
724 }
725
726 let node_id = self.staging.add_node(entry);
728
729 self.node_cache
731 .insert((canonical_qualified_name, kind), node_id);
732
733 node_id
734 }
735
736 fn add_node_verbatim(
737 &mut self,
738 name: &str,
739 span: Option<Span>,
740 kind: NodeKind,
741 attributes: &[(&str, bool)],
742 visibility: Option<&str>,
743 signature: Option<&str>,
744 ) -> NodeId {
745 let mut is_async = false;
746 let mut is_static = false;
747 let mut is_unsafe = false;
748 for &(key, value) in attributes {
749 match key {
750 "async" => is_async |= value,
751 "static" => is_static |= value,
752 "unsafe" => is_unsafe |= value,
753 _ => {}
754 }
755 }
756
757 if let Some(&id) = self.node_cache.get(&(name.to_string(), kind)) {
758 let visibility_id = visibility.map(|vis| self.intern(vis));
759 let signature_id = signature.map(|sig| self.intern(sig));
760 self.staging.update_node_entry(
761 id,
762 span,
763 is_async,
764 is_static,
765 is_unsafe,
766 visibility_id,
767 signature_id,
768 );
769 return id;
770 }
771
772 let name_id = self.intern(name);
773 let mut entry = NodeEntry::new(kind, name_id, self.file_id);
774
775 if let Some(s) = span {
776 let start_line = u32::try_from(s.start.line.saturating_add(1)).unwrap_or(u32::MAX);
777 let start_column = u32::try_from(s.start.column).unwrap_or(u32::MAX);
778 let end_line = u32::try_from(s.end.line.saturating_add(1)).unwrap_or(u32::MAX);
779 let end_column = u32::try_from(s.end.column).unwrap_or(u32::MAX);
780 entry = entry.with_location(start_line, start_column, end_line, end_column);
781 }
782
783 if is_async {
784 entry = entry.with_async(true);
785 }
786 if is_static {
787 entry = entry.with_static(true);
788 }
789 if is_unsafe {
790 entry = entry.with_unsafe(true);
791 }
792
793 if let Some(vis) = visibility {
794 let vis_id = self.intern(vis);
795 entry = entry.with_visibility(vis_id);
796 }
797 if let Some(sig) = signature {
798 let sig_id = self.intern(sig);
799 entry = entry.with_signature(sig_id);
800 }
801
802 let node_id = self.staging.add_node(entry);
803 self.node_cache.insert((name.to_string(), kind), node_id);
804 node_id
805 }
806
807 pub fn add_call_edge(&mut self, caller: NodeId, callee: NodeId) {
809 self.add_call_edge_with_span(caller, callee, Vec::new());
810 }
811
812 pub fn add_call_edge_with_span(
822 &mut self,
823 caller: NodeId,
824 callee: NodeId,
825 spans: Vec<crate::graph::node::Span>,
826 ) {
827 self.staging.add_edge_with_spans(
828 caller,
829 callee,
830 EdgeKind::Calls {
831 argument_count: 255,
832 is_async: false,
833 },
834 self.file_id,
835 spans,
836 );
837 }
838
839 pub fn add_call_edge_full(
870 &mut self,
871 caller: NodeId,
872 callee: NodeId,
873 argument_count: u8,
874 is_async: bool,
875 ) {
876 self.staging.add_edge(
877 caller,
878 callee,
879 EdgeKind::Calls {
880 argument_count,
881 is_async,
882 },
883 self.file_id,
884 );
885 }
886
887 pub fn add_call_edge_full_with_span(
892 &mut self,
893 caller: NodeId,
894 callee: NodeId,
895 argument_count: u8,
896 is_async: bool,
897 spans: Vec<crate::graph::node::Span>,
898 ) {
899 self.staging.add_edge_with_spans(
900 caller,
901 callee,
902 EdgeKind::Calls {
903 argument_count,
904 is_async,
905 },
906 self.file_id,
907 spans,
908 );
909 }
910
911 pub fn add_table_read_edge_with_span(
913 &mut self,
914 reader: NodeId,
915 table: NodeId,
916 table_name: &str,
917 schema: Option<&str>,
918 spans: Vec<crate::graph::node::Span>,
919 ) {
920 let table_name_id = self.intern(table_name);
921 let schema_id = schema.map(|s| self.intern(s));
922 self.staging.add_edge_with_spans(
923 reader,
924 table,
925 EdgeKind::TableRead {
926 table_name: table_name_id,
927 schema: schema_id,
928 },
929 self.file_id,
930 spans,
931 );
932 }
933
934 pub fn add_table_write_edge_with_span(
936 &mut self,
937 writer: NodeId,
938 table: NodeId,
939 table_name: &str,
940 schema: Option<&str>,
941 operation: TableWriteOp,
942 spans: Vec<crate::graph::node::Span>,
943 ) {
944 let table_name_id = self.intern(table_name);
945 let schema_id = schema.map(|s| self.intern(s));
946 self.staging.add_edge_with_spans(
947 writer,
948 table,
949 EdgeKind::TableWrite {
950 table_name: table_name_id,
951 schema: schema_id,
952 operation,
953 },
954 self.file_id,
955 spans,
956 );
957 }
958
959 pub fn add_triggered_by_edge_with_span(
963 &mut self,
964 trigger: NodeId,
965 table: NodeId,
966 trigger_name: &str,
967 schema: Option<&str>,
968 spans: Vec<crate::graph::node::Span>,
969 ) {
970 let trigger_name_id = self.intern(trigger_name);
971 let schema_id = schema.map(|s| self.intern(s));
972 self.staging.add_edge_with_spans(
973 trigger,
974 table,
975 EdgeKind::TriggeredBy {
976 trigger_name: trigger_name_id,
977 schema: schema_id,
978 },
979 self.file_id,
980 spans,
981 );
982 }
983
984 pub fn add_import_edge(&mut self, importer: NodeId, imported: NodeId) {
990 self.staging.add_edge(
991 importer,
992 imported,
993 EdgeKind::Imports {
994 alias: None,
995 is_wildcard: false,
996 },
997 self.file_id,
998 );
999 }
1000
1001 pub fn add_import_edge_full(
1033 &mut self,
1034 importer: NodeId,
1035 imported: NodeId,
1036 alias: Option<&str>,
1037 is_wildcard: bool,
1038 ) {
1039 let alias_id = alias.map(|s| self.intern(s));
1040 self.staging.add_edge(
1041 importer,
1042 imported,
1043 EdgeKind::Imports {
1044 alias: alias_id,
1045 is_wildcard,
1046 },
1047 self.file_id,
1048 );
1049 }
1050
1051 pub fn add_export_edge(&mut self, module: NodeId, exported: NodeId) {
1057 self.staging.add_edge(
1058 module,
1059 exported,
1060 EdgeKind::Exports {
1061 kind: ExportKind::Direct,
1062 alias: None,
1063 },
1064 self.file_id,
1065 );
1066 }
1067
1068 pub fn add_export_edge_full(
1110 &mut self,
1111 module: NodeId,
1112 exported: NodeId,
1113 kind: ExportKind,
1114 alias: Option<&str>,
1115 ) {
1116 let alias_id = alias.map(|s| self.intern(s));
1117 self.staging.add_edge(
1118 module,
1119 exported,
1120 EdgeKind::Exports {
1121 kind,
1122 alias: alias_id,
1123 },
1124 self.file_id,
1125 );
1126 }
1127
1128 pub fn add_reference_edge(&mut self, from: NodeId, to: NodeId) {
1130 self.staging
1131 .add_edge(from, to, EdgeKind::References, self.file_id);
1132 }
1133
1134 pub fn add_defines_edge(&mut self, parent: NodeId, child: NodeId) {
1136 self.staging
1137 .add_edge(parent, child, EdgeKind::Defines, self.file_id);
1138 }
1139
1140 pub fn add_typeof_edge(&mut self, source: NodeId, target: NodeId) {
1145 self.add_typeof_edge_with_context(source, target, None, None, None);
1146 }
1147
1148 pub fn add_typeof_edge_with_context(
1187 &mut self,
1188 source: NodeId,
1189 target: NodeId,
1190 context: Option<TypeOfContext>,
1191 index: Option<u16>,
1192 name: Option<&str>,
1193 ) {
1194 let name_id = name.map(|n| self.intern(n));
1195 self.staging.add_edge(
1196 source,
1197 target,
1198 EdgeKind::TypeOf {
1199 context,
1200 index,
1201 name: name_id,
1202 },
1203 self.file_id,
1204 );
1205 }
1206
1207 pub fn add_implements_edge(&mut self, implementor: NodeId, interface: NodeId) {
1209 self.staging
1210 .add_edge(implementor, interface, EdgeKind::Implements, self.file_id);
1211 }
1212
1213 pub fn add_inherits_edge(&mut self, child: NodeId, parent: NodeId) {
1215 self.staging
1216 .add_edge(child, parent, EdgeKind::Inherits, self.file_id);
1217 }
1218
1219 pub fn add_contains_edge(&mut self, parent: NodeId, child: NodeId) {
1221 self.staging
1222 .add_edge(parent, child, EdgeKind::Contains, self.file_id);
1223 }
1224
1225 pub fn add_webassembly_edge(&mut self, caller: NodeId, wasm_target: NodeId) {
1232 self.staging
1233 .add_edge(caller, wasm_target, EdgeKind::WebAssemblyCall, self.file_id);
1234 }
1235
1236 pub fn add_ffi_edge(&mut self, caller: NodeId, ffi_target: NodeId, convention: FfiConvention) {
1244 self.staging.add_edge(
1245 caller,
1246 ffi_target,
1247 EdgeKind::FfiCall { convention },
1248 self.file_id,
1249 );
1250 }
1251
1252 pub fn add_http_request_edge(
1256 &mut self,
1257 caller: NodeId,
1258 target: NodeId,
1259 method: HttpMethod,
1260 url: Option<&str>,
1261 ) {
1262 let url_id = url.map(|value| self.intern(value));
1263 self.staging.add_edge(
1264 caller,
1265 target,
1266 EdgeKind::HttpRequest {
1267 method,
1268 url: url_id,
1269 },
1270 self.file_id,
1271 );
1272 }
1273
1274 pub fn ensure_function(
1286 &mut self,
1287 qualified_name: &str,
1288 span: Option<Span>,
1289 is_async: bool,
1290 is_unsafe: bool,
1291 ) -> NodeId {
1292 let canonical = canonicalize_graph_qualified_name(self.language, qualified_name);
1293 if let Some(&id) = self.node_cache.get(&(canonical, NodeKind::Method)) {
1294 return id;
1295 }
1296 self.add_function(qualified_name, span, is_async, is_unsafe)
1297 }
1298
1299 pub fn ensure_method(
1305 &mut self,
1306 qualified_name: &str,
1307 span: Option<Span>,
1308 is_async: bool,
1309 is_static: bool,
1310 ) -> NodeId {
1311 let canonical = canonicalize_graph_qualified_name(self.language, qualified_name);
1312 if let Some(&id) = self.node_cache.get(&(canonical, NodeKind::Function)) {
1313 return id;
1314 }
1315 self.add_method(qualified_name, span, is_async, is_static)
1316 }
1317
1318 #[must_use]
1320 pub fn stats(&self) -> HelperStats {
1321 let staging_stats = self.staging.stats();
1322 HelperStats {
1323 strings_interned: self.string_cache.len(),
1324 nodes_created: self.node_cache.len(),
1325 nodes_staged: staging_stats.nodes_staged,
1326 edges_staged: staging_stats.edges_staged,
1327 }
1328 }
1329}
1330
1331fn semantic_name_for_node_input(original: &str, canonical: &str) -> String {
1332 if original.contains('/') {
1333 return original.to_string();
1334 }
1335
1336 canonical
1337 .rsplit("::")
1338 .next()
1339 .map_or_else(|| original.to_string(), ToString::to_string)
1340}
1341
1342#[derive(Debug, Clone, Default)]
1344pub struct HelperStats {
1345 pub strings_interned: usize,
1347 pub nodes_created: usize,
1349 pub nodes_staged: usize,
1351 pub edges_staged: usize,
1353}
1354
1355#[cfg(test)]
1356mod tests {
1357 use super::*;
1358 use crate::graph::node::Position;
1359 use crate::graph::unified::build::staging::StagingOp;
1360 use std::path::PathBuf;
1361
1362 #[test]
1363 fn test_helper_add_function() {
1364 let mut staging = StagingGraph::new();
1365 let file = PathBuf::from("test.rs");
1366 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
1367
1368 let node_id = helper.add_function("main", None, false, false);
1369 assert!(!node_id.is_invalid());
1370 assert_eq!(helper.stats().nodes_created, 1);
1371 }
1372
1373 #[test]
1374 fn test_helper_deduplication() {
1375 let mut staging = StagingGraph::new();
1376 let file = PathBuf::from("test.rs");
1377 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
1378
1379 let id1 = helper.add_function("main", None, false, false);
1380 let id2 = helper.add_function("main", None, false, false);
1381
1382 assert_eq!(id1, id2, "Same function should return same NodeId");
1383 assert_eq!(
1384 helper.stats().nodes_created,
1385 1,
1386 "Should only create one node"
1387 );
1388 }
1389
1390 #[test]
1391 fn test_helper_string_interning() {
1392 let mut staging = StagingGraph::new();
1393 let file = PathBuf::from("test.rs");
1394 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
1395
1396 let s1 = helper.intern("hello");
1397 let s2 = helper.intern("world");
1398 let s3 = helper.intern("hello"); assert_ne!(s1, s2, "Different strings should have different IDs");
1401 assert_eq!(s1, s3, "Same string should return same ID");
1402 assert_eq!(helper.stats().strings_interned, 2);
1403 }
1404
1405 #[test]
1406 fn test_helper_add_call_edge() {
1407 let mut staging = StagingGraph::new();
1408 let file = PathBuf::from("test.rs");
1409 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
1410
1411 let main_id = helper.add_function("main", None, false, false);
1412 let helper_id = helper.add_function("helper", None, false, false);
1413
1414 helper.add_call_edge(main_id, helper_id);
1415
1416 assert_eq!(helper.stats().edges_staged, 1);
1417 let edge_kind = staging.operations().iter().find_map(|op| {
1418 if let StagingOp::AddEdge { kind, .. } = op {
1419 Some(kind)
1420 } else {
1421 None
1422 }
1423 });
1424 match edge_kind {
1425 Some(EdgeKind::Calls {
1426 argument_count,
1427 is_async,
1428 }) => {
1429 assert_eq!(*argument_count, 255);
1430 assert!(!*is_async);
1431 }
1432 _ => panic!("Expected Calls edge"),
1433 }
1434 }
1435
1436 #[test]
1437 fn test_helper_multiple_node_kinds() {
1438 let mut staging = StagingGraph::new();
1439 let file = PathBuf::from("test.py");
1440 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Python);
1441
1442 let _class_id = helper.add_class("MyClass", None);
1443 let _method_id = helper.add_method("MyClass.my_method", None, false, false);
1444 let _func_id = helper.add_function("standalone_func", None, true, false);
1445
1446 assert_eq!(helper.stats().nodes_created, 3);
1447 }
1448
1449 #[test]
1450 fn test_helper_canonicalizes_language_native_qualified_names() {
1451 let mut staging = StagingGraph::new();
1452 let file = PathBuf::from("test.py");
1453 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Python);
1454
1455 let _method_id = helper.add_method("pkg.module.run", None, false, false);
1456
1457 let add_node_op = staging
1458 .operations()
1459 .iter()
1460 .find(|op| matches!(op, StagingOp::AddNode { .. }))
1461 .expect("Expected AddNode operation");
1462
1463 if let StagingOp::AddNode { entry, .. } = add_node_op {
1464 assert_eq!(staging.resolve_local_string(entry.name), Some("run"));
1465 assert_eq!(
1466 staging.resolve_node_name(entry),
1467 Some("pkg::module::run"),
1468 "expected GraphBuildHelper to canonicalize Python dotted qualified names"
1469 );
1470 }
1471 }
1472
1473 #[test]
1474 fn test_helper_preserves_path_qualified_names() {
1475 let mut staging = StagingGraph::new();
1476 let file = PathBuf::from("test.js");
1477 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::JavaScript);
1478
1479 let _func_id = helper.add_function("frontend/api.js::fetchUsers", None, false, false);
1480
1481 let add_node_op = staging
1482 .operations()
1483 .iter()
1484 .find(|op| matches!(op, StagingOp::AddNode { .. }))
1485 .expect("Expected AddNode operation");
1486
1487 if let StagingOp::AddNode { entry, .. } = add_node_op {
1488 assert_eq!(
1489 staging.resolve_local_string(entry.name),
1490 Some("frontend/api.js::fetchUsers")
1491 );
1492 assert_eq!(
1493 staging.resolve_node_name(entry),
1494 Some("frontend/api.js::fetchUsers"),
1495 "expected path-qualified names to remain unchanged"
1496 );
1497 }
1498 }
1499
1500 #[test]
1501 fn test_helper_verbatim_import_preserves_resource_name() {
1502 let mut staging = StagingGraph::new();
1503 let file = PathBuf::from("index.html");
1504 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Html);
1505
1506 let _import_id = helper.add_verbatim_import("styles.css", None);
1507
1508 let add_node_op = staging
1509 .operations()
1510 .iter()
1511 .find(|op| matches!(op, StagingOp::AddNode { .. }))
1512 .expect("Expected AddNode operation");
1513
1514 if let StagingOp::AddNode { entry, .. } = add_node_op {
1515 assert_eq!(staging.resolve_local_string(entry.name), Some("styles.css"));
1516 assert_eq!(entry.qualified_name, None);
1517 assert_eq!(
1518 staging.resolve_node_name(entry),
1519 Some("styles.css"),
1520 "expected verbatim resource imports to preserve their literal identity"
1521 );
1522 }
1523 }
1524
1525 #[test]
1526 fn test_helper_verbatim_variable_preserves_resource_name() {
1527 let mut staging = StagingGraph::new();
1528 let file = PathBuf::from("index.html");
1529 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Html);
1530
1531 let _variable_id = helper.add_verbatim_variable("/assets/logo.icon.png", None);
1532
1533 let add_node_op = staging
1534 .operations()
1535 .iter()
1536 .find(|op| matches!(op, StagingOp::AddNode { .. }))
1537 .expect("Expected AddNode operation");
1538
1539 if let StagingOp::AddNode { entry, .. } = add_node_op {
1540 assert_eq!(
1541 staging.resolve_local_string(entry.name),
1542 Some("/assets/logo.icon.png")
1543 );
1544 assert_eq!(entry.qualified_name, None);
1545 assert_eq!(
1546 staging.resolve_node_name(entry),
1547 Some("/assets/logo.icon.png"),
1548 "expected verbatim resource variables to preserve their literal identity"
1549 );
1550 }
1551 }
1552
1553 #[test]
1554 fn test_helper_ensure_function() {
1555 let mut staging = StagingGraph::new();
1556 let file = PathBuf::from("test.rs");
1557 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
1558
1559 let id1 = helper.ensure_function("foo", None, false, false);
1560 let id2 = helper.ensure_function("foo", None, true, false); assert_eq!(id1, id2, "ensure_function should be idempotent by name");
1563 }
1564
1565 #[test]
1566 fn test_helper_with_span() {
1567 let mut staging = StagingGraph::new();
1568 let file = PathBuf::from("test.rs");
1569 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
1570
1571 let span = Span {
1572 start: Position {
1573 line: 10,
1574 column: 0,
1575 },
1576 end: Position {
1577 line: 15,
1578 column: 1,
1579 },
1580 };
1581
1582 let node_id = helper.add_function("main", Some(span), false, false);
1583 assert!(!node_id.is_invalid());
1584 }
1585
1586 #[test]
1587 fn test_helper_add_call_edge_full() {
1588 let mut staging = StagingGraph::new();
1589 let file = PathBuf::from("test.rs");
1590 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
1591
1592 let caller_id = helper.add_function("caller", None, false, false);
1593 let callee_id = helper.add_function("callee", None, false, false);
1594
1595 helper.add_call_edge_full(caller_id, callee_id, 3, true);
1597
1598 assert_eq!(helper.stats().edges_staged, 1);
1599
1600 let edges = staging.operations();
1602 let call_edge = edges.iter().find(|op| {
1603 matches!(
1604 op,
1605 StagingOp::AddEdge {
1606 kind: EdgeKind::Calls { .. },
1607 ..
1608 }
1609 )
1610 });
1611
1612 assert!(call_edge.is_some());
1613 if let StagingOp::AddEdge {
1614 kind:
1615 EdgeKind::Calls {
1616 argument_count,
1617 is_async,
1618 },
1619 ..
1620 } = call_edge.unwrap()
1621 {
1622 assert_eq!(*argument_count, 3);
1623 assert!(*is_async);
1624 }
1625 }
1626
1627 #[test]
1628 fn test_helper_add_import_edge_full() {
1629 let mut staging = StagingGraph::new();
1630 let file = PathBuf::from("test.js");
1631 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::JavaScript);
1632
1633 let module_id = helper.add_module("app", None);
1634 let imported_id = helper.add_function("utils", None, false, false);
1635
1636 helper.add_import_edge_full(module_id, imported_id, Some("helpers"), false);
1638
1639 assert_eq!(helper.stats().edges_staged, 1);
1640
1641 let edges = staging.operations();
1643 let import_edge = edges.iter().find(|op| {
1644 matches!(
1645 op,
1646 StagingOp::AddEdge {
1647 kind: EdgeKind::Imports { .. },
1648 ..
1649 }
1650 )
1651 });
1652
1653 assert!(import_edge.is_some());
1654 if let StagingOp::AddEdge {
1655 kind: EdgeKind::Imports { alias, is_wildcard },
1656 ..
1657 } = import_edge.unwrap()
1658 {
1659 assert!(alias.is_some(), "Alias should be present");
1660 assert!(!*is_wildcard);
1661 }
1662 }
1663
1664 #[test]
1665 fn test_helper_add_import_edge_wildcard() {
1666 let mut staging = StagingGraph::new();
1667 let file = PathBuf::from("test.js");
1668 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::JavaScript);
1669
1670 let module_id = helper.add_module("app", None);
1671 let imported_id = helper.add_module("lodash", None);
1672
1673 helper.add_import_edge_full(module_id, imported_id, None, true);
1675
1676 let edges = staging.operations();
1677 let import_edge = edges.iter().find(|op| {
1678 matches!(
1679 op,
1680 StagingOp::AddEdge {
1681 kind: EdgeKind::Imports { .. },
1682 ..
1683 }
1684 )
1685 });
1686
1687 if let StagingOp::AddEdge {
1688 kind: EdgeKind::Imports { alias, is_wildcard },
1689 ..
1690 } = import_edge.unwrap()
1691 {
1692 assert!(alias.is_none());
1693 assert!(*is_wildcard);
1694 }
1695 }
1696
1697 #[test]
1698 fn test_helper_add_export_edge_full() {
1699 let mut staging = StagingGraph::new();
1700 let file = PathBuf::from("test.js");
1701 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::JavaScript);
1702
1703 let module_id = helper.add_module("app", None);
1704 let component_id = helper.add_class("MyComponent", None);
1705
1706 helper.add_export_edge_full(module_id, component_id, ExportKind::Default, None);
1708
1709 assert_eq!(helper.stats().edges_staged, 1);
1710
1711 let edges = staging.operations();
1712 let export_edge = edges.iter().find(|op| {
1713 matches!(
1714 op,
1715 StagingOp::AddEdge {
1716 kind: EdgeKind::Exports { .. },
1717 ..
1718 }
1719 )
1720 });
1721
1722 assert!(export_edge.is_some());
1723 if let StagingOp::AddEdge {
1724 kind: EdgeKind::Exports { kind, alias },
1725 ..
1726 } = export_edge.unwrap()
1727 {
1728 assert_eq!(*kind, ExportKind::Default);
1729 assert!(alias.is_none());
1730 }
1731 }
1732
1733 #[test]
1734 fn test_helper_add_export_edge_with_alias() {
1735 let mut staging = StagingGraph::new();
1736 let file = PathBuf::from("test.js");
1737 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::JavaScript);
1738
1739 let module_id = helper.add_module("app", None);
1740 let helper_fn_id = helper.add_function("internalHelper", None, false, false);
1741
1742 helper.add_export_edge_full(module_id, helper_fn_id, ExportKind::Direct, Some("helper"));
1744
1745 let edges = staging.operations();
1746 let export_edge = edges.iter().find(|op| {
1747 matches!(
1748 op,
1749 StagingOp::AddEdge {
1750 kind: EdgeKind::Exports { .. },
1751 ..
1752 }
1753 )
1754 });
1755
1756 if let StagingOp::AddEdge {
1757 kind: EdgeKind::Exports { kind, alias },
1758 ..
1759 } = export_edge.unwrap()
1760 {
1761 assert_eq!(*kind, ExportKind::Direct);
1762 assert!(alias.is_some(), "Alias should be present");
1763 }
1764 }
1765
1766 #[test]
1767 fn test_helper_add_export_edge_reexport() {
1768 let mut staging = StagingGraph::new();
1769 let file = PathBuf::from("index.js");
1770 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::JavaScript);
1771
1772 let module_id = helper.add_module("index", None);
1773 let utils_id = helper.add_module("utils", None);
1774
1775 helper.add_export_edge_full(module_id, utils_id, ExportKind::Namespace, Some("utils"));
1777
1778 let edges = staging.operations();
1779 let export_edge = edges.iter().find(|op| {
1780 matches!(
1781 op,
1782 StagingOp::AddEdge {
1783 kind: EdgeKind::Exports { .. },
1784 ..
1785 }
1786 )
1787 });
1788
1789 if let StagingOp::AddEdge {
1790 kind: EdgeKind::Exports { kind, alias },
1791 ..
1792 } = export_edge.unwrap()
1793 {
1794 assert_eq!(*kind, ExportKind::Namespace);
1795 assert!(alias.is_some());
1796 }
1797 }
1798
1799 #[test]
1800 fn test_helper_add_call_edge_full_with_span() {
1801 let mut staging = StagingGraph::new();
1802 let file = PathBuf::from("test.rs");
1803 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
1804
1805 let caller_id = helper.add_function("caller", None, false, false);
1806 let callee_id = helper.add_function("callee", None, false, false);
1807
1808 let span = Span {
1809 start: Position { line: 5, column: 4 },
1810 end: Position {
1811 line: 5,
1812 column: 20,
1813 },
1814 };
1815
1816 helper.add_call_edge_full_with_span(caller_id, callee_id, 2, false, vec![span]);
1817
1818 let edges = staging.operations();
1819 let call_edge = edges.iter().find(|op| {
1820 matches!(
1821 op,
1822 StagingOp::AddEdge {
1823 kind: EdgeKind::Calls { .. },
1824 ..
1825 }
1826 )
1827 });
1828
1829 if let StagingOp::AddEdge {
1830 kind:
1831 EdgeKind::Calls {
1832 argument_count,
1833 is_async,
1834 },
1835 spans: edge_spans,
1836 ..
1837 } = call_edge.unwrap()
1838 {
1839 assert_eq!(*argument_count, 2);
1840 assert!(!*is_async);
1841 assert!(!edge_spans.is_empty());
1842 }
1843 }
1844
1845 #[test]
1846 fn test_helper_add_function_with_async_attribute() {
1847 let mut staging = StagingGraph::new();
1848 let file = PathBuf::from("test.kt");
1849 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Kotlin);
1850
1851 let _func_id = helper.add_function("fetchData", None, true, false);
1853
1854 let ops = staging.operations();
1856 let add_node_op = ops
1857 .iter()
1858 .find(|op| matches!(op, StagingOp::AddNode { .. }));
1859
1860 assert!(add_node_op.is_some(), "Expected AddNode operation");
1861 if let StagingOp::AddNode { entry, .. } = add_node_op.unwrap() {
1862 assert!(
1863 entry.is_async,
1864 "Expected is_async=true for suspend function, got is_async=false"
1865 );
1866 }
1867 }
1868
1869 #[test]
1870 fn test_helper_add_method_with_static_attribute() {
1871 let mut staging = StagingGraph::new();
1872 let file = PathBuf::from("test.java");
1873 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Java);
1874
1875 let _method_id = helper.add_method("MyClass.staticMethod", None, false, true);
1877
1878 let ops = staging.operations();
1880 let add_node_op = ops
1881 .iter()
1882 .find(|op| matches!(op, StagingOp::AddNode { .. }));
1883
1884 assert!(add_node_op.is_some(), "Expected AddNode operation");
1885 if let StagingOp::AddNode { entry, .. } = add_node_op.unwrap() {
1886 assert!(
1887 entry.is_static,
1888 "Expected is_static=true for static method, got is_static=false"
1889 );
1890 }
1891 }
1892
1893 #[test]
1894 fn test_helper_add_function_without_attributes() {
1895 let mut staging = StagingGraph::new();
1896 let file = PathBuf::from("test.rs");
1897 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
1898
1899 let _func_id = helper.add_function("regular_function", None, false, false);
1901
1902 let ops = staging.operations();
1904 let add_node_op = ops
1905 .iter()
1906 .find(|op| matches!(op, StagingOp::AddNode { .. }));
1907
1908 assert!(add_node_op.is_some(), "Expected AddNode operation");
1909 if let StagingOp::AddNode { entry, .. } = add_node_op.unwrap() {
1910 assert!(
1911 !entry.is_async,
1912 "Expected is_async=false for regular function"
1913 );
1914 assert!(
1915 !entry.is_static,
1916 "Expected is_static=false for regular function"
1917 );
1918 }
1919 }
1920
1921 #[test]
1922 fn test_helper_add_method_with_both_attributes() {
1923 let mut staging = StagingGraph::new();
1924 let file = PathBuf::from("test.kt");
1925 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Kotlin);
1926
1927 let _method_id = helper.add_method("Service.asyncStaticMethod", None, true, true);
1929
1930 let ops = staging.operations();
1932 let add_node_op = ops
1933 .iter()
1934 .find(|op| matches!(op, StagingOp::AddNode { .. }));
1935
1936 assert!(add_node_op.is_some(), "Expected AddNode operation");
1937 if let StagingOp::AddNode { entry, .. } = add_node_op.unwrap() {
1938 assert!(entry.is_async, "Expected is_async=true for async method");
1939 assert!(entry.is_static, "Expected is_static=true for static method");
1940 }
1941 }
1942
1943 #[test]
1944 fn test_helper_add_function_with_unsafe_attribute() {
1945 let mut staging = StagingGraph::new();
1946 let file = PathBuf::from("test.rs");
1947 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
1948
1949 let _func_id = helper.add_function("unsafe_function", None, false, true);
1951
1952 let ops = staging.operations();
1954 let add_node_op = ops
1955 .iter()
1956 .find(|op| matches!(op, StagingOp::AddNode { .. }));
1957
1958 assert!(add_node_op.is_some(), "Expected AddNode operation");
1959 if let StagingOp::AddNode { entry, .. } = add_node_op.unwrap() {
1960 assert!(
1961 entry.is_unsafe,
1962 "Expected is_unsafe=true for unsafe function, got is_unsafe={}",
1963 entry.is_unsafe
1964 );
1965 }
1966 }
1967
1968 #[test]
1973 fn test_ensure_function_reuses_existing_method_node() {
1974 let mut staging = StagingGraph::new();
1975 let file = PathBuf::from("test.ts");
1976 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::TypeScript);
1977
1978 let span = Span::new(
1979 Position { line: 5, column: 4 },
1980 Position {
1981 line: 10,
1982 column: 5,
1983 },
1984 );
1985
1986 let method_id = helper.add_method("MyClass.doWork", Some(span), true, false);
1988
1989 let reused_id = helper.ensure_function("MyClass.doWork", None, true, false);
1991
1992 assert_eq!(
1993 method_id, reused_id,
1994 "ensure_function should reuse the existing Method node"
1995 );
1996 assert_eq!(
1997 helper.stats().nodes_created,
1998 1,
1999 "Only the Method node should exist"
2000 );
2001 }
2002
2003 #[test]
2004 fn test_ensure_method_reuses_existing_function_node() {
2005 let mut staging = StagingGraph::new();
2006 let file = PathBuf::from("test.ts");
2007 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::TypeScript);
2008
2009 let func_id = helper.add_function("standalone", None, false, false);
2010 let reused_id = helper.ensure_method("standalone", None, false, false);
2011
2012 assert_eq!(
2013 func_id, reused_id,
2014 "ensure_method should reuse the existing function node"
2015 );
2016 assert_eq!(helper.stats().nodes_created, 1);
2017 }
2018
2019 #[test]
2020 fn test_ensure_function_creates_new_when_no_method_exists() {
2021 let mut staging = StagingGraph::new();
2022 let file = PathBuf::from("test.ts");
2023 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::TypeScript);
2024
2025 let func_id = helper.ensure_function("topLevel", None, false, false);
2026 assert!(!func_id.is_invalid());
2027 assert_eq!(helper.stats().nodes_created, 1);
2028
2029 let func_id2 = helper.ensure_function("topLevel", None, false, false);
2030 assert_eq!(func_id, func_id2);
2031 assert_eq!(helper.stats().nodes_created, 1);
2032 }
2033
2034 #[test]
2035 fn test_no_method_function_duplicate_after_cross_kind_reuse() {
2036 let mut staging = StagingGraph::new();
2037 let file = PathBuf::from("browser-manager.ts");
2038 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::TypeScript);
2039
2040 let span_a = Span::new(
2041 Position { line: 3, column: 4 },
2042 Position { line: 8, column: 5 },
2043 );
2044 let span_b = Span::new(
2045 Position {
2046 line: 10,
2047 column: 4,
2048 },
2049 Position {
2050 line: 15,
2051 column: 5,
2052 },
2053 );
2054
2055 let _method_a = helper.add_method("BrowserManager.newTab", Some(span_a), true, false);
2057 let _method_b = helper.add_method("BrowserManager.restoreState", Some(span_b), true, false);
2058
2059 let _caller_a = helper.ensure_function("BrowserManager.newTab", None, true, false);
2061 let _caller_b = helper.ensure_function("BrowserManager.restoreState", None, true, false);
2062
2063 let ops = staging.operations();
2065 let mut method_names = std::collections::HashSet::new();
2066 let mut function_names = std::collections::HashSet::new();
2067
2068 for op in ops {
2069 if let StagingOp::AddNode { entry, .. } = op {
2070 if entry.kind == NodeKind::Method {
2071 method_names.insert(entry.name);
2072 } else if entry.kind == NodeKind::Function {
2073 function_names.insert(entry.name);
2074 }
2075 }
2076 }
2077
2078 let overlap: Vec<_> = method_names.intersection(&function_names).collect();
2079 assert!(
2080 overlap.is_empty(),
2081 "Found names that are both Method and Function: {overlap:?}"
2082 );
2083 }
2084}