1use std::collections::{HashMap, HashSet, VecDeque};
8use std::ops::ControlFlow;
9
10use super::concurrent::GraphSnapshot;
11use super::edge::kind::EdgeKind;
12use super::edge::store::StoreEdgeRef;
13use super::materialize::materialize_node;
14use super::node::id::NodeId;
15use super::traversal::{
16 EdgeClassification, MaterializedEdge, TraversalMetadata, TraversalResult, TruncationReason,
17};
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum TraversalDirection {
24 Outgoing,
26 Incoming,
28 Both,
30}
31
32#[derive(Debug, Clone)]
37#[allow(clippy::struct_excessive_bools)] pub struct EdgeFilter {
39 pub include_calls: bool,
41 pub include_imports: bool,
43 pub include_references: bool,
45 pub include_inheritance: bool,
47 pub include_structural: bool,
49 pub include_type_edges: bool,
51 pub include_database: bool,
53 pub include_service: bool,
55}
56
57impl EdgeFilter {
58 #[must_use]
60 pub const fn all() -> Self {
61 Self {
62 include_calls: true,
63 include_imports: true,
64 include_references: true,
65 include_inheritance: true,
66 include_structural: true,
67 include_type_edges: true,
68 include_database: true,
69 include_service: true,
70 }
71 }
72
73 #[must_use]
75 pub const fn calls_only() -> Self {
76 Self {
77 include_calls: true,
78 include_imports: false,
79 include_references: false,
80 include_inheritance: false,
81 include_structural: false,
82 include_type_edges: false,
83 include_database: false,
84 include_service: false,
85 }
86 }
87
88 #[must_use]
90 pub const fn calls_and_imports() -> Self {
91 Self {
92 include_calls: true,
93 include_imports: true,
94 include_references: false,
95 include_inheritance: false,
96 include_structural: false,
97 include_type_edges: false,
98 include_database: false,
99 include_service: false,
100 }
101 }
102
103 #[must_use]
105 pub const fn dependency_edges() -> Self {
106 Self {
107 include_calls: true,
108 include_imports: true,
109 include_references: true,
110 include_inheritance: true,
111 include_structural: false,
112 include_type_edges: false,
113 include_database: false,
114 include_service: false,
115 }
116 }
117
118 #[must_use]
120 pub fn accepts(&self, classification: &EdgeClassification) -> bool {
121 match classification {
122 EdgeClassification::Call { .. } => self.include_calls,
123 EdgeClassification::Import { .. } | EdgeClassification::Export { .. } => {
124 self.include_imports
125 }
126 EdgeClassification::Reference => self.include_references,
127 EdgeClassification::Inherits | EdgeClassification::Implements => {
128 self.include_inheritance
129 }
130 EdgeClassification::Contains | EdgeClassification::Defines => self.include_structural,
131 EdgeClassification::TypeOf => self.include_type_edges,
132 EdgeClassification::DatabaseAccess => self.include_database,
133 EdgeClassification::ServiceInteraction => self.include_service,
134 }
135 }
136}
137
138#[derive(Debug, Clone)]
140pub struct TraversalLimits {
141 pub max_depth: u32,
143 pub max_nodes: Option<usize>,
145 pub max_edges: Option<usize>,
147 pub max_paths: Option<usize>,
149}
150
151impl TraversalLimits {
152 #[must_use]
154 pub const fn default_subgraph() -> Self {
155 Self {
156 max_depth: 2,
157 max_nodes: Some(50),
158 max_edges: None,
159 max_paths: None,
160 }
161 }
162
163 #[must_use]
165 pub const fn default_export() -> Self {
166 Self {
167 max_depth: 2,
168 max_nodes: None,
169 max_edges: Some(1000),
170 max_paths: None,
171 }
172 }
173
174 #[must_use]
176 pub const fn default_impact() -> Self {
177 Self {
178 max_depth: 3,
179 max_nodes: Some(500),
180 max_edges: None,
181 max_paths: None,
182 }
183 }
184
185 #[must_use]
187 pub const fn default_trace() -> Self {
188 Self {
189 max_depth: 5,
190 max_nodes: None,
191 max_edges: None,
192 max_paths: Some(5),
193 }
194 }
195}
196
197#[derive(Debug, Clone)]
199pub struct TraversalConfig {
200 pub direction: TraversalDirection,
202 pub edge_filter: EdgeFilter,
204 pub limits: TraversalLimits,
206}
207
208#[derive(Debug, Clone, Copy, PartialEq, Eq)]
212pub enum FrontierMode {
213 Standard,
215 PathEnumeration,
217}
218
219#[derive(Debug, Clone, Copy, PartialEq, Eq)]
221pub enum VisitedPolicy {
222 Global,
224 PathLocal,
227}
228
229pub trait TraversalStrategy {
233 fn accept_edge(&mut self, _edge: &StoreEdgeRef, _depth: u32) -> bool {
235 true
236 }
237
238 fn should_enqueue(
240 &mut self,
241 _node_id: NodeId,
242 _from: NodeId,
243 _edge: &EdgeKind,
244 _depth: u32,
245 ) -> bool {
246 true
247 }
248
249 fn on_path_complete(&mut self, _path: &[NodeId]) -> ControlFlow<()> {
252 ControlFlow::Continue(())
253 }
254
255 fn frontier_mode(&self) -> FrontierMode {
257 FrontierMode::Standard
258 }
259
260 fn visited_policy(&self) -> VisitedPolicy {
262 VisitedPolicy::Global
263 }
264
265 fn path_target(&self) -> Option<NodeId> {
267 None
268 }
269}
270
271struct DefaultStrategy;
273impl TraversalStrategy for DefaultStrategy {}
274
275#[must_use]
289pub fn is_followable_edge(kind: &EdgeKind, min_confidence: f64) -> bool {
290 let confidence = match kind {
291 EdgeKind::Calls { .. } | EdgeKind::TraitMethodBinding { .. } => 1.0,
292 EdgeKind::Inherits | EdgeKind::Implements | EdgeKind::SealedPermit => 0.9,
293 EdgeKind::Imports { .. } | EdgeKind::Exports { .. } => 0.8,
294 EdgeKind::References => 0.7,
295 EdgeKind::FfiCall { .. }
296 | EdgeKind::HttpRequest { .. }
297 | EdgeKind::GrpcCall { .. }
298 | EdgeKind::WebAssemblyCall => 0.6,
299 EdgeKind::MacroExpansion { .. } => 0.5,
300 _ => 0.3,
301 };
302 confidence >= min_confidence
303}
304
305pub struct SimplePathStrategy {
312 target: NodeId,
314 min_confidence: f64,
316 allow_cross_language: bool,
318}
319
320impl SimplePathStrategy {
321 #[must_use]
323 pub fn new(target: NodeId, min_confidence: f64, allow_cross_language: bool) -> Self {
324 Self {
325 target,
326 min_confidence,
327 allow_cross_language,
328 }
329 }
330}
331
332impl TraversalStrategy for SimplePathStrategy {
333 fn accept_edge(&mut self, edge: &StoreEdgeRef, _depth: u32) -> bool {
334 is_followable_edge(&edge.kind, self.min_confidence)
335 && (self.allow_cross_language || !edge.kind.is_cross_boundary())
336 }
337
338 fn frontier_mode(&self) -> FrontierMode {
339 FrontierMode::PathEnumeration
340 }
341
342 fn visited_policy(&self) -> VisitedPolicy {
343 VisitedPolicy::PathLocal
344 }
345
346 fn path_target(&self) -> Option<NodeId> {
347 Some(self.target)
348 }
349}
350
351pub struct SccPathStrategy<'a> {
356 scc_data: &'a super::analysis::scc::SccData,
358 cond_dag: &'a super::analysis::condensation::CondensationDag,
360 target: NodeId,
362 target_scc: Option<u32>,
364 min_confidence: f64,
366 allow_cross_language: bool,
368}
369
370impl<'a> SccPathStrategy<'a> {
371 #[must_use]
373 pub fn new(
374 scc_data: &'a super::analysis::scc::SccData,
375 cond_dag: &'a super::analysis::condensation::CondensationDag,
376 target: NodeId,
377 min_confidence: f64,
378 allow_cross_language: bool,
379 ) -> Self {
380 let target_scc = scc_data.scc_of(target);
381 Self {
382 scc_data,
383 cond_dag,
384 target,
385 target_scc,
386 min_confidence,
387 allow_cross_language,
388 }
389 }
390}
391
392impl TraversalStrategy for SccPathStrategy<'_> {
393 fn accept_edge(&mut self, edge: &StoreEdgeRef, _depth: u32) -> bool {
394 is_followable_edge(&edge.kind, self.min_confidence)
395 && (self.allow_cross_language || !edge.kind.is_cross_boundary())
396 }
397
398 fn should_enqueue(
399 &mut self,
400 node_id: NodeId,
401 _from: NodeId,
402 _edge: &EdgeKind,
403 _depth: u32,
404 ) -> bool {
405 let Some(target_scc) = self.target_scc else {
407 return true;
408 };
409
410 let Some(node_scc) = self.scc_data.scc_of(node_id) else {
412 return true;
413 };
414
415 self.cond_dag.can_reach(node_scc, target_scc)
416 }
417
418 fn on_path_complete(&mut self, _path: &[NodeId]) -> ControlFlow<()> {
419 ControlFlow::Continue(())
420 }
421
422 fn frontier_mode(&self) -> FrontierMode {
423 FrontierMode::PathEnumeration
424 }
425
426 fn visited_policy(&self) -> VisitedPolicy {
427 VisitedPolicy::PathLocal
428 }
429
430 fn path_target(&self) -> Option<NodeId> {
431 Some(self.target)
432 }
433}
434
435#[must_use]
442pub fn traverse(
443 snapshot: &GraphSnapshot,
444 seeds: &[NodeId],
445 config: &TraversalConfig,
446 strategy: Option<&mut dyn TraversalStrategy>,
447) -> TraversalResult {
448 let mut default_strategy = DefaultStrategy;
449 let strategy = strategy.unwrap_or(&mut default_strategy);
450
451 let frontier_mode = strategy.frontier_mode();
452 let visited_policy = strategy.visited_policy();
453
454 match (frontier_mode, visited_policy) {
455 (FrontierMode::Standard, _) => {
456 run_standard_bfs(snapshot, seeds, config, strategy)
458 }
459 (FrontierMode::PathEnumeration, VisitedPolicy::PathLocal) => {
460 run_path_bfs(snapshot, seeds, config, strategy)
461 }
462 (FrontierMode::PathEnumeration, VisitedPolicy::Global) => {
463 run_path_bfs(snapshot, seeds, config, strategy)
465 }
466 }
467}
468
469struct RawEdge {
473 source: NodeId,
474 target: NodeId,
475 kind: EdgeKind,
476 depth: u32,
477}
478
479fn run_standard_bfs(
481 snapshot: &GraphSnapshot,
482 seeds: &[NodeId],
483 config: &TraversalConfig,
484 strategy: &mut dyn TraversalStrategy,
485) -> TraversalResult {
486 let mut visited: HashSet<NodeId> = HashSet::new();
487 let mut queue: VecDeque<(NodeId, u32)> = VecDeque::new();
488 let mut raw_edges: Vec<RawEdge> = Vec::new();
489 let mut discovered_order: Vec<NodeId> = Vec::new();
490 let mut truncation: Option<TruncationReason> = None;
491 let mut max_depth_reached = false;
492 let mut nodes_visited: usize = 0;
493
494 for &seed in seeds {
496 if visited.insert(seed) {
497 discovered_order.push(seed);
498 queue.push_back((seed, 0));
499 }
500 }
501
502 'bfs: while let Some((current, depth)) = queue.pop_front() {
503 nodes_visited += 1;
504
505 if depth >= config.limits.max_depth {
506 max_depth_reached = true;
507 continue;
508 }
509
510 if let Some(max_nodes) = config.limits.max_nodes
512 && discovered_order.len() >= max_nodes
513 {
514 truncation = Some(TruncationReason::NodeLimit);
515 break;
516 }
517
518 let edges = collect_edges(snapshot, current, config.direction);
519
520 for edge_ref in &edges {
521 if let Some(max_edges) = config.limits.max_edges
523 && raw_edges.len() >= max_edges
524 {
525 truncation = Some(TruncationReason::EdgeLimit);
526 break 'bfs;
527 }
528
529 let classification = EdgeClassification::from(&edge_ref.kind);
530 if !config.edge_filter.accepts(&classification) {
531 continue;
532 }
533
534 if !strategy.accept_edge(edge_ref, depth) {
535 continue;
536 }
537
538 let next = neighbor_of(edge_ref, current);
540
541 raw_edges.push(RawEdge {
542 source: edge_ref.source,
543 target: edge_ref.target,
544 kind: edge_ref.kind.clone(),
545 depth: depth + 1,
546 });
547
548 if visited.insert(next)
549 && strategy.should_enqueue(next, current, &edge_ref.kind, depth + 1)
550 {
551 if let Some(max_nodes) = config.limits.max_nodes
553 && discovered_order.len() >= max_nodes
554 {
555 truncation = Some(TruncationReason::NodeLimit);
556 break 'bfs;
557 }
558 discovered_order.push(next);
559 queue.push_back((next, depth + 1));
560 }
561 }
562 }
563
564 materialize_result(
565 snapshot,
566 &discovered_order,
567 &raw_edges,
568 None,
569 truncation,
570 max_depth_reached,
571 seeds.len(),
572 nodes_visited,
573 )
574}
575
576#[allow(clippy::too_many_lines)] fn run_path_bfs(
581 snapshot: &GraphSnapshot,
582 seeds: &[NodeId],
583 config: &TraversalConfig,
584 strategy: &mut dyn TraversalStrategy,
585) -> TraversalResult {
586 let target = strategy.path_target();
587 let mut collected_paths: Vec<Vec<NodeId>> = Vec::new();
588 let mut discovered_order: Vec<NodeId> = Vec::new();
589 let mut seen: HashSet<NodeId> = HashSet::new();
590 let mut raw_edges: Vec<RawEdge> = Vec::new();
591 let mut truncation: Option<TruncationReason> = None;
592 let mut max_depth_reached = false;
593 let mut nodes_visited: usize = 0;
594
595 let mut queue: VecDeque<(NodeId, Vec<NodeId>, u32)> = VecDeque::new();
597
598 for &seed in seeds {
599 queue.push_back((seed, vec![seed], 0));
600 if seen.insert(seed) {
601 discovered_order.push(seed);
602 }
603 }
604
605 let use_global_visited = strategy.visited_policy() == VisitedPolicy::Global;
606 let mut global_visited: HashSet<NodeId> = if use_global_visited {
607 seeds.iter().copied().collect()
608 } else {
609 HashSet::new()
610 };
611
612 'bfs: while let Some((current, path, depth)) = queue.pop_front() {
613 nodes_visited += 1;
614
615 if let Some(t) = target
617 && current == t
618 && path.len() > 1
619 {
620 for &node in &path {
622 if seen.insert(node) {
623 discovered_order.push(node);
624 }
625 }
626
627 let control = strategy.on_path_complete(&path);
628 collected_paths.push(path);
629
630 if let Some(max_paths) = config.limits.max_paths
632 && collected_paths.len() >= max_paths
633 {
634 truncation = Some(TruncationReason::PathLimit);
635 break 'bfs;
636 }
637
638 if control.is_break() {
639 break 'bfs;
640 }
641 continue;
642 }
643
644 if depth >= config.limits.max_depth {
645 max_depth_reached = true;
646 continue;
647 }
648
649 if let Some(max_nodes) = config.limits.max_nodes
651 && discovered_order.len() >= max_nodes
652 {
653 truncation = Some(TruncationReason::NodeLimit);
654 break;
655 }
656
657 let edges = collect_edges(snapshot, current, config.direction);
658
659 let mut has_followable_successor = false;
661
662 for edge_ref in &edges {
663 if let Some(max_edges) = config.limits.max_edges
665 && raw_edges.len() >= max_edges
666 {
667 truncation = Some(TruncationReason::EdgeLimit);
668 break 'bfs;
669 }
670
671 let classification = EdgeClassification::from(&edge_ref.kind);
672 if !config.edge_filter.accepts(&classification) {
673 continue;
674 }
675
676 if !strategy.accept_edge(edge_ref, depth) {
677 continue;
678 }
679
680 let next = neighbor_of(edge_ref, current);
681
682 if use_global_visited {
684 if !global_visited.insert(next) {
685 continue;
686 }
687 } else if path.contains(&next) {
688 continue;
689 }
690
691 if !strategy.should_enqueue(next, current, &edge_ref.kind, depth + 1) {
692 continue;
693 }
694
695 has_followable_successor = true;
696
697 raw_edges.push(RawEdge {
698 source: edge_ref.source,
699 target: edge_ref.target,
700 kind: edge_ref.kind.clone(),
701 depth: depth + 1,
702 });
703
704 if let Some(max_nodes) = config.limits.max_nodes
706 && discovered_order.len() >= max_nodes
707 {
708 truncation = Some(TruncationReason::NodeLimit);
709 break 'bfs;
710 }
711
712 if seen.insert(next) {
713 discovered_order.push(next);
714 }
715
716 let mut new_path = path.clone();
717 new_path.push(next);
718 queue.push_back((next, new_path, depth + 1));
719 }
720
721 if target.is_none() && !has_followable_successor && path.len() > 1 {
723 for &node in &path {
724 if seen.insert(node) {
725 discovered_order.push(node);
726 }
727 }
728
729 let control = strategy.on_path_complete(&path);
730 collected_paths.push(path);
731
732 if let Some(max_paths) = config.limits.max_paths
733 && collected_paths.len() >= max_paths
734 {
735 truncation = Some(TruncationReason::PathLimit);
736 break 'bfs;
737 }
738
739 if control.is_break() {
740 break 'bfs;
741 }
742 }
743 }
744
745 let paths_for_result = Some(collected_paths);
746
747 materialize_result(
748 snapshot,
749 &discovered_order,
750 &raw_edges,
751 paths_for_result,
752 truncation,
753 max_depth_reached,
754 seeds.len(),
755 nodes_visited,
756 )
757}
758
759fn collect_edges(
763 snapshot: &GraphSnapshot,
764 node: NodeId,
765 direction: TraversalDirection,
766) -> Vec<StoreEdgeRef> {
767 match direction {
768 TraversalDirection::Outgoing => snapshot.edges().edges_from(node),
769 TraversalDirection::Incoming => snapshot.edges().edges_to(node),
770 TraversalDirection::Both => {
771 let mut edges = snapshot.edges().edges_from(node);
772 edges.extend(snapshot.edges().edges_to(node));
773 edges
774 }
775 }
776}
777
778fn neighbor_of(edge_ref: &StoreEdgeRef, current: NodeId) -> NodeId {
780 if edge_ref.source == current {
781 edge_ref.target
782 } else {
783 edge_ref.source
784 }
785}
786
787#[allow(clippy::too_many_arguments)]
792fn materialize_result(
793 snapshot: &GraphSnapshot,
794 discovered_order: &[NodeId],
795 raw_edges: &[RawEdge],
796 raw_paths: Option<Vec<Vec<NodeId>>>,
797 truncation: Option<TruncationReason>,
798 max_depth_reached: bool,
799 seed_count: usize,
800 nodes_visited: usize,
801) -> TraversalResult {
802 let mut nodes = Vec::with_capacity(discovered_order.len());
804 let mut node_index: HashMap<NodeId, usize> = HashMap::with_capacity(discovered_order.len());
805
806 for &node_id in discovered_order {
807 if node_index.contains_key(&node_id) {
808 continue;
809 }
810 if let Some(materialized) = materialize_node(snapshot, node_id) {
811 let idx = nodes.len();
812 node_index.insert(node_id, idx);
813 nodes.push(materialized);
814 }
815 }
816
817 let mut edges = Vec::with_capacity(raw_edges.len());
819 for raw in raw_edges {
820 if let (Some(&source_idx), Some(&target_idx)) =
821 (node_index.get(&raw.source), node_index.get(&raw.target))
822 {
823 edges.push(MaterializedEdge {
824 source_idx,
825 target_idx,
826 classification: EdgeClassification::from(&raw.kind),
827 raw_kind: raw.kind.clone(),
828 depth: raw.depth,
829 });
830 }
831 }
832
833 edges.dedup_by(|a, b| {
835 a.source_idx == b.source_idx
836 && a.target_idx == b.target_idx
837 && a.classification == b.classification
838 });
839
840 let paths = raw_paths.map(|path_list| {
842 path_list
843 .iter()
844 .filter_map(|path| {
845 let converted: Vec<usize> = path
846 .iter()
847 .filter_map(|node_id| node_index.get(node_id).copied())
848 .collect();
849 if converted.len() == path.len() {
851 Some(converted)
852 } else {
853 None
854 }
855 })
856 .collect()
857 });
858
859 TraversalResult {
860 metadata: TraversalMetadata {
861 truncation,
862 max_depth_reached,
863 seed_count,
864 nodes_visited,
865 total_nodes: nodes.len(),
866 total_edges: edges.len(),
867 },
868 nodes,
869 edges,
870 paths,
871 }
872}
873
874#[cfg(test)]
875mod tests {
876 use super::*;
877 use crate::graph::node::Language;
878 use crate::graph::unified::concurrent::CodeGraph;
879 use crate::graph::unified::node::kind::NodeKind;
880 use crate::graph::unified::storage::arena::NodeEntry;
881
882 use crate::graph::unified::file::FileId;
883
884 struct TestGraph {
886 graph: CodeGraph,
887 file_id: Option<FileId>,
888 }
889
890 impl TestGraph {
891 fn new() -> Self {
892 Self {
893 graph: CodeGraph::new(),
894 file_id: None,
895 }
896 }
897
898 fn ensure_file_id(&mut self) -> FileId {
899 if let Some(fid) = self.file_id {
900 return fid;
901 }
902 let file_path = std::path::PathBuf::from("/kernel-tests/test.rs");
903 let fid = self
904 .graph
905 .files_mut()
906 .register_with_language(&file_path, Some(Language::Rust))
907 .unwrap();
908 self.file_id = Some(fid);
909 fid
910 }
911
912 fn add_node(&mut self, name: &str) -> NodeId {
913 self.add_node_with_kind(name, NodeKind::Function)
914 }
915
916 fn add_node_with_kind(&mut self, name: &str, kind: NodeKind) -> NodeId {
917 let file_id = self.ensure_file_id();
918 let name_id = self.graph.strings_mut().intern(name).unwrap();
919 let qn_id = self
920 .graph
921 .strings_mut()
922 .intern(&format!("test::{name}"))
923 .unwrap();
924
925 let entry = NodeEntry::new(kind, name_id, file_id)
926 .with_qualified_name(qn_id)
927 .with_location(1, 0, 10, 0);
928
929 let node_id = self.graph.nodes_mut().alloc(entry).unwrap();
930 self.graph
931 .indices_mut()
932 .add(node_id, kind, name_id, Some(qn_id), file_id);
933 node_id
934 }
935
936 fn add_call_edge(&mut self, source: NodeId, target: NodeId) {
937 let file_id = self.ensure_file_id();
938 self.graph.edges_mut().add_edge(
939 source,
940 target,
941 EdgeKind::Calls {
942 argument_count: 0,
943 is_async: false,
944 },
945 file_id,
946 );
947 }
948
949 fn add_edge(&mut self, source: NodeId, target: NodeId, kind: EdgeKind) {
950 let file_id = self.ensure_file_id();
951 self.graph
952 .edges_mut()
953 .add_edge(source, target, kind, file_id);
954 }
955
956 fn snapshot(&self) -> GraphSnapshot {
957 self.graph.snapshot()
958 }
959 }
960
961 fn calls_config(depth: u32) -> TraversalConfig {
962 TraversalConfig {
963 direction: TraversalDirection::Outgoing,
964 edge_filter: EdgeFilter::calls_only(),
965 limits: TraversalLimits {
966 max_depth: depth,
967 max_nodes: None,
968 max_edges: None,
969 max_paths: None,
970 },
971 }
972 }
973
974 #[test]
975 fn standard_outgoing_bfs() {
976 let mut tg = TestGraph::new();
977 let a = tg.add_node("a");
978 let b = tg.add_node("b");
979 let c = tg.add_node("c");
980 tg.add_call_edge(a, b);
981 tg.add_call_edge(b, c);
982
983 let snapshot = tg.snapshot();
984 let result = traverse(&snapshot, &[a], &calls_config(3), None);
985
986 assert_eq!(result.nodes.len(), 3);
987 assert_eq!(result.edges.len(), 2);
988 assert!(result.metadata.truncation.is_none());
989 assert_eq!(result.nodes[0].node_id, a);
991 }
992
993 #[test]
994 fn depth_limit() {
995 let mut tg = TestGraph::new();
996 let a = tg.add_node("a");
997 let b = tg.add_node("b");
998 let c = tg.add_node("c");
999 tg.add_call_edge(a, b);
1000 tg.add_call_edge(b, c);
1001
1002 let snapshot = tg.snapshot();
1003 let result = traverse(&snapshot, &[a], &calls_config(1), None);
1004
1005 assert_eq!(result.nodes.len(), 2);
1007 assert_eq!(result.edges.len(), 1);
1008 assert!(result.metadata.max_depth_reached);
1009 }
1010
1011 #[test]
1012 fn node_limit_truncation() {
1013 let mut tg = TestGraph::new();
1014 let a = tg.add_node("a");
1015 let b = tg.add_node("b");
1016 let c = tg.add_node("c");
1017 let d = tg.add_node("d");
1018 tg.add_call_edge(a, b);
1019 tg.add_call_edge(a, c);
1020 tg.add_call_edge(a, d);
1021
1022 let snapshot = tg.snapshot();
1023 let config = TraversalConfig {
1024 direction: TraversalDirection::Outgoing,
1025 edge_filter: EdgeFilter::calls_only(),
1026 limits: TraversalLimits {
1027 max_depth: 5,
1028 max_nodes: Some(2),
1029 max_edges: None,
1030 max_paths: None,
1031 },
1032 };
1033
1034 let result = traverse(&snapshot, &[a], &config, None);
1035
1036 assert_eq!(
1037 result.metadata.truncation,
1038 Some(TruncationReason::NodeLimit)
1039 );
1040 }
1041
1042 #[test]
1043 fn empty_seeds() {
1044 let tg = TestGraph::new();
1045 let snapshot = tg.snapshot();
1046 let result = traverse(&snapshot, &[], &calls_config(3), None);
1047
1048 assert!(result.nodes.is_empty());
1049 assert!(result.edges.is_empty());
1050 assert!(result.metadata.truncation.is_none());
1051 assert_eq!(result.metadata.seed_count, 0);
1052 }
1053
1054 #[test]
1055 fn incoming_bfs() {
1056 let mut tg = TestGraph::new();
1057 let a = tg.add_node("a");
1058 let b = tg.add_node("b");
1059 let c = tg.add_node("c");
1060 tg.add_call_edge(a, b);
1061 tg.add_call_edge(c, b);
1062
1063 let snapshot = tg.snapshot();
1064 let config = TraversalConfig {
1065 direction: TraversalDirection::Incoming,
1066 edge_filter: EdgeFilter::calls_only(),
1067 limits: TraversalLimits {
1068 max_depth: 3,
1069 max_nodes: None,
1070 max_edges: None,
1071 max_paths: None,
1072 },
1073 };
1074
1075 let result = traverse(&snapshot, &[b], &config, None);
1076
1077 assert_eq!(result.nodes.len(), 3);
1079 }
1080
1081 #[test]
1082 fn bidirectional_bfs() {
1083 let mut tg = TestGraph::new();
1084 let a = tg.add_node("a");
1085 let b = tg.add_node("b");
1086 let c = tg.add_node("c");
1087 tg.add_call_edge(a, b);
1088 tg.add_call_edge(b, c);
1089
1090 let snapshot = tg.snapshot();
1091 let config = TraversalConfig {
1092 direction: TraversalDirection::Both,
1093 edge_filter: EdgeFilter::calls_only(),
1094 limits: TraversalLimits {
1095 max_depth: 3,
1096 max_nodes: None,
1097 max_edges: None,
1098 max_paths: None,
1099 },
1100 };
1101
1102 let result = traverse(&snapshot, &[b], &config, None);
1104 assert_eq!(result.nodes.len(), 3);
1105 }
1106
1107 #[test]
1108 fn edge_filtering() {
1109 let mut tg = TestGraph::new();
1110 let a = tg.add_node("a");
1111 let b = tg.add_node("b");
1112 let c = tg.add_node("c");
1113 tg.add_call_edge(a, b);
1114 tg.add_edge(
1115 a,
1116 c,
1117 EdgeKind::Imports {
1118 alias: None,
1119 is_wildcard: false,
1120 },
1121 );
1122
1123 let snapshot = tg.snapshot();
1124
1125 let result = traverse(&snapshot, &[a], &calls_config(3), None);
1127 assert_eq!(result.nodes.len(), 2); let config = TraversalConfig {
1131 direction: TraversalDirection::Outgoing,
1132 edge_filter: EdgeFilter::calls_and_imports(),
1133 limits: TraversalLimits {
1134 max_depth: 3,
1135 max_nodes: None,
1136 max_edges: None,
1137 max_paths: None,
1138 },
1139 };
1140 let result = traverse(&snapshot, &[a], &config, None);
1141 assert_eq!(result.nodes.len(), 3); }
1143
1144 #[test]
1145 fn cycle_handling_standard() {
1146 let mut tg = TestGraph::new();
1147 let a = tg.add_node("a");
1148 let b = tg.add_node("b");
1149 tg.add_call_edge(a, b);
1150 tg.add_call_edge(b, a);
1151
1152 let snapshot = tg.snapshot();
1153 let result = traverse(&snapshot, &[a], &calls_config(10), None);
1154
1155 assert_eq!(result.nodes.len(), 2);
1157 }
1158
1159 #[test]
1160 fn edge_limit_truncation() {
1161 let mut tg = TestGraph::new();
1162 let a = tg.add_node("a");
1163 let b = tg.add_node("b");
1164 let c = tg.add_node("c");
1165 let d = tg.add_node("d");
1166 tg.add_call_edge(a, b);
1167 tg.add_call_edge(a, c);
1168 tg.add_call_edge(a, d);
1169
1170 let snapshot = tg.snapshot();
1171 let config = TraversalConfig {
1172 direction: TraversalDirection::Outgoing,
1173 edge_filter: EdgeFilter::calls_only(),
1174 limits: TraversalLimits {
1175 max_depth: 5,
1176 max_nodes: None,
1177 max_edges: Some(2),
1178 max_paths: None,
1179 },
1180 };
1181
1182 let result = traverse(&snapshot, &[a], &config, None);
1183 assert_eq!(
1184 result.metadata.truncation,
1185 Some(TruncationReason::EdgeLimit)
1186 );
1187 }
1188
1189 #[test]
1190 fn index_invariants_hold() {
1191 let mut tg = TestGraph::new();
1192 let a = tg.add_node("a");
1193 let b = tg.add_node("b");
1194 let c = tg.add_node("c");
1195 tg.add_call_edge(a, b);
1196 tg.add_call_edge(b, c);
1197
1198 let snapshot = tg.snapshot();
1199 let result = traverse(&snapshot, &[a], &calls_config(5), None);
1200
1201 for edge in &result.edges {
1203 assert!(edge.source_idx < result.nodes.len());
1204 assert!(edge.target_idx < result.nodes.len());
1205 }
1206
1207 assert_eq!(result.metadata.total_nodes, result.nodes.len());
1209 assert_eq!(result.metadata.total_edges, result.edges.len());
1210 }
1211
1212 #[test]
1213 fn all_filter_includes_structural() {
1214 let mut tg = TestGraph::new();
1215 let a = tg.add_node("a");
1216 let b = tg.add_node("b");
1217 tg.add_edge(a, b, EdgeKind::Defines);
1218
1219 let snapshot = tg.snapshot();
1220 let config = TraversalConfig {
1221 direction: TraversalDirection::Outgoing,
1222 edge_filter: EdgeFilter::all(),
1223 limits: TraversalLimits {
1224 max_depth: 3,
1225 max_nodes: None,
1226 max_edges: None,
1227 max_paths: None,
1228 },
1229 };
1230 let result = traverse(&snapshot, &[a], &config, None);
1231 assert_eq!(result.nodes.len(), 2);
1232 assert_eq!(result.edges.len(), 1);
1233 assert_eq!(result.edges[0].classification, EdgeClassification::Defines);
1234 }
1235
1236 #[test]
1239 fn path_enumeration_finds_path() {
1240 let mut tg = TestGraph::new();
1241 let a = tg.add_node("pa");
1242 let b = tg.add_node("pb");
1243 let c = tg.add_node("pc");
1244 tg.add_call_edge(a, b);
1245 tg.add_call_edge(b, c);
1246
1247 let snapshot = tg.snapshot();
1248 let config = TraversalConfig {
1249 direction: TraversalDirection::Outgoing,
1250 edge_filter: EdgeFilter::calls_only(),
1251 limits: TraversalLimits {
1252 max_depth: 5,
1253 max_nodes: None,
1254 max_edges: None,
1255 max_paths: Some(10),
1256 },
1257 };
1258
1259 let mut strategy = SimplePathStrategy::new(c, 0.0, true);
1260 let result = traverse(&snapshot, &[a], &config, Some(&mut strategy));
1261
1262 assert!(result.paths.is_some());
1263 let paths = result.paths.as_ref().unwrap();
1264 assert_eq!(paths.len(), 1, "expected 1 path from a→c");
1265 assert_eq!(paths[0].len(), 3);
1267 }
1268
1269 #[test]
1270 fn path_local_allows_shared_nodes_in_diamond() {
1271 let mut tg = TestGraph::new();
1273 let a = tg.add_node("da");
1274 let b = tg.add_node("db");
1275 let c = tg.add_node("dc");
1276 let d = tg.add_node("dd");
1277 tg.add_call_edge(a, b);
1278 tg.add_call_edge(a, c);
1279 tg.add_call_edge(b, d);
1280 tg.add_call_edge(c, d);
1281
1282 let snapshot = tg.snapshot();
1283 let config = TraversalConfig {
1284 direction: TraversalDirection::Outgoing,
1285 edge_filter: EdgeFilter::calls_only(),
1286 limits: TraversalLimits {
1287 max_depth: 5,
1288 max_nodes: None,
1289 max_edges: None,
1290 max_paths: Some(10),
1291 },
1292 };
1293
1294 let mut strategy = SimplePathStrategy::new(d, 0.0, true);
1295 let result = traverse(&snapshot, &[a], &config, Some(&mut strategy));
1296
1297 let paths = result.paths.as_ref().unwrap();
1298 assert_eq!(paths.len(), 2, "expected 2 paths in diamond, got {paths:?}");
1300 }
1301
1302 #[test]
1303 fn path_enumeration_handles_cycles() {
1304 let mut tg = TestGraph::new();
1306 let a = tg.add_node("ca");
1307 let b = tg.add_node("cb");
1308 let c = tg.add_node("cc");
1309 tg.add_call_edge(a, b);
1310 tg.add_call_edge(b, c);
1311 tg.add_call_edge(c, a); let snapshot = tg.snapshot();
1314 let config = TraversalConfig {
1315 direction: TraversalDirection::Outgoing,
1316 edge_filter: EdgeFilter::calls_only(),
1317 limits: TraversalLimits {
1318 max_depth: 10,
1319 max_nodes: None,
1320 max_edges: None,
1321 max_paths: Some(10),
1322 },
1323 };
1324
1325 let mut strategy = SimplePathStrategy::new(c, 0.0, true);
1326 let result = traverse(&snapshot, &[a], &config, Some(&mut strategy));
1327
1328 let paths = result.paths.as_ref().unwrap();
1329 assert_eq!(paths.len(), 1);
1331 assert_eq!(paths[0].len(), 3);
1332 }
1333
1334 #[test]
1335 fn path_enumeration_no_path() {
1336 let mut tg = TestGraph::new();
1338 let a = tg.add_node("na");
1339 let b = tg.add_node("nb");
1340 let c = tg.add_node("nc");
1341 tg.add_call_edge(a, b);
1342
1343 let snapshot = tg.snapshot();
1344 let config = TraversalConfig {
1345 direction: TraversalDirection::Outgoing,
1346 edge_filter: EdgeFilter::calls_only(),
1347 limits: TraversalLimits {
1348 max_depth: 5,
1349 max_nodes: None,
1350 max_edges: None,
1351 max_paths: Some(10),
1352 },
1353 };
1354
1355 let mut strategy = SimplePathStrategy::new(c, 0.0, true);
1356 let result = traverse(&snapshot, &[a], &config, Some(&mut strategy));
1357
1358 let paths = result.paths.as_ref().unwrap();
1359 assert!(paths.is_empty(), "expected no paths, got {paths:?}");
1360 }
1361
1362 #[test]
1363 fn path_limit_truncation() {
1364 let mut tg = TestGraph::new();
1366 let a = tg.add_node("la");
1367 let b1 = tg.add_node("lb1");
1368 let b2 = tg.add_node("lb2");
1369 let b3 = tg.add_node("lb3");
1370 let c = tg.add_node("lc");
1371 tg.add_call_edge(a, b1);
1372 tg.add_call_edge(a, b2);
1373 tg.add_call_edge(a, b3);
1374 tg.add_call_edge(b1, c);
1375 tg.add_call_edge(b2, c);
1376 tg.add_call_edge(b3, c);
1377
1378 let snapshot = tg.snapshot();
1379 let config = TraversalConfig {
1380 direction: TraversalDirection::Outgoing,
1381 edge_filter: EdgeFilter::calls_only(),
1382 limits: TraversalLimits {
1383 max_depth: 5,
1384 max_nodes: None,
1385 max_edges: None,
1386 max_paths: Some(2),
1387 },
1388 };
1389
1390 let mut strategy = SimplePathStrategy::new(c, 0.0, true);
1391 let result = traverse(&snapshot, &[a], &config, Some(&mut strategy));
1392
1393 let paths = result.paths.as_ref().unwrap();
1394 assert_eq!(paths.len(), 2, "expected exactly 2 paths (limited)");
1395 assert_eq!(
1396 result.metadata.truncation,
1397 Some(TruncationReason::PathLimit)
1398 );
1399 }
1400
1401 #[test]
1402 fn path_bfs_node_limit_truncation() {
1403 let mut tg = TestGraph::new();
1405 let a = tg.add_node("pna");
1406 let b = tg.add_node("pnb");
1407 let c = tg.add_node("pnc");
1408 let d = tg.add_node("pnd");
1409 tg.add_call_edge(a, b);
1410 tg.add_call_edge(b, c);
1411 tg.add_call_edge(c, d);
1412
1413 let snapshot = tg.snapshot();
1414 let config = TraversalConfig {
1415 direction: TraversalDirection::Outgoing,
1416 edge_filter: EdgeFilter::calls_only(),
1417 limits: TraversalLimits {
1418 max_depth: 10,
1419 max_nodes: Some(2),
1420 max_edges: None,
1421 max_paths: Some(10),
1422 },
1423 };
1424
1425 let mut strategy = SimplePathStrategy::new(d, 0.0, true);
1426 let result = traverse(&snapshot, &[a], &config, Some(&mut strategy));
1427
1428 assert!(
1429 result.nodes.len() <= 2,
1430 "node limit violated: {} nodes",
1431 result.nodes.len()
1432 );
1433 assert_eq!(
1434 result.metadata.truncation,
1435 Some(TruncationReason::NodeLimit)
1436 );
1437 }
1438
1439 #[test]
1440 fn path_bfs_edge_limit_truncation() {
1441 let mut tg = TestGraph::new();
1443 let a = tg.add_node("pea");
1444 let b = tg.add_node("peb");
1445 let c = tg.add_node("pec");
1446 let d = tg.add_node("ped");
1447 tg.add_call_edge(a, b);
1448 tg.add_call_edge(b, c);
1449 tg.add_call_edge(c, d);
1450
1451 let snapshot = tg.snapshot();
1452 let config = TraversalConfig {
1453 direction: TraversalDirection::Outgoing,
1454 edge_filter: EdgeFilter::calls_only(),
1455 limits: TraversalLimits {
1456 max_depth: 10,
1457 max_nodes: None,
1458 max_edges: Some(1),
1459 max_paths: Some(10),
1460 },
1461 };
1462
1463 let mut strategy = SimplePathStrategy::new(d, 0.0, true);
1464 let result = traverse(&snapshot, &[a], &config, Some(&mut strategy));
1465
1466 assert_eq!(
1467 result.metadata.truncation,
1468 Some(TruncationReason::EdgeLimit)
1469 );
1470 }
1471
1472 #[test]
1473 fn path_bfs_leaf_enumeration_no_target() {
1474 let mut tg = TestGraph::new();
1476 let a = tg.add_node("la");
1477 let b = tg.add_node("lb");
1478 let c = tg.add_node("lc");
1479 let d = tg.add_node("ld");
1480 tg.add_call_edge(a, b);
1481 tg.add_call_edge(b, c);
1482 tg.add_call_edge(a, d);
1483
1484 let snapshot = tg.snapshot();
1485 let config = TraversalConfig {
1486 direction: TraversalDirection::Outgoing,
1487 edge_filter: EdgeFilter::calls_only(),
1488 limits: TraversalLimits {
1489 max_depth: 10,
1490 max_nodes: None,
1491 max_edges: None,
1492 max_paths: Some(10),
1493 },
1494 };
1495
1496 let mut strategy = LeafPathStrategy;
1498 let result = traverse(&snapshot, &[a], &config, Some(&mut strategy));
1499
1500 let paths = result.paths.as_ref().unwrap();
1501 assert!(
1503 paths.len() >= 2,
1504 "expected at least 2 leaf paths, got {}",
1505 paths.len()
1506 );
1507 }
1508
1509 struct LeafPathStrategy;
1511
1512 impl TraversalStrategy for LeafPathStrategy {
1513 fn frontier_mode(&self) -> FrontierMode {
1514 FrontierMode::PathEnumeration
1515 }
1516
1517 fn visited_policy(&self) -> VisitedPolicy {
1518 VisitedPolicy::PathLocal
1519 }
1520
1521 fn path_target(&self) -> Option<NodeId> {
1522 None
1523 }
1524 }
1525
1526 #[test]
1527 fn is_followable_edge_confidence_filtering() {
1528 let calls = EdgeKind::Calls {
1530 argument_count: 0,
1531 is_async: false,
1532 };
1533 assert!(is_followable_edge(&calls, 0.0));
1534 assert!(is_followable_edge(&calls, 1.0));
1535
1536 let ffi = EdgeKind::FfiCall {
1538 convention: crate::graph::unified::edge::kind::FfiConvention::C,
1539 };
1540 assert!(is_followable_edge(&ffi, 0.5));
1541 assert!(is_followable_edge(&ffi, 0.6));
1542 assert!(!is_followable_edge(&ffi, 0.7));
1543
1544 assert!(is_followable_edge(&EdgeKind::Defines, 0.3));
1546 assert!(!is_followable_edge(&EdgeKind::Defines, 0.4));
1547 }
1548}