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
54pub(crate) const CALL_COMPATIBLE_KINDS: &[NodeKind] = &[
62 NodeKind::Function,
63 NodeKind::Method,
64 NodeKind::Macro,
65 NodeKind::Constant,
66 NodeKind::LambdaTarget,
67];
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum CalleeKindHint {
75 Function,
77 Method,
79 Macro,
81 Constant,
83 LambdaTarget,
85 Any,
87}
88
89impl CalleeKindHint {
90 fn to_node_kind(self) -> NodeKind {
92 match self {
93 Self::Function | Self::Any => NodeKind::Function,
94 Self::Method => NodeKind::Method,
95 Self::Macro => NodeKind::Macro,
96 Self::Constant => NodeKind::Constant,
97 Self::LambdaTarget => NodeKind::LambdaTarget,
98 }
99 }
100}
101
102#[derive(Debug)]
109pub struct GraphBuildHelper<'a> {
110 staging: &'a mut StagingGraph,
112 language: Language,
114 file_id: FileId,
116 file_path: String,
118 string_cache: HashMap<String, StringId>,
120 next_string_id: u32,
122 node_cache: HashMap<(String, NodeKind), NodeId>,
130}
131
132impl<'a> GraphBuildHelper<'a> {
133 pub fn new(staging: &'a mut StagingGraph, file: &Path, language: Language) -> Self {
138 Self {
139 staging,
140 language,
141 file_id: FileId::new(0), file_path: file.display().to_string(),
143 string_cache: HashMap::new(),
144 next_string_id: 0,
145 node_cache: HashMap::new(),
146 }
147 }
148
149 pub fn with_file_id(
151 staging: &'a mut StagingGraph,
152 file: &Path,
153 language: Language,
154 file_id: FileId,
155 ) -> Self {
156 Self {
157 staging,
158 language,
159 file_id,
160 file_path: file.display().to_string(),
161 string_cache: HashMap::new(),
162 next_string_id: 0,
163 node_cache: HashMap::new(),
164 }
165 }
166
167 #[must_use]
169 pub fn language(&self) -> Language {
170 self.language
171 }
172
173 #[must_use]
175 pub fn file_id(&self) -> FileId {
176 self.file_id
177 }
178
179 #[must_use]
185 pub fn lookup_node(&self, name: &str, kind: NodeKind) -> Option<NodeId> {
186 self.node_cache.get(&(name.to_string(), kind)).copied()
187 }
188
189 #[must_use]
191 pub fn file_path(&self) -> &str {
192 &self.file_path
193 }
194
195 pub fn attach_body_hashes(&mut self, content: &[u8]) {
203 self.staging.attach_body_hashes(content);
204 }
205
206 pub fn intern(&mut self, s: &str) -> StringId {
212 if let Some(&id) = self.string_cache.get(s) {
213 return id;
214 }
215
216 let id = StringId::new_local(self.next_string_id);
217 self.next_string_id += 1;
218 self.string_cache.insert(s.to_string(), id);
219 self.staging.intern_string(id, s.to_string());
221 id
222 }
223
224 #[must_use]
226 pub fn has_node(&self, qualified_name: &str) -> bool {
227 self.node_cache
228 .keys()
229 .any(|(name, _)| name == qualified_name)
230 }
231
232 #[must_use]
234 pub fn get_node(&self, qualified_name: &str) -> Option<NodeId> {
235 self.node_cache
236 .iter()
237 .find_map(|((name, _), id)| (name == qualified_name).then_some(*id))
238 }
239
240 #[must_use]
242 pub fn has_node_with_kind(&self, qualified_name: &str, kind: NodeKind) -> bool {
243 self.node_cache
244 .contains_key(&(qualified_name.to_string(), kind))
245 }
246
247 #[must_use]
249 pub fn get_node_with_kind(&self, qualified_name: &str, kind: NodeKind) -> Option<NodeId> {
250 self.node_cache
251 .get(&(qualified_name.to_string(), kind))
252 .copied()
253 }
254
255 pub fn add_function(
259 &mut self,
260 qualified_name: &str,
261 span: Option<Span>,
262 is_async: bool,
263 is_unsafe: bool,
264 ) -> NodeId {
265 self.add_node_internal(
266 qualified_name,
267 span,
268 NodeKind::Function,
269 &[("async", is_async), ("unsafe", is_unsafe)],
270 None,
271 None,
272 )
273 }
274
275 pub fn add_function_with_visibility(
279 &mut self,
280 qualified_name: &str,
281 span: Option<Span>,
282 is_async: bool,
283 is_unsafe: bool,
284 visibility: Option<&str>,
285 ) -> NodeId {
286 self.add_node_internal(
287 qualified_name,
288 span,
289 NodeKind::Function,
290 &[("async", is_async), ("unsafe", is_unsafe)],
291 visibility,
292 None,
293 )
294 }
295
296 pub fn add_function_with_signature(
301 &mut self,
302 qualified_name: &str,
303 span: Option<Span>,
304 is_async: bool,
305 is_unsafe: bool,
306 visibility: Option<&str>,
307 signature: Option<&str>,
308 ) -> NodeId {
309 self.add_node_internal(
310 qualified_name,
311 span,
312 NodeKind::Function,
313 &[("async", is_async), ("unsafe", is_unsafe)],
314 visibility,
315 signature,
316 )
317 }
318
319 pub fn add_method(
321 &mut self,
322 qualified_name: &str,
323 span: Option<Span>,
324 is_async: bool,
325 is_static: bool,
326 ) -> NodeId {
327 self.add_node_internal(
328 qualified_name,
329 span,
330 NodeKind::Method,
331 &[("async", is_async), ("static", is_static)],
332 None,
333 None,
334 )
335 }
336
337 pub fn add_method_with_visibility(
339 &mut self,
340 qualified_name: &str,
341 span: Option<Span>,
342 is_async: bool,
343 is_static: bool,
344 visibility: Option<&str>,
345 ) -> NodeId {
346 self.add_node_internal(
347 qualified_name,
348 span,
349 NodeKind::Method,
350 &[("async", is_async), ("static", is_static)],
351 visibility,
352 None,
353 )
354 }
355
356 pub fn add_method_with_signature(
361 &mut self,
362 qualified_name: &str,
363 span: Option<Span>,
364 is_async: bool,
365 is_static: bool,
366 visibility: Option<&str>,
367 signature: Option<&str>,
368 ) -> NodeId {
369 self.add_node_internal(
370 qualified_name,
371 span,
372 NodeKind::Method,
373 &[("async", is_async), ("static", is_static)],
374 visibility,
375 signature,
376 )
377 }
378
379 pub fn add_class(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
381 self.add_node_internal(qualified_name, span, NodeKind::Class, &[], None, None)
382 }
383
384 pub fn add_class_with_visibility(
386 &mut self,
387 qualified_name: &str,
388 span: Option<Span>,
389 visibility: Option<&str>,
390 ) -> NodeId {
391 self.add_node_internal(qualified_name, span, NodeKind::Class, &[], visibility, None)
392 }
393
394 pub fn add_struct(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
396 self.add_node_internal(qualified_name, span, NodeKind::Struct, &[], None, None)
397 }
398
399 pub fn add_struct_with_visibility(
401 &mut self,
402 qualified_name: &str,
403 span: Option<Span>,
404 visibility: Option<&str>,
405 ) -> NodeId {
406 self.add_node_internal(
407 qualified_name,
408 span,
409 NodeKind::Struct,
410 &[],
411 visibility,
412 None,
413 )
414 }
415
416 pub fn add_module(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
418 self.add_node_internal(qualified_name, span, NodeKind::Module, &[], None, None)
419 }
420
421 pub fn add_resource(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
423 self.add_node_internal(qualified_name, span, NodeKind::Resource, &[], None, None)
424 }
425
426 pub fn add_endpoint(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
434 self.add_node_internal(qualified_name, span, NodeKind::Endpoint, &[], None, None)
435 }
436
437 pub fn add_import(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
439 self.add_node_internal(qualified_name, span, NodeKind::Import, &[], None, None)
440 }
441
442 pub fn add_verbatim_import(&mut self, name: &str, span: Option<Span>) -> NodeId {
448 self.add_node_verbatim(name, span, NodeKind::Import, &[], None, None)
449 }
450
451 pub fn add_variable(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
453 self.add_node_internal(qualified_name, span, NodeKind::Variable, &[], None, None)
454 }
455
456 pub fn add_verbatim_variable(&mut self, name: &str, span: Option<Span>) -> NodeId {
461 self.add_node_verbatim(name, span, NodeKind::Variable, &[], None, None)
462 }
463
464 pub fn add_constant(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
466 self.add_node_internal(qualified_name, span, NodeKind::Constant, &[], None, None)
467 }
468
469 pub fn add_constant_with_visibility(
471 &mut self,
472 qualified_name: &str,
473 span: Option<Span>,
474 visibility: Option<&str>,
475 ) -> NodeId {
476 self.add_node_internal(
477 qualified_name,
478 span,
479 NodeKind::Constant,
480 &[],
481 visibility,
482 None,
483 )
484 }
485
486 pub fn add_constant_with_static_and_visibility(
488 &mut self,
489 qualified_name: &str,
490 span: Option<Span>,
491 is_static: bool,
492 visibility: Option<&str>,
493 ) -> NodeId {
494 let attrs: &[(&str, bool)] = if is_static { &[("static", true)] } else { &[] };
495 self.add_node_internal(
496 qualified_name,
497 span,
498 NodeKind::Constant,
499 attrs,
500 visibility,
501 None,
502 )
503 }
504
505 pub fn add_property_with_static_and_visibility(
507 &mut self,
508 qualified_name: &str,
509 span: Option<Span>,
510 is_static: bool,
511 visibility: Option<&str>,
512 ) -> NodeId {
513 let attrs: &[(&str, bool)] = if is_static { &[("static", true)] } else { &[] };
514 self.add_node_internal(
515 qualified_name,
516 span,
517 NodeKind::Property,
518 attrs,
519 visibility,
520 None,
521 )
522 }
523
524 pub fn add_enum(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
526 self.add_node_internal(qualified_name, span, NodeKind::Enum, &[], None, None)
527 }
528
529 pub fn add_enum_with_visibility(
531 &mut self,
532 qualified_name: &str,
533 span: Option<Span>,
534 visibility: Option<&str>,
535 ) -> NodeId {
536 self.add_node_internal(qualified_name, span, NodeKind::Enum, &[], visibility, None)
537 }
538
539 pub fn add_interface(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
541 self.add_node_internal(qualified_name, span, NodeKind::Interface, &[], None, None)
542 }
543
544 pub fn add_interface_with_visibility(
546 &mut self,
547 qualified_name: &str,
548 span: Option<Span>,
549 visibility: Option<&str>,
550 ) -> NodeId {
551 self.add_node_internal(
552 qualified_name,
553 span,
554 NodeKind::Interface,
555 &[],
556 visibility,
557 None,
558 )
559 }
560
561 pub fn add_type(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
563 self.add_node_internal(qualified_name, span, NodeKind::Type, &[], None, None)
564 }
565
566 pub fn add_type_with_visibility(
568 &mut self,
569 qualified_name: &str,
570 span: Option<Span>,
571 visibility: Option<&str>,
572 ) -> NodeId {
573 self.add_node_internal(qualified_name, span, NodeKind::Type, &[], visibility, None)
574 }
575
576 pub fn add_lifetime(&mut self, qualified_name: &str, span: Option<Span>) -> NodeId {
578 self.add_node_internal(qualified_name, span, NodeKind::Lifetime, &[], None, None)
579 }
580
581 pub fn add_lifetime_constraint_edge(
583 &mut self,
584 source: NodeId,
585 target: NodeId,
586 constraint_kind: LifetimeConstraintKind,
587 ) {
588 self.staging.add_edge(
589 source,
590 target,
591 EdgeKind::LifetimeConstraint { constraint_kind },
592 self.file_id,
593 );
594 }
595
596 pub fn add_trait_method_binding_edge(
601 &mut self,
602 caller: NodeId,
603 callee: NodeId,
604 trait_name: &str,
605 impl_type: &str,
606 is_ambiguous: bool,
607 ) {
608 let trait_name_id = self.intern(trait_name);
609 let impl_type_id = self.intern(impl_type);
610 self.staging.add_edge(
611 caller,
612 callee,
613 EdgeKind::TraitMethodBinding {
614 trait_name: trait_name_id,
615 impl_type: impl_type_id,
616 is_ambiguous,
617 },
618 self.file_id,
619 );
620 }
621
622 pub fn add_macro_expansion_edge(
648 &mut self,
649 invocation: NodeId,
650 expansion: NodeId,
651 expansion_kind: MacroExpansionKind,
652 is_verified: bool,
653 ) {
654 self.staging.add_edge(
655 invocation,
656 expansion,
657 EdgeKind::MacroExpansion {
658 expansion_kind,
659 is_verified,
660 },
661 self.file_id,
662 );
663 }
664
665 pub fn add_node(&mut self, qualified_name: &str, span: Option<Span>, kind: NodeKind) -> NodeId {
667 self.add_node_internal(qualified_name, span, kind, &[], None, None)
668 }
669
670 pub fn add_node_with_visibility(
672 &mut self,
673 qualified_name: &str,
674 span: Option<Span>,
675 kind: NodeKind,
676 visibility: Option<&str>,
677 ) -> NodeId {
678 self.add_node_internal(qualified_name, span, kind, &[], visibility, None)
679 }
680
681 fn add_node_internal(
691 &mut self,
692 qualified_name: &str,
693 span: Option<Span>,
694 kind: NodeKind,
695 attributes: &[(&str, bool)],
696 visibility: Option<&str>,
697 signature: Option<&str>,
698 ) -> NodeId {
699 let canonical_qualified_name =
700 canonicalize_graph_qualified_name(self.language, qualified_name);
701 let semantic_name = semantic_name_for_node_input(qualified_name, &canonical_qualified_name);
702 let mut is_async = false;
703 let mut is_static = false;
704 let mut is_unsafe = false;
705 for &(key, value) in attributes {
706 match key {
707 "async" => is_async |= value,
708 "static" => is_static |= value,
709 "unsafe" => is_unsafe |= value,
710 _ => {}
711 }
712 }
713
714 if let Some(&id) = self
716 .node_cache
717 .get(&(canonical_qualified_name.clone(), kind))
718 {
719 let visibility_id = visibility.map(|vis| self.intern(vis));
720 let signature_id = signature.map(|sig| self.intern(sig));
721 self.staging.update_node_entry(
722 id,
723 span,
724 is_async,
725 is_static,
726 is_unsafe,
727 visibility_id,
728 signature_id,
729 );
730 return id;
731 }
732
733 let name_id = self.intern(&semantic_name);
734
735 let mut entry = NodeEntry::new(kind, name_id, self.file_id);
737 if semantic_name != canonical_qualified_name {
738 let qualified_name_id = self.intern(&canonical_qualified_name);
739 entry = entry.with_qualified_name(qualified_name_id);
740 }
741
742 if let Some(s) = span {
744 let start_line = u32::try_from(s.start.line.saturating_add(1)).unwrap_or(u32::MAX);
745 let start_column = u32::try_from(s.start.column).unwrap_or(u32::MAX);
746 let end_line = u32::try_from(s.end.line.saturating_add(1)).unwrap_or(u32::MAX);
747 let end_column = u32::try_from(s.end.column).unwrap_or(u32::MAX);
748 entry = entry.with_location(start_line, start_column, end_line, end_column);
749 }
750
751 if is_async {
753 entry = entry.with_async(true);
754 }
755 if is_static {
756 entry = entry.with_static(true);
757 }
758 if is_unsafe {
759 entry = entry.with_unsafe(true);
760 }
761
762 if let Some(vis) = visibility {
764 let vis_id = self.intern(vis);
765 entry = entry.with_visibility(vis_id);
766 }
767
768 if let Some(sig) = signature {
770 let sig_id = self.intern(sig);
771 entry = entry.with_signature(sig_id);
772 }
773
774 let node_id = self.staging.add_node(entry);
776
777 self.node_cache
779 .insert((canonical_qualified_name, kind), node_id);
780
781 node_id
782 }
783
784 fn add_node_verbatim(
785 &mut self,
786 name: &str,
787 span: Option<Span>,
788 kind: NodeKind,
789 attributes: &[(&str, bool)],
790 visibility: Option<&str>,
791 signature: Option<&str>,
792 ) -> NodeId {
793 let mut is_async = false;
794 let mut is_static = false;
795 let mut is_unsafe = false;
796 for &(key, value) in attributes {
797 match key {
798 "async" => is_async |= value,
799 "static" => is_static |= value,
800 "unsafe" => is_unsafe |= value,
801 _ => {}
802 }
803 }
804
805 if let Some(&id) = self.node_cache.get(&(name.to_string(), kind)) {
806 let visibility_id = visibility.map(|vis| self.intern(vis));
807 let signature_id = signature.map(|sig| self.intern(sig));
808 self.staging.update_node_entry(
809 id,
810 span,
811 is_async,
812 is_static,
813 is_unsafe,
814 visibility_id,
815 signature_id,
816 );
817 return id;
818 }
819
820 let name_id = self.intern(name);
821 let mut entry = NodeEntry::new(kind, name_id, self.file_id);
822
823 if let Some(s) = span {
824 let start_line = u32::try_from(s.start.line.saturating_add(1)).unwrap_or(u32::MAX);
825 let start_column = u32::try_from(s.start.column).unwrap_or(u32::MAX);
826 let end_line = u32::try_from(s.end.line.saturating_add(1)).unwrap_or(u32::MAX);
827 let end_column = u32::try_from(s.end.column).unwrap_or(u32::MAX);
828 entry = entry.with_location(start_line, start_column, end_line, end_column);
829 }
830
831 if is_async {
832 entry = entry.with_async(true);
833 }
834 if is_static {
835 entry = entry.with_static(true);
836 }
837 if is_unsafe {
838 entry = entry.with_unsafe(true);
839 }
840
841 if let Some(vis) = visibility {
842 let vis_id = self.intern(vis);
843 entry = entry.with_visibility(vis_id);
844 }
845 if let Some(sig) = signature {
846 let sig_id = self.intern(sig);
847 entry = entry.with_signature(sig_id);
848 }
849
850 let node_id = self.staging.add_node(entry);
851 self.node_cache.insert((name.to_string(), kind), node_id);
852 node_id
853 }
854
855 pub fn add_call_edge(&mut self, caller: NodeId, callee: NodeId) {
857 self.add_call_edge_with_span(caller, callee, Vec::new());
858 }
859
860 pub fn add_call_edge_with_span(
870 &mut self,
871 caller: NodeId,
872 callee: NodeId,
873 spans: Vec<crate::graph::node::Span>,
874 ) {
875 self.staging.add_edge_with_spans(
876 caller,
877 callee,
878 EdgeKind::Calls {
879 argument_count: 255,
880 is_async: false,
881 },
882 self.file_id,
883 spans,
884 );
885 }
886
887 pub fn add_call_edge_full(
918 &mut self,
919 caller: NodeId,
920 callee: NodeId,
921 argument_count: u8,
922 is_async: bool,
923 ) {
924 self.staging.add_edge(
925 caller,
926 callee,
927 EdgeKind::Calls {
928 argument_count,
929 is_async,
930 },
931 self.file_id,
932 );
933 }
934
935 pub fn add_call_edge_full_with_span(
940 &mut self,
941 caller: NodeId,
942 callee: NodeId,
943 argument_count: u8,
944 is_async: bool,
945 spans: Vec<crate::graph::node::Span>,
946 ) {
947 self.staging.add_edge_with_spans(
948 caller,
949 callee,
950 EdgeKind::Calls {
951 argument_count,
952 is_async,
953 },
954 self.file_id,
955 spans,
956 );
957 }
958
959 pub fn add_table_read_edge_with_span(
961 &mut self,
962 reader: NodeId,
963 table: NodeId,
964 table_name: &str,
965 schema: Option<&str>,
966 spans: Vec<crate::graph::node::Span>,
967 ) {
968 let table_name_id = self.intern(table_name);
969 let schema_id = schema.map(|s| self.intern(s));
970 self.staging.add_edge_with_spans(
971 reader,
972 table,
973 EdgeKind::TableRead {
974 table_name: table_name_id,
975 schema: schema_id,
976 },
977 self.file_id,
978 spans,
979 );
980 }
981
982 pub fn add_table_write_edge_with_span(
984 &mut self,
985 writer: NodeId,
986 table: NodeId,
987 table_name: &str,
988 schema: Option<&str>,
989 operation: TableWriteOp,
990 spans: Vec<crate::graph::node::Span>,
991 ) {
992 let table_name_id = self.intern(table_name);
993 let schema_id = schema.map(|s| self.intern(s));
994 self.staging.add_edge_with_spans(
995 writer,
996 table,
997 EdgeKind::TableWrite {
998 table_name: table_name_id,
999 schema: schema_id,
1000 operation,
1001 },
1002 self.file_id,
1003 spans,
1004 );
1005 }
1006
1007 pub fn add_triggered_by_edge_with_span(
1011 &mut self,
1012 trigger: NodeId,
1013 table: NodeId,
1014 trigger_name: &str,
1015 schema: Option<&str>,
1016 spans: Vec<crate::graph::node::Span>,
1017 ) {
1018 let trigger_name_id = self.intern(trigger_name);
1019 let schema_id = schema.map(|s| self.intern(s));
1020 self.staging.add_edge_with_spans(
1021 trigger,
1022 table,
1023 EdgeKind::TriggeredBy {
1024 trigger_name: trigger_name_id,
1025 schema: schema_id,
1026 },
1027 self.file_id,
1028 spans,
1029 );
1030 }
1031
1032 pub fn add_import_edge(&mut self, importer: NodeId, imported: NodeId) {
1038 self.staging.add_edge(
1039 importer,
1040 imported,
1041 EdgeKind::Imports {
1042 alias: None,
1043 is_wildcard: false,
1044 },
1045 self.file_id,
1046 );
1047 }
1048
1049 pub fn add_import_edge_full(
1081 &mut self,
1082 importer: NodeId,
1083 imported: NodeId,
1084 alias: Option<&str>,
1085 is_wildcard: bool,
1086 ) {
1087 let alias_id = alias.map(|s| self.intern(s));
1088 self.staging.add_edge(
1089 importer,
1090 imported,
1091 EdgeKind::Imports {
1092 alias: alias_id,
1093 is_wildcard,
1094 },
1095 self.file_id,
1096 );
1097 }
1098
1099 pub fn add_export_edge(&mut self, module: NodeId, exported: NodeId) {
1105 self.staging.add_edge(
1106 module,
1107 exported,
1108 EdgeKind::Exports {
1109 kind: ExportKind::Direct,
1110 alias: None,
1111 },
1112 self.file_id,
1113 );
1114 }
1115
1116 pub fn add_export_edge_full(
1158 &mut self,
1159 module: NodeId,
1160 exported: NodeId,
1161 kind: ExportKind,
1162 alias: Option<&str>,
1163 ) {
1164 let alias_id = alias.map(|s| self.intern(s));
1165 self.staging.add_edge(
1166 module,
1167 exported,
1168 EdgeKind::Exports {
1169 kind,
1170 alias: alias_id,
1171 },
1172 self.file_id,
1173 );
1174 }
1175
1176 pub fn add_reference_edge(&mut self, from: NodeId, to: NodeId) {
1178 self.staging
1179 .add_edge(from, to, EdgeKind::References, self.file_id);
1180 }
1181
1182 pub fn add_defines_edge(&mut self, parent: NodeId, child: NodeId) {
1184 self.staging
1185 .add_edge(parent, child, EdgeKind::Defines, self.file_id);
1186 }
1187
1188 pub fn add_typeof_edge(&mut self, source: NodeId, target: NodeId) {
1193 self.add_typeof_edge_with_context(source, target, None, None, None);
1194 }
1195
1196 pub fn add_typeof_edge_with_context(
1235 &mut self,
1236 source: NodeId,
1237 target: NodeId,
1238 context: Option<TypeOfContext>,
1239 index: Option<u16>,
1240 name: Option<&str>,
1241 ) {
1242 let name_id = name.map(|n| self.intern(n));
1243 self.staging.add_edge(
1244 source,
1245 target,
1246 EdgeKind::TypeOf {
1247 context,
1248 index,
1249 name: name_id,
1250 },
1251 self.file_id,
1252 );
1253 }
1254
1255 pub fn add_implements_edge(&mut self, implementor: NodeId, interface: NodeId) {
1257 self.staging
1258 .add_edge(implementor, interface, EdgeKind::Implements, self.file_id);
1259 }
1260
1261 pub fn add_inherits_edge(&mut self, child: NodeId, parent: NodeId) {
1263 self.staging
1264 .add_edge(child, parent, EdgeKind::Inherits, self.file_id);
1265 }
1266
1267 pub fn add_contains_edge(&mut self, parent: NodeId, child: NodeId) {
1269 self.staging
1270 .add_edge(parent, child, EdgeKind::Contains, self.file_id);
1271 }
1272
1273 pub fn add_webassembly_edge(&mut self, caller: NodeId, wasm_target: NodeId) {
1280 self.staging
1281 .add_edge(caller, wasm_target, EdgeKind::WebAssemblyCall, self.file_id);
1282 }
1283
1284 pub fn add_ffi_edge(&mut self, caller: NodeId, ffi_target: NodeId, convention: FfiConvention) {
1292 self.staging.add_edge(
1293 caller,
1294 ffi_target,
1295 EdgeKind::FfiCall { convention },
1296 self.file_id,
1297 );
1298 }
1299
1300 pub fn add_http_request_edge(
1304 &mut self,
1305 caller: NodeId,
1306 target: NodeId,
1307 method: HttpMethod,
1308 url: Option<&str>,
1309 ) {
1310 let url_id = url.map(|value| self.intern(value));
1311 self.staging.add_edge(
1312 caller,
1313 target,
1314 EdgeKind::HttpRequest {
1315 method,
1316 url: url_id,
1317 },
1318 self.file_id,
1319 );
1320 }
1321
1322 fn reuse_across_call_compatible_kinds(
1329 &self,
1330 canonical: &str,
1331 exclude: NodeKind,
1332 ) -> Option<NodeId> {
1333 for &kind in CALL_COMPATIBLE_KINDS {
1334 if kind == exclude {
1335 continue;
1336 }
1337 if let Some(&id) = self.node_cache.get(&(canonical.to_string(), kind)) {
1338 return Some(id);
1339 }
1340 }
1341 None
1342 }
1343
1344 pub fn ensure_callee(
1355 &mut self,
1356 qualified_name: &str,
1357 call_site_span: Span,
1358 kind_hint: CalleeKindHint,
1359 ) -> NodeId {
1360 let canonical = canonicalize_graph_qualified_name(self.language, qualified_name);
1361 let target_kind = kind_hint.to_node_kind();
1362
1363 if let Some(&id) = self.node_cache.get(&(canonical.clone(), target_kind)) {
1365 return id;
1366 }
1367 if let Some(id) = self.reuse_across_call_compatible_kinds(&canonical, target_kind) {
1369 return id;
1370 }
1371 self.add_node_internal(
1373 qualified_name,
1374 Some(call_site_span),
1375 target_kind,
1376 &[],
1377 None,
1378 None,
1379 )
1380 }
1381
1382 pub fn ensure_function(
1394 &mut self,
1395 qualified_name: &str,
1396 span: Option<Span>,
1397 is_async: bool,
1398 is_unsafe: bool,
1399 ) -> NodeId {
1400 let canonical = canonicalize_graph_qualified_name(self.language, qualified_name);
1401 if let Some(id) = self.reuse_across_call_compatible_kinds(&canonical, NodeKind::Function) {
1402 return id;
1403 }
1404 self.add_function(qualified_name, span, is_async, is_unsafe)
1405 }
1406
1407 pub fn ensure_method(
1414 &mut self,
1415 qualified_name: &str,
1416 span: Option<Span>,
1417 is_async: bool,
1418 is_static: bool,
1419 ) -> NodeId {
1420 let canonical = canonicalize_graph_qualified_name(self.language, qualified_name);
1421 if let Some(id) = self.reuse_across_call_compatible_kinds(&canonical, NodeKind::Method) {
1422 return id;
1423 }
1424 self.add_method(qualified_name, span, is_async, is_static)
1425 }
1426
1427 #[must_use]
1429 pub fn stats(&self) -> HelperStats {
1430 let staging_stats = self.staging.stats();
1431 HelperStats {
1432 strings_interned: self.string_cache.len(),
1433 nodes_created: self.node_cache.len(),
1434 nodes_staged: staging_stats.nodes_staged,
1435 edges_staged: staging_stats.edges_staged,
1436 }
1437 }
1438}
1439
1440fn semantic_name_for_node_input(original: &str, canonical: &str) -> String {
1441 if original.contains('/') {
1442 return original.to_string();
1443 }
1444
1445 canonical
1446 .rsplit("::")
1447 .next()
1448 .map_or_else(|| original.to_string(), ToString::to_string)
1449}
1450
1451#[derive(Debug, Clone, Default)]
1453pub struct HelperStats {
1454 pub strings_interned: usize,
1456 pub nodes_created: usize,
1458 pub nodes_staged: usize,
1460 pub edges_staged: usize,
1462}
1463
1464#[cfg(test)]
1465mod tests {
1466 use super::*;
1467 use crate::graph::node::Position;
1468 use crate::graph::unified::build::staging::StagingOp;
1469 use std::path::PathBuf;
1470
1471 #[test]
1472 fn test_helper_add_function() {
1473 let mut staging = StagingGraph::new();
1474 let file = PathBuf::from("test.rs");
1475 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
1476
1477 let node_id = helper.add_function("main", None, false, false);
1478 assert!(!node_id.is_invalid());
1479 assert_eq!(helper.stats().nodes_created, 1);
1480 }
1481
1482 #[test]
1483 fn test_helper_deduplication() {
1484 let mut staging = StagingGraph::new();
1485 let file = PathBuf::from("test.rs");
1486 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
1487
1488 let id1 = helper.add_function("main", None, false, false);
1489 let id2 = helper.add_function("main", None, false, false);
1490
1491 assert_eq!(id1, id2, "Same function should return same NodeId");
1492 assert_eq!(
1493 helper.stats().nodes_created,
1494 1,
1495 "Should only create one node"
1496 );
1497 }
1498
1499 #[test]
1500 fn test_helper_string_interning() {
1501 let mut staging = StagingGraph::new();
1502 let file = PathBuf::from("test.rs");
1503 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
1504
1505 let s1 = helper.intern("hello");
1506 let s2 = helper.intern("world");
1507 let s3 = helper.intern("hello"); assert_ne!(s1, s2, "Different strings should have different IDs");
1510 assert_eq!(s1, s3, "Same string should return same ID");
1511 assert_eq!(helper.stats().strings_interned, 2);
1512 }
1513
1514 #[test]
1515 fn test_helper_add_call_edge() {
1516 let mut staging = StagingGraph::new();
1517 let file = PathBuf::from("test.rs");
1518 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
1519
1520 let main_id = helper.add_function("main", None, false, false);
1521 let helper_id = helper.add_function("helper", None, false, false);
1522
1523 helper.add_call_edge(main_id, helper_id);
1524
1525 assert_eq!(helper.stats().edges_staged, 1);
1526 let edge_kind = staging.operations().iter().find_map(|op| {
1527 if let StagingOp::AddEdge { kind, .. } = op {
1528 Some(kind)
1529 } else {
1530 None
1531 }
1532 });
1533 match edge_kind {
1534 Some(EdgeKind::Calls {
1535 argument_count,
1536 is_async,
1537 }) => {
1538 assert_eq!(*argument_count, 255);
1539 assert!(!*is_async);
1540 }
1541 _ => panic!("Expected Calls edge"),
1542 }
1543 }
1544
1545 #[test]
1546 fn test_helper_multiple_node_kinds() {
1547 let mut staging = StagingGraph::new();
1548 let file = PathBuf::from("test.py");
1549 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Python);
1550
1551 let _class_id = helper.add_class("MyClass", None);
1552 let _method_id = helper.add_method("MyClass.my_method", None, false, false);
1553 let _func_id = helper.add_function("standalone_func", None, true, false);
1554
1555 assert_eq!(helper.stats().nodes_created, 3);
1556 }
1557
1558 #[test]
1559 fn test_helper_canonicalizes_language_native_qualified_names() {
1560 let mut staging = StagingGraph::new();
1561 let file = PathBuf::from("test.py");
1562 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Python);
1563
1564 let _method_id = helper.add_method("pkg.module.run", None, false, false);
1565
1566 let add_node_op = staging
1567 .operations()
1568 .iter()
1569 .find(|op| matches!(op, StagingOp::AddNode { .. }))
1570 .expect("Expected AddNode operation");
1571
1572 if let StagingOp::AddNode { entry, .. } = add_node_op {
1573 assert_eq!(staging.resolve_local_string(entry.name), Some("run"));
1574 assert_eq!(
1575 staging.resolve_node_name(entry),
1576 Some("pkg::module::run"),
1577 "expected GraphBuildHelper to canonicalize Python dotted qualified names"
1578 );
1579 }
1580 }
1581
1582 #[test]
1583 fn test_helper_preserves_path_qualified_names() {
1584 let mut staging = StagingGraph::new();
1585 let file = PathBuf::from("test.js");
1586 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::JavaScript);
1587
1588 let _func_id = helper.add_function("frontend/api.js::fetchUsers", None, false, false);
1589
1590 let add_node_op = staging
1591 .operations()
1592 .iter()
1593 .find(|op| matches!(op, StagingOp::AddNode { .. }))
1594 .expect("Expected AddNode operation");
1595
1596 if let StagingOp::AddNode { entry, .. } = add_node_op {
1597 assert_eq!(
1598 staging.resolve_local_string(entry.name),
1599 Some("frontend/api.js::fetchUsers")
1600 );
1601 assert_eq!(
1602 staging.resolve_node_name(entry),
1603 Some("frontend/api.js::fetchUsers"),
1604 "expected path-qualified names to remain unchanged"
1605 );
1606 }
1607 }
1608
1609 #[test]
1610 fn test_helper_verbatim_import_preserves_resource_name() {
1611 let mut staging = StagingGraph::new();
1612 let file = PathBuf::from("index.html");
1613 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Html);
1614
1615 let _import_id = helper.add_verbatim_import("styles.css", None);
1616
1617 let add_node_op = staging
1618 .operations()
1619 .iter()
1620 .find(|op| matches!(op, StagingOp::AddNode { .. }))
1621 .expect("Expected AddNode operation");
1622
1623 if let StagingOp::AddNode { entry, .. } = add_node_op {
1624 assert_eq!(staging.resolve_local_string(entry.name), Some("styles.css"));
1625 assert_eq!(entry.qualified_name, None);
1626 assert_eq!(
1627 staging.resolve_node_name(entry),
1628 Some("styles.css"),
1629 "expected verbatim resource imports to preserve their literal identity"
1630 );
1631 }
1632 }
1633
1634 #[test]
1635 fn test_helper_verbatim_variable_preserves_resource_name() {
1636 let mut staging = StagingGraph::new();
1637 let file = PathBuf::from("index.html");
1638 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Html);
1639
1640 let _variable_id = helper.add_verbatim_variable("/assets/logo.icon.png", None);
1641
1642 let add_node_op = staging
1643 .operations()
1644 .iter()
1645 .find(|op| matches!(op, StagingOp::AddNode { .. }))
1646 .expect("Expected AddNode operation");
1647
1648 if let StagingOp::AddNode { entry, .. } = add_node_op {
1649 assert_eq!(
1650 staging.resolve_local_string(entry.name),
1651 Some("/assets/logo.icon.png")
1652 );
1653 assert_eq!(entry.qualified_name, None);
1654 assert_eq!(
1655 staging.resolve_node_name(entry),
1656 Some("/assets/logo.icon.png"),
1657 "expected verbatim resource variables to preserve their literal identity"
1658 );
1659 }
1660 }
1661
1662 #[test]
1663 fn test_helper_ensure_function() {
1664 let mut staging = StagingGraph::new();
1665 let file = PathBuf::from("test.rs");
1666 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
1667
1668 let id1 = helper.ensure_function("foo", None, false, false);
1669 let id2 = helper.ensure_function("foo", None, true, false); assert_eq!(id1, id2, "ensure_function should be idempotent by name");
1672 }
1673
1674 #[test]
1675 fn test_helper_with_span() {
1676 let mut staging = StagingGraph::new();
1677 let file = PathBuf::from("test.rs");
1678 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
1679
1680 let span = Span {
1681 start: Position {
1682 line: 10,
1683 column: 0,
1684 },
1685 end: Position {
1686 line: 15,
1687 column: 1,
1688 },
1689 };
1690
1691 let node_id = helper.add_function("main", Some(span), false, false);
1692 assert!(!node_id.is_invalid());
1693 }
1694
1695 #[test]
1696 fn test_helper_add_call_edge_full() {
1697 let mut staging = StagingGraph::new();
1698 let file = PathBuf::from("test.rs");
1699 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
1700
1701 let caller_id = helper.add_function("caller", None, false, false);
1702 let callee_id = helper.add_function("callee", None, false, false);
1703
1704 helper.add_call_edge_full(caller_id, callee_id, 3, true);
1706
1707 assert_eq!(helper.stats().edges_staged, 1);
1708
1709 let edges = staging.operations();
1711 let call_edge = edges.iter().find(|op| {
1712 matches!(
1713 op,
1714 StagingOp::AddEdge {
1715 kind: EdgeKind::Calls { .. },
1716 ..
1717 }
1718 )
1719 });
1720
1721 assert!(call_edge.is_some());
1722 if let StagingOp::AddEdge {
1723 kind:
1724 EdgeKind::Calls {
1725 argument_count,
1726 is_async,
1727 },
1728 ..
1729 } = call_edge.unwrap()
1730 {
1731 assert_eq!(*argument_count, 3);
1732 assert!(*is_async);
1733 }
1734 }
1735
1736 #[test]
1737 fn test_helper_add_import_edge_full() {
1738 let mut staging = StagingGraph::new();
1739 let file = PathBuf::from("test.js");
1740 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::JavaScript);
1741
1742 let module_id = helper.add_module("app", None);
1743 let imported_id = helper.add_function("utils", None, false, false);
1744
1745 helper.add_import_edge_full(module_id, imported_id, Some("helpers"), false);
1747
1748 assert_eq!(helper.stats().edges_staged, 1);
1749
1750 let edges = staging.operations();
1752 let import_edge = edges.iter().find(|op| {
1753 matches!(
1754 op,
1755 StagingOp::AddEdge {
1756 kind: EdgeKind::Imports { .. },
1757 ..
1758 }
1759 )
1760 });
1761
1762 assert!(import_edge.is_some());
1763 if let StagingOp::AddEdge {
1764 kind: EdgeKind::Imports { alias, is_wildcard },
1765 ..
1766 } = import_edge.unwrap()
1767 {
1768 assert!(alias.is_some(), "Alias should be present");
1769 assert!(!*is_wildcard);
1770 }
1771 }
1772
1773 #[test]
1774 fn test_helper_add_import_edge_wildcard() {
1775 let mut staging = StagingGraph::new();
1776 let file = PathBuf::from("test.js");
1777 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::JavaScript);
1778
1779 let module_id = helper.add_module("app", None);
1780 let imported_id = helper.add_module("lodash", None);
1781
1782 helper.add_import_edge_full(module_id, imported_id, None, true);
1784
1785 let edges = staging.operations();
1786 let import_edge = edges.iter().find(|op| {
1787 matches!(
1788 op,
1789 StagingOp::AddEdge {
1790 kind: EdgeKind::Imports { .. },
1791 ..
1792 }
1793 )
1794 });
1795
1796 if let StagingOp::AddEdge {
1797 kind: EdgeKind::Imports { alias, is_wildcard },
1798 ..
1799 } = import_edge.unwrap()
1800 {
1801 assert!(alias.is_none());
1802 assert!(*is_wildcard);
1803 }
1804 }
1805
1806 #[test]
1807 fn test_helper_add_export_edge_full() {
1808 let mut staging = StagingGraph::new();
1809 let file = PathBuf::from("test.js");
1810 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::JavaScript);
1811
1812 let module_id = helper.add_module("app", None);
1813 let component_id = helper.add_class("MyComponent", None);
1814
1815 helper.add_export_edge_full(module_id, component_id, ExportKind::Default, None);
1817
1818 assert_eq!(helper.stats().edges_staged, 1);
1819
1820 let edges = staging.operations();
1821 let export_edge = edges.iter().find(|op| {
1822 matches!(
1823 op,
1824 StagingOp::AddEdge {
1825 kind: EdgeKind::Exports { .. },
1826 ..
1827 }
1828 )
1829 });
1830
1831 assert!(export_edge.is_some());
1832 if let StagingOp::AddEdge {
1833 kind: EdgeKind::Exports { kind, alias },
1834 ..
1835 } = export_edge.unwrap()
1836 {
1837 assert_eq!(*kind, ExportKind::Default);
1838 assert!(alias.is_none());
1839 }
1840 }
1841
1842 #[test]
1843 fn test_helper_add_export_edge_with_alias() {
1844 let mut staging = StagingGraph::new();
1845 let file = PathBuf::from("test.js");
1846 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::JavaScript);
1847
1848 let module_id = helper.add_module("app", None);
1849 let helper_fn_id = helper.add_function("internalHelper", None, false, false);
1850
1851 helper.add_export_edge_full(module_id, helper_fn_id, ExportKind::Direct, Some("helper"));
1853
1854 let edges = staging.operations();
1855 let export_edge = edges.iter().find(|op| {
1856 matches!(
1857 op,
1858 StagingOp::AddEdge {
1859 kind: EdgeKind::Exports { .. },
1860 ..
1861 }
1862 )
1863 });
1864
1865 if let StagingOp::AddEdge {
1866 kind: EdgeKind::Exports { kind, alias },
1867 ..
1868 } = export_edge.unwrap()
1869 {
1870 assert_eq!(*kind, ExportKind::Direct);
1871 assert!(alias.is_some(), "Alias should be present");
1872 }
1873 }
1874
1875 #[test]
1876 fn test_helper_add_export_edge_reexport() {
1877 let mut staging = StagingGraph::new();
1878 let file = PathBuf::from("index.js");
1879 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::JavaScript);
1880
1881 let module_id = helper.add_module("index", None);
1882 let utils_id = helper.add_module("utils", None);
1883
1884 helper.add_export_edge_full(module_id, utils_id, ExportKind::Namespace, Some("utils"));
1886
1887 let edges = staging.operations();
1888 let export_edge = edges.iter().find(|op| {
1889 matches!(
1890 op,
1891 StagingOp::AddEdge {
1892 kind: EdgeKind::Exports { .. },
1893 ..
1894 }
1895 )
1896 });
1897
1898 if let StagingOp::AddEdge {
1899 kind: EdgeKind::Exports { kind, alias },
1900 ..
1901 } = export_edge.unwrap()
1902 {
1903 assert_eq!(*kind, ExportKind::Namespace);
1904 assert!(alias.is_some());
1905 }
1906 }
1907
1908 #[test]
1909 fn test_helper_add_call_edge_full_with_span() {
1910 let mut staging = StagingGraph::new();
1911 let file = PathBuf::from("test.rs");
1912 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
1913
1914 let caller_id = helper.add_function("caller", None, false, false);
1915 let callee_id = helper.add_function("callee", None, false, false);
1916
1917 let span = Span {
1918 start: Position { line: 5, column: 4 },
1919 end: Position {
1920 line: 5,
1921 column: 20,
1922 },
1923 };
1924
1925 helper.add_call_edge_full_with_span(caller_id, callee_id, 2, false, vec![span]);
1926
1927 let edges = staging.operations();
1928 let call_edge = edges.iter().find(|op| {
1929 matches!(
1930 op,
1931 StagingOp::AddEdge {
1932 kind: EdgeKind::Calls { .. },
1933 ..
1934 }
1935 )
1936 });
1937
1938 if let StagingOp::AddEdge {
1939 kind:
1940 EdgeKind::Calls {
1941 argument_count,
1942 is_async,
1943 },
1944 spans: edge_spans,
1945 ..
1946 } = call_edge.unwrap()
1947 {
1948 assert_eq!(*argument_count, 2);
1949 assert!(!*is_async);
1950 assert!(!edge_spans.is_empty());
1951 }
1952 }
1953
1954 #[test]
1955 fn test_helper_add_function_with_async_attribute() {
1956 let mut staging = StagingGraph::new();
1957 let file = PathBuf::from("test.kt");
1958 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Kotlin);
1959
1960 let _func_id = helper.add_function("fetchData", None, true, false);
1962
1963 let ops = staging.operations();
1965 let add_node_op = ops
1966 .iter()
1967 .find(|op| matches!(op, StagingOp::AddNode { .. }));
1968
1969 assert!(add_node_op.is_some(), "Expected AddNode operation");
1970 if let StagingOp::AddNode { entry, .. } = add_node_op.unwrap() {
1971 assert!(
1972 entry.is_async,
1973 "Expected is_async=true for suspend function, got is_async=false"
1974 );
1975 }
1976 }
1977
1978 #[test]
1979 fn test_helper_add_method_with_static_attribute() {
1980 let mut staging = StagingGraph::new();
1981 let file = PathBuf::from("test.java");
1982 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Java);
1983
1984 let _method_id = helper.add_method("MyClass.staticMethod", None, false, true);
1986
1987 let ops = staging.operations();
1989 let add_node_op = ops
1990 .iter()
1991 .find(|op| matches!(op, StagingOp::AddNode { .. }));
1992
1993 assert!(add_node_op.is_some(), "Expected AddNode operation");
1994 if let StagingOp::AddNode { entry, .. } = add_node_op.unwrap() {
1995 assert!(
1996 entry.is_static,
1997 "Expected is_static=true for static method, got is_static=false"
1998 );
1999 }
2000 }
2001
2002 #[test]
2003 fn test_helper_add_function_without_attributes() {
2004 let mut staging = StagingGraph::new();
2005 let file = PathBuf::from("test.rs");
2006 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
2007
2008 let _func_id = helper.add_function("regular_function", None, false, false);
2010
2011 let ops = staging.operations();
2013 let add_node_op = ops
2014 .iter()
2015 .find(|op| matches!(op, StagingOp::AddNode { .. }));
2016
2017 assert!(add_node_op.is_some(), "Expected AddNode operation");
2018 if let StagingOp::AddNode { entry, .. } = add_node_op.unwrap() {
2019 assert!(
2020 !entry.is_async,
2021 "Expected is_async=false for regular function"
2022 );
2023 assert!(
2024 !entry.is_static,
2025 "Expected is_static=false for regular function"
2026 );
2027 }
2028 }
2029
2030 #[test]
2031 fn test_helper_add_method_with_both_attributes() {
2032 let mut staging = StagingGraph::new();
2033 let file = PathBuf::from("test.kt");
2034 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Kotlin);
2035
2036 let _method_id = helper.add_method("Service.asyncStaticMethod", None, true, true);
2038
2039 let ops = staging.operations();
2041 let add_node_op = ops
2042 .iter()
2043 .find(|op| matches!(op, StagingOp::AddNode { .. }));
2044
2045 assert!(add_node_op.is_some(), "Expected AddNode operation");
2046 if let StagingOp::AddNode { entry, .. } = add_node_op.unwrap() {
2047 assert!(entry.is_async, "Expected is_async=true for async method");
2048 assert!(entry.is_static, "Expected is_static=true for static method");
2049 }
2050 }
2051
2052 #[test]
2053 fn test_helper_add_function_with_unsafe_attribute() {
2054 let mut staging = StagingGraph::new();
2055 let file = PathBuf::from("test.rs");
2056 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
2057
2058 let _func_id = helper.add_function("unsafe_function", None, false, true);
2060
2061 let ops = staging.operations();
2063 let add_node_op = ops
2064 .iter()
2065 .find(|op| matches!(op, StagingOp::AddNode { .. }));
2066
2067 assert!(add_node_op.is_some(), "Expected AddNode operation");
2068 if let StagingOp::AddNode { entry, .. } = add_node_op.unwrap() {
2069 assert!(
2070 entry.is_unsafe,
2071 "Expected is_unsafe=true for unsafe function, got is_unsafe={}",
2072 entry.is_unsafe
2073 );
2074 }
2075 }
2076
2077 #[test]
2082 fn test_ensure_function_reuses_existing_method_node() {
2083 let mut staging = StagingGraph::new();
2084 let file = PathBuf::from("test.ts");
2085 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::TypeScript);
2086
2087 let span = Span::new(
2088 Position { line: 5, column: 4 },
2089 Position {
2090 line: 10,
2091 column: 5,
2092 },
2093 );
2094
2095 let method_id = helper.add_method("MyClass.doWork", Some(span), true, false);
2097
2098 let reused_id = helper.ensure_function("MyClass.doWork", None, true, false);
2100
2101 assert_eq!(
2102 method_id, reused_id,
2103 "ensure_function should reuse the existing Method node"
2104 );
2105 assert_eq!(
2106 helper.stats().nodes_created,
2107 1,
2108 "Only the Method node should exist"
2109 );
2110 }
2111
2112 #[test]
2113 fn test_ensure_method_reuses_existing_function_node() {
2114 let mut staging = StagingGraph::new();
2115 let file = PathBuf::from("test.ts");
2116 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::TypeScript);
2117
2118 let func_id = helper.add_function("standalone", None, false, false);
2119 let reused_id = helper.ensure_method("standalone", None, false, false);
2120
2121 assert_eq!(
2122 func_id, reused_id,
2123 "ensure_method should reuse the existing function node"
2124 );
2125 assert_eq!(helper.stats().nodes_created, 1);
2126 }
2127
2128 #[test]
2129 fn test_ensure_function_creates_new_when_no_method_exists() {
2130 let mut staging = StagingGraph::new();
2131 let file = PathBuf::from("test.ts");
2132 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::TypeScript);
2133
2134 let func_id = helper.ensure_function("topLevel", None, false, false);
2135 assert!(!func_id.is_invalid());
2136 assert_eq!(helper.stats().nodes_created, 1);
2137
2138 let func_id2 = helper.ensure_function("topLevel", None, false, false);
2139 assert_eq!(func_id, func_id2);
2140 assert_eq!(helper.stats().nodes_created, 1);
2141 }
2142
2143 #[test]
2144 fn test_no_method_function_duplicate_after_cross_kind_reuse() {
2145 let mut staging = StagingGraph::new();
2146 let file = PathBuf::from("browser-manager.ts");
2147 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::TypeScript);
2148
2149 let span_a = Span::new(
2150 Position { line: 3, column: 4 },
2151 Position { line: 8, column: 5 },
2152 );
2153 let span_b = Span::new(
2154 Position {
2155 line: 10,
2156 column: 4,
2157 },
2158 Position {
2159 line: 15,
2160 column: 5,
2161 },
2162 );
2163
2164 let _method_a = helper.add_method("BrowserManager.newTab", Some(span_a), true, false);
2166 let _method_b = helper.add_method("BrowserManager.restoreState", Some(span_b), true, false);
2167
2168 let _caller_a = helper.ensure_function("BrowserManager.newTab", None, true, false);
2170 let _caller_b = helper.ensure_function("BrowserManager.restoreState", None, true, false);
2171
2172 let ops = staging.operations();
2174 let mut method_names = std::collections::HashSet::new();
2175 let mut function_names = std::collections::HashSet::new();
2176
2177 for op in ops {
2178 if let StagingOp::AddNode { entry, .. } = op {
2179 if entry.kind == NodeKind::Method {
2180 method_names.insert(entry.name);
2181 } else if entry.kind == NodeKind::Function {
2182 function_names.insert(entry.name);
2183 }
2184 }
2185 }
2186
2187 let overlap: Vec<_> = method_names.intersection(&function_names).collect();
2188 assert!(
2189 overlap.is_empty(),
2190 "Found names that are both Method and Function: {overlap:?}"
2191 );
2192 }
2193
2194 #[test]
2199 fn test_ensure_function_reuses_existing_macro_node() {
2200 let mut staging = StagingGraph::new();
2201 let file = PathBuf::from("test.c");
2202 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::C);
2203
2204 let span = Span::new(
2205 Position { line: 1, column: 0 },
2206 Position {
2207 line: 1,
2208 column: 40,
2209 },
2210 );
2211
2212 let macro_id = helper.add_node("list_for_each_entry", Some(span), NodeKind::Macro);
2214
2215 let reused_id = helper.ensure_function("list_for_each_entry", None, false, false);
2217
2218 assert_eq!(
2219 macro_id, reused_id,
2220 "ensure_function should reuse the existing Macro node"
2221 );
2222 assert_eq!(helper.stats().nodes_created, 1);
2223 }
2224
2225 #[test]
2226 fn test_ensure_function_reuses_existing_constant_node() {
2227 let mut staging = StagingGraph::new();
2228 let file = PathBuf::from("test.c");
2229 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::C);
2230
2231 let span = Span::new(
2232 Position { line: 3, column: 0 },
2233 Position {
2234 line: 3,
2235 column: 30,
2236 },
2237 );
2238
2239 let const_id = helper.add_constant("handler_fn", Some(span));
2241
2242 let reused_id = helper.ensure_function("handler_fn", None, false, false);
2243
2244 assert_eq!(
2245 const_id, reused_id,
2246 "ensure_function should reuse the existing Constant node"
2247 );
2248 assert_eq!(helper.stats().nodes_created, 1);
2249 }
2250
2251 #[test]
2252 fn test_ensure_method_reuses_existing_lambda_target_node() {
2253 let mut staging = StagingGraph::new();
2254 let file = PathBuf::from("test.java");
2255 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Java);
2256
2257 let span = Span::new(
2258 Position { line: 7, column: 8 },
2259 Position {
2260 line: 10,
2261 column: 9,
2262 },
2263 );
2264
2265 let lambda_id = helper.add_node("Comparator.compare", Some(span), NodeKind::LambdaTarget);
2266
2267 let reused_id = helper.ensure_method("Comparator.compare", None, false, false);
2268
2269 assert_eq!(
2270 lambda_id, reused_id,
2271 "ensure_method should reuse the existing LambdaTarget node"
2272 );
2273 assert_eq!(helper.stats().nodes_created, 1);
2274 }
2275
2276 #[test]
2277 fn test_cross_kind_reuse_does_not_merge_incompatible_kinds() {
2278 let mut staging = StagingGraph::new();
2279 let file = PathBuf::from("test.css");
2280 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Css);
2281
2282 let style_id =
2284 helper.add_node_verbatim(".container", None, NodeKind::StyleRule, &[], None, None);
2285
2286 let func_id = helper.ensure_function(".container", None, false, false);
2288
2289 assert_ne!(
2290 style_id, func_id,
2291 "ensure_function must NOT merge into a StyleRule"
2292 );
2293 assert_eq!(helper.stats().nodes_created, 2);
2294 }
2295
2296 #[test]
2303 fn test_stub_first_ensure_function_then_add_method_reuses() {
2304 let mut staging = StagingGraph::new();
2305 let file = PathBuf::from("test.ts");
2306 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::TypeScript);
2307
2308 let stub_id = helper.ensure_function("Widget.render", None, false, false);
2310
2311 let span = Span::new(
2313 Position {
2314 line: 10,
2315 column: 4,
2316 },
2317 Position {
2318 line: 20,
2319 column: 5,
2320 },
2321 );
2322 let decl_id = helper.add_method("Widget.render", Some(span), false, false);
2323
2324 assert!(!stub_id.is_invalid());
2329 assert!(!decl_id.is_invalid());
2330 }
2333
2334 #[test]
2335 fn test_stub_first_ensure_method_then_add_function_reuses() {
2336 let mut staging = StagingGraph::new();
2337 let file = PathBuf::from("test.py");
2338 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Python);
2339
2340 let stub_id = helper.ensure_method("process_data", None, false, false);
2342
2343 let span = Span::new(
2345 Position { line: 5, column: 0 },
2346 Position {
2347 line: 15,
2348 column: 0,
2349 },
2350 );
2351 let decl_id = helper.add_function("process_data", Some(span), false, false);
2352
2353 assert!(!stub_id.is_invalid());
2354 assert!(!decl_id.is_invalid());
2355 }
2356
2357 #[test]
2358 fn test_ensure_callee_then_add_function_same_name_no_panic() {
2359 let mut staging = StagingGraph::new();
2360 let file = PathBuf::from("test.c");
2361 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::C);
2362
2363 let call_span = Span::new(
2364 Position {
2365 line: 50,
2366 column: 4,
2367 },
2368 Position {
2369 line: 50,
2370 column: 20,
2371 },
2372 );
2373 let callee_id = helper.ensure_callee("kfree", call_span, CalleeKindHint::Function);
2374
2375 let def_span = Span::new(
2376 Position { line: 1, column: 0 },
2377 Position {
2378 line: 10,
2379 column: 1,
2380 },
2381 );
2382 let def_id = helper.add_function("kfree", Some(def_span), false, false);
2383
2384 assert_eq!(
2387 callee_id, def_id,
2388 "add_function should reuse the node created by ensure_callee"
2389 );
2390 assert_eq!(helper.stats().nodes_created, 1);
2391 }
2392
2393 #[test]
2398 fn test_ensure_callee_function_hint_creates_with_span() {
2399 let mut staging = StagingGraph::new();
2400 let file = PathBuf::from("test.rs");
2401 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
2402
2403 let call_span = Span::new(
2404 Position {
2405 line: 20,
2406 column: 4,
2407 },
2408 Position {
2409 line: 20,
2410 column: 30,
2411 },
2412 );
2413
2414 let id = helper.ensure_callee("target_fn", call_span, CalleeKindHint::Function);
2415 assert!(!id.is_invalid());
2416
2417 let ops = staging.operations();
2419 let node_op = ops
2420 .iter()
2421 .find(|op| matches!(op, StagingOp::AddNode { .. }));
2422 if let Some(StagingOp::AddNode { entry, .. }) = node_op {
2423 assert!(
2424 entry.start_line > 0,
2425 "ensure_callee must produce nodes with line > 0"
2426 );
2427 }
2428 }
2429
2430 #[test]
2431 fn test_ensure_callee_macro_hint_reuses_existing_macro() {
2432 let mut staging = StagingGraph::new();
2433 let file = PathBuf::from("test.c");
2434 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::C);
2435
2436 let def_span = Span::new(
2437 Position { line: 5, column: 0 },
2438 Position {
2439 line: 5,
2440 column: 40,
2441 },
2442 );
2443 let call_span = Span::new(
2444 Position {
2445 line: 99,
2446 column: 4,
2447 },
2448 Position {
2449 line: 99,
2450 column: 30,
2451 },
2452 );
2453
2454 let macro_id = helper.add_node("IS_ERR", Some(def_span), NodeKind::Macro);
2455 let reused_id = helper.ensure_callee("IS_ERR", call_span, CalleeKindHint::Macro);
2456
2457 assert_eq!(
2458 macro_id, reused_id,
2459 "ensure_callee should reuse existing Macro node"
2460 );
2461 assert_eq!(helper.stats().nodes_created, 1);
2462 }
2463
2464 #[test]
2465 fn test_ensure_callee_idempotent_returns_first_spans_node() {
2466 let mut staging = StagingGraph::new();
2467 let file = PathBuf::from("test.rs");
2468 let mut helper = GraphBuildHelper::new(&mut staging, &file, Language::Rust);
2469
2470 let span1 = Span::new(
2471 Position {
2472 line: 10,
2473 column: 0,
2474 },
2475 Position {
2476 line: 10,
2477 column: 20,
2478 },
2479 );
2480 let span2 = Span::new(
2481 Position {
2482 line: 50,
2483 column: 0,
2484 },
2485 Position {
2486 line: 50,
2487 column: 20,
2488 },
2489 );
2490
2491 let id1 = helper.ensure_callee("func", span1, CalleeKindHint::Function);
2492 let id2 = helper.ensure_callee("func", span2, CalleeKindHint::Function);
2493
2494 assert_eq!(
2495 id1, id2,
2496 "Two ensure_callee calls for the same name return the same NodeId"
2497 );
2498 }
2499
2500 #[test]
2501 fn test_call_compatible_kinds_dry_no_body_changes_needed() {
2502 assert!(CALL_COMPATIBLE_KINDS.contains(&NodeKind::Function));
2508 assert!(CALL_COMPATIBLE_KINDS.contains(&NodeKind::Method));
2509 assert!(CALL_COMPATIBLE_KINDS.contains(&NodeKind::Macro));
2510 assert!(CALL_COMPATIBLE_KINDS.contains(&NodeKind::Constant));
2511 assert!(CALL_COMPATIBLE_KINDS.contains(&NodeKind::LambdaTarget));
2512 assert_eq!(CALL_COMPATIBLE_KINDS.len(), 5);
2513 }
2514}