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)] #[allow(clippy::too_many_lines)] fn run_path_bfs(
582 snapshot: &GraphSnapshot,
583 seeds: &[NodeId],
584 config: &TraversalConfig,
585 strategy: &mut dyn TraversalStrategy,
586) -> TraversalResult {
587 let target = strategy.path_target();
588 let mut collected_paths: Vec<Vec<NodeId>> = Vec::new();
589 let mut discovered_order: Vec<NodeId> = Vec::new();
590 let mut seen: HashSet<NodeId> = HashSet::new();
591 let mut raw_edges: Vec<RawEdge> = Vec::new();
592 let mut truncation: Option<TruncationReason> = None;
593 let mut max_depth_reached = false;
594 let mut nodes_visited: usize = 0;
595
596 let mut queue: VecDeque<(NodeId, Vec<NodeId>, u32)> = VecDeque::new();
598
599 for &seed in seeds {
600 queue.push_back((seed, vec![seed], 0));
601 if seen.insert(seed) {
602 discovered_order.push(seed);
603 }
604 }
605
606 let use_global_visited = strategy.visited_policy() == VisitedPolicy::Global;
607 let mut global_visited: HashSet<NodeId> = if use_global_visited {
608 seeds.iter().copied().collect()
609 } else {
610 HashSet::new()
611 };
612
613 'bfs: while let Some((current, path, depth)) = queue.pop_front() {
614 nodes_visited += 1;
615
616 if let Some(t) = target
618 && current == t
619 && path.len() > 1
620 {
621 for &node in &path {
623 if seen.insert(node) {
624 discovered_order.push(node);
625 }
626 }
627
628 let control = strategy.on_path_complete(&path);
629 collected_paths.push(path);
630
631 if let Some(max_paths) = config.limits.max_paths
633 && collected_paths.len() >= max_paths
634 {
635 truncation = Some(TruncationReason::PathLimit);
636 break 'bfs;
637 }
638
639 if control.is_break() {
640 break 'bfs;
641 }
642 continue;
643 }
644
645 if depth >= config.limits.max_depth {
646 max_depth_reached = true;
647 continue;
648 }
649
650 if let Some(max_nodes) = config.limits.max_nodes
652 && discovered_order.len() >= max_nodes
653 {
654 truncation = Some(TruncationReason::NodeLimit);
655 break;
656 }
657
658 let edges = collect_edges(snapshot, current, config.direction);
659
660 let mut has_followable_successor = false;
662
663 for edge_ref in &edges {
664 if let Some(max_edges) = config.limits.max_edges
666 && raw_edges.len() >= max_edges
667 {
668 truncation = Some(TruncationReason::EdgeLimit);
669 break 'bfs;
670 }
671
672 let classification = EdgeClassification::from(&edge_ref.kind);
673 if !config.edge_filter.accepts(&classification) {
674 continue;
675 }
676
677 if !strategy.accept_edge(edge_ref, depth) {
678 continue;
679 }
680
681 let next = neighbor_of(edge_ref, current);
682
683 if use_global_visited {
685 if !global_visited.insert(next) {
686 continue;
687 }
688 } else if path.contains(&next) {
689 continue;
690 }
691
692 if !strategy.should_enqueue(next, current, &edge_ref.kind, depth + 1) {
693 continue;
694 }
695
696 has_followable_successor = true;
697
698 raw_edges.push(RawEdge {
699 source: edge_ref.source,
700 target: edge_ref.target,
701 kind: edge_ref.kind.clone(),
702 depth: depth + 1,
703 });
704
705 if let Some(max_nodes) = config.limits.max_nodes
707 && discovered_order.len() >= max_nodes
708 {
709 truncation = Some(TruncationReason::NodeLimit);
710 break 'bfs;
711 }
712
713 if seen.insert(next) {
714 discovered_order.push(next);
715 }
716
717 let mut new_path = path.clone();
718 new_path.push(next);
719 queue.push_back((next, new_path, depth + 1));
720 }
721
722 if target.is_none() && !has_followable_successor && path.len() > 1 {
724 for &node in &path {
725 if seen.insert(node) {
726 discovered_order.push(node);
727 }
728 }
729
730 let control = strategy.on_path_complete(&path);
731 collected_paths.push(path);
732
733 if let Some(max_paths) = config.limits.max_paths
734 && collected_paths.len() >= max_paths
735 {
736 truncation = Some(TruncationReason::PathLimit);
737 break 'bfs;
738 }
739
740 if control.is_break() {
741 break 'bfs;
742 }
743 }
744 }
745
746 let paths_for_result = Some(collected_paths);
747
748 materialize_result(
749 snapshot,
750 &discovered_order,
751 &raw_edges,
752 paths_for_result,
753 truncation,
754 max_depth_reached,
755 seeds.len(),
756 nodes_visited,
757 )
758}
759
760fn collect_edges(
764 snapshot: &GraphSnapshot,
765 node: NodeId,
766 direction: TraversalDirection,
767) -> Vec<StoreEdgeRef> {
768 match direction {
769 TraversalDirection::Outgoing => snapshot.edges().edges_from(node),
770 TraversalDirection::Incoming => snapshot.edges().edges_to(node),
771 TraversalDirection::Both => {
772 let mut edges = snapshot.edges().edges_from(node);
773 edges.extend(snapshot.edges().edges_to(node));
774 edges
775 }
776 }
777}
778
779fn neighbor_of(edge_ref: &StoreEdgeRef, current: NodeId) -> NodeId {
781 if edge_ref.source == current {
782 edge_ref.target
783 } else {
784 edge_ref.source
785 }
786}
787
788#[allow(clippy::too_many_arguments)]
793fn materialize_result(
794 snapshot: &GraphSnapshot,
795 discovered_order: &[NodeId],
796 raw_edges: &[RawEdge],
797 raw_paths: Option<Vec<Vec<NodeId>>>,
798 truncation: Option<TruncationReason>,
799 max_depth_reached: bool,
800 seed_count: usize,
801 nodes_visited: usize,
802) -> TraversalResult {
803 let mut nodes = Vec::with_capacity(discovered_order.len());
805 let mut node_index: HashMap<NodeId, usize> = HashMap::with_capacity(discovered_order.len());
806
807 for &node_id in discovered_order {
808 if node_index.contains_key(&node_id) {
809 continue;
810 }
811 if let Some(materialized) = materialize_node(snapshot, node_id) {
812 let idx = nodes.len();
813 node_index.insert(node_id, idx);
814 nodes.push(materialized);
815 }
816 }
817
818 let mut edges = Vec::with_capacity(raw_edges.len());
820 for raw in raw_edges {
821 if let (Some(&source_idx), Some(&target_idx)) =
822 (node_index.get(&raw.source), node_index.get(&raw.target))
823 {
824 edges.push(MaterializedEdge {
825 source_idx,
826 target_idx,
827 classification: EdgeClassification::from(&raw.kind),
828 raw_kind: raw.kind.clone(),
829 depth: raw.depth,
830 });
831 }
832 }
833
834 edges.dedup_by(|a, b| {
836 a.source_idx == b.source_idx
837 && a.target_idx == b.target_idx
838 && a.classification == b.classification
839 });
840
841 let paths = raw_paths.map(|path_list| {
843 path_list
844 .iter()
845 .filter_map(|path| {
846 let converted: Vec<usize> = path
847 .iter()
848 .filter_map(|node_id| node_index.get(node_id).copied())
849 .collect();
850 if converted.len() == path.len() {
852 Some(converted)
853 } else {
854 None
855 }
856 })
857 .collect()
858 });
859
860 TraversalResult {
861 metadata: TraversalMetadata {
862 truncation,
863 max_depth_reached,
864 seed_count,
865 nodes_visited,
866 total_nodes: nodes.len(),
867 total_edges: edges.len(),
868 },
869 nodes,
870 edges,
871 paths,
872 }
873}
874
875#[cfg(test)]
876mod tests {
877 use super::*;
878 use crate::graph::node::Language;
879 use crate::graph::unified::concurrent::CodeGraph;
880 use crate::graph::unified::node::kind::NodeKind;
881 use crate::graph::unified::storage::arena::NodeEntry;
882
883 use crate::graph::unified::file::FileId;
884
885 struct TestGraph {
887 graph: CodeGraph,
888 file_id: Option<FileId>,
889 }
890
891 impl TestGraph {
892 fn new() -> Self {
893 Self {
894 graph: CodeGraph::new(),
895 file_id: None,
896 }
897 }
898
899 fn ensure_file_id(&mut self) -> FileId {
900 if let Some(fid) = self.file_id {
901 return fid;
902 }
903 let file_path = std::path::PathBuf::from("/kernel-tests/test.rs");
904 let fid = self
905 .graph
906 .files_mut()
907 .register_with_language(&file_path, Some(Language::Rust))
908 .unwrap();
909 self.file_id = Some(fid);
910 fid
911 }
912
913 fn add_node(&mut self, name: &str) -> NodeId {
914 self.add_node_with_kind(name, NodeKind::Function)
915 }
916
917 fn add_node_with_kind(&mut self, name: &str, kind: NodeKind) -> NodeId {
918 let file_id = self.ensure_file_id();
919 let name_id = self.graph.strings_mut().intern(name).unwrap();
920 let qn_id = self
921 .graph
922 .strings_mut()
923 .intern(&format!("test::{name}"))
924 .unwrap();
925
926 let entry = NodeEntry::new(kind, name_id, file_id)
927 .with_qualified_name(qn_id)
928 .with_location(1, 0, 10, 0);
929
930 let node_id = self.graph.nodes_mut().alloc(entry).unwrap();
931 self.graph
932 .indices_mut()
933 .add(node_id, kind, name_id, Some(qn_id), file_id);
934 node_id
935 }
936
937 fn add_call_edge(&mut self, source: NodeId, target: NodeId) {
938 let file_id = self.ensure_file_id();
939 self.graph.edges_mut().add_edge(
940 source,
941 target,
942 EdgeKind::Calls {
943 argument_count: 0,
944 is_async: false,
945 },
946 file_id,
947 );
948 }
949
950 fn add_edge(&mut self, source: NodeId, target: NodeId, kind: EdgeKind) {
951 let file_id = self.ensure_file_id();
952 self.graph
953 .edges_mut()
954 .add_edge(source, target, kind, file_id);
955 }
956
957 fn snapshot(&self) -> GraphSnapshot {
958 self.graph.snapshot()
959 }
960 }
961
962 fn calls_config(depth: u32) -> TraversalConfig {
963 TraversalConfig {
964 direction: TraversalDirection::Outgoing,
965 edge_filter: EdgeFilter::calls_only(),
966 limits: TraversalLimits {
967 max_depth: depth,
968 max_nodes: None,
969 max_edges: None,
970 max_paths: None,
971 },
972 }
973 }
974
975 #[test]
976 fn standard_outgoing_bfs() {
977 let mut tg = TestGraph::new();
978 let a = tg.add_node("a");
979 let b = tg.add_node("b");
980 let c = tg.add_node("c");
981 tg.add_call_edge(a, b);
982 tg.add_call_edge(b, c);
983
984 let snapshot = tg.snapshot();
985 let result = traverse(&snapshot, &[a], &calls_config(3), None);
986
987 assert_eq!(result.nodes.len(), 3);
988 assert_eq!(result.edges.len(), 2);
989 assert!(result.metadata.truncation.is_none());
990 assert_eq!(result.nodes[0].node_id, a);
992 }
993
994 #[test]
995 fn depth_limit() {
996 let mut tg = TestGraph::new();
997 let a = tg.add_node("a");
998 let b = tg.add_node("b");
999 let c = tg.add_node("c");
1000 tg.add_call_edge(a, b);
1001 tg.add_call_edge(b, c);
1002
1003 let snapshot = tg.snapshot();
1004 let result = traverse(&snapshot, &[a], &calls_config(1), None);
1005
1006 assert_eq!(result.nodes.len(), 2);
1008 assert_eq!(result.edges.len(), 1);
1009 assert!(result.metadata.max_depth_reached);
1010 }
1011
1012 #[test]
1013 fn node_limit_truncation() {
1014 let mut tg = TestGraph::new();
1015 let a = tg.add_node("a");
1016 let b = tg.add_node("b");
1017 let c = tg.add_node("c");
1018 let d = tg.add_node("d");
1019 tg.add_call_edge(a, b);
1020 tg.add_call_edge(a, c);
1021 tg.add_call_edge(a, d);
1022
1023 let snapshot = tg.snapshot();
1024 let config = TraversalConfig {
1025 direction: TraversalDirection::Outgoing,
1026 edge_filter: EdgeFilter::calls_only(),
1027 limits: TraversalLimits {
1028 max_depth: 5,
1029 max_nodes: Some(2),
1030 max_edges: None,
1031 max_paths: None,
1032 },
1033 };
1034
1035 let result = traverse(&snapshot, &[a], &config, None);
1036
1037 assert_eq!(
1038 result.metadata.truncation,
1039 Some(TruncationReason::NodeLimit)
1040 );
1041 }
1042
1043 #[test]
1044 fn empty_seeds() {
1045 let tg = TestGraph::new();
1046 let snapshot = tg.snapshot();
1047 let result = traverse(&snapshot, &[], &calls_config(3), None);
1048
1049 assert!(result.nodes.is_empty());
1050 assert!(result.edges.is_empty());
1051 assert!(result.metadata.truncation.is_none());
1052 assert_eq!(result.metadata.seed_count, 0);
1053 }
1054
1055 #[test]
1056 fn incoming_bfs() {
1057 let mut tg = TestGraph::new();
1058 let a = tg.add_node("a");
1059 let b = tg.add_node("b");
1060 let c = tg.add_node("c");
1061 tg.add_call_edge(a, b);
1062 tg.add_call_edge(c, b);
1063
1064 let snapshot = tg.snapshot();
1065 let config = TraversalConfig {
1066 direction: TraversalDirection::Incoming,
1067 edge_filter: EdgeFilter::calls_only(),
1068 limits: TraversalLimits {
1069 max_depth: 3,
1070 max_nodes: None,
1071 max_edges: None,
1072 max_paths: None,
1073 },
1074 };
1075
1076 let result = traverse(&snapshot, &[b], &config, None);
1077
1078 assert_eq!(result.nodes.len(), 3);
1080 }
1081
1082 #[test]
1083 fn bidirectional_bfs() {
1084 let mut tg = TestGraph::new();
1085 let a = tg.add_node("a");
1086 let b = tg.add_node("b");
1087 let c = tg.add_node("c");
1088 tg.add_call_edge(a, b);
1089 tg.add_call_edge(b, c);
1090
1091 let snapshot = tg.snapshot();
1092 let config = TraversalConfig {
1093 direction: TraversalDirection::Both,
1094 edge_filter: EdgeFilter::calls_only(),
1095 limits: TraversalLimits {
1096 max_depth: 3,
1097 max_nodes: None,
1098 max_edges: None,
1099 max_paths: None,
1100 },
1101 };
1102
1103 let result = traverse(&snapshot, &[b], &config, None);
1105 assert_eq!(result.nodes.len(), 3);
1106 }
1107
1108 #[test]
1109 fn edge_filtering() {
1110 let mut tg = TestGraph::new();
1111 let a = tg.add_node("a");
1112 let b = tg.add_node("b");
1113 let c = tg.add_node("c");
1114 tg.add_call_edge(a, b);
1115 tg.add_edge(
1116 a,
1117 c,
1118 EdgeKind::Imports {
1119 alias: None,
1120 is_wildcard: false,
1121 },
1122 );
1123
1124 let snapshot = tg.snapshot();
1125
1126 let result = traverse(&snapshot, &[a], &calls_config(3), None);
1128 assert_eq!(result.nodes.len(), 2); let config = TraversalConfig {
1132 direction: TraversalDirection::Outgoing,
1133 edge_filter: EdgeFilter::calls_and_imports(),
1134 limits: TraversalLimits {
1135 max_depth: 3,
1136 max_nodes: None,
1137 max_edges: None,
1138 max_paths: None,
1139 },
1140 };
1141 let result = traverse(&snapshot, &[a], &config, None);
1142 assert_eq!(result.nodes.len(), 3); }
1144
1145 #[test]
1146 fn cycle_handling_standard() {
1147 let mut tg = TestGraph::new();
1148 let a = tg.add_node("a");
1149 let b = tg.add_node("b");
1150 tg.add_call_edge(a, b);
1151 tg.add_call_edge(b, a);
1152
1153 let snapshot = tg.snapshot();
1154 let result = traverse(&snapshot, &[a], &calls_config(10), None);
1155
1156 assert_eq!(result.nodes.len(), 2);
1158 }
1159
1160 #[test]
1161 fn edge_limit_truncation() {
1162 let mut tg = TestGraph::new();
1163 let a = tg.add_node("a");
1164 let b = tg.add_node("b");
1165 let c = tg.add_node("c");
1166 let d = tg.add_node("d");
1167 tg.add_call_edge(a, b);
1168 tg.add_call_edge(a, c);
1169 tg.add_call_edge(a, d);
1170
1171 let snapshot = tg.snapshot();
1172 let config = TraversalConfig {
1173 direction: TraversalDirection::Outgoing,
1174 edge_filter: EdgeFilter::calls_only(),
1175 limits: TraversalLimits {
1176 max_depth: 5,
1177 max_nodes: None,
1178 max_edges: Some(2),
1179 max_paths: None,
1180 },
1181 };
1182
1183 let result = traverse(&snapshot, &[a], &config, None);
1184 assert_eq!(
1185 result.metadata.truncation,
1186 Some(TruncationReason::EdgeLimit)
1187 );
1188 }
1189
1190 #[test]
1191 fn index_invariants_hold() {
1192 let mut tg = TestGraph::new();
1193 let a = tg.add_node("a");
1194 let b = tg.add_node("b");
1195 let c = tg.add_node("c");
1196 tg.add_call_edge(a, b);
1197 tg.add_call_edge(b, c);
1198
1199 let snapshot = tg.snapshot();
1200 let result = traverse(&snapshot, &[a], &calls_config(5), None);
1201
1202 for edge in &result.edges {
1204 assert!(edge.source_idx < result.nodes.len());
1205 assert!(edge.target_idx < result.nodes.len());
1206 }
1207
1208 assert_eq!(result.metadata.total_nodes, result.nodes.len());
1210 assert_eq!(result.metadata.total_edges, result.edges.len());
1211 }
1212
1213 #[test]
1214 fn all_filter_includes_structural() {
1215 let mut tg = TestGraph::new();
1216 let a = tg.add_node("a");
1217 let b = tg.add_node("b");
1218 tg.add_edge(a, b, EdgeKind::Defines);
1219
1220 let snapshot = tg.snapshot();
1221 let config = TraversalConfig {
1222 direction: TraversalDirection::Outgoing,
1223 edge_filter: EdgeFilter::all(),
1224 limits: TraversalLimits {
1225 max_depth: 3,
1226 max_nodes: None,
1227 max_edges: None,
1228 max_paths: None,
1229 },
1230 };
1231 let result = traverse(&snapshot, &[a], &config, None);
1232 assert_eq!(result.nodes.len(), 2);
1233 assert_eq!(result.edges.len(), 1);
1234 assert_eq!(result.edges[0].classification, EdgeClassification::Defines);
1235 }
1236
1237 #[test]
1240 fn path_enumeration_finds_path() {
1241 let mut tg = TestGraph::new();
1242 let a = tg.add_node("pa");
1243 let b = tg.add_node("pb");
1244 let c = tg.add_node("pc");
1245 tg.add_call_edge(a, b);
1246 tg.add_call_edge(b, c);
1247
1248 let snapshot = tg.snapshot();
1249 let config = TraversalConfig {
1250 direction: TraversalDirection::Outgoing,
1251 edge_filter: EdgeFilter::calls_only(),
1252 limits: TraversalLimits {
1253 max_depth: 5,
1254 max_nodes: None,
1255 max_edges: None,
1256 max_paths: Some(10),
1257 },
1258 };
1259
1260 let mut strategy = SimplePathStrategy::new(c, 0.0, true);
1261 let result = traverse(&snapshot, &[a], &config, Some(&mut strategy));
1262
1263 assert!(result.paths.is_some());
1264 let paths = result.paths.as_ref().unwrap();
1265 assert_eq!(paths.len(), 1, "expected 1 path from a→c");
1266 assert_eq!(paths[0].len(), 3);
1268 }
1269
1270 #[test]
1271 fn path_local_allows_shared_nodes_in_diamond() {
1272 let mut tg = TestGraph::new();
1274 let a = tg.add_node("da");
1275 let b = tg.add_node("db");
1276 let c = tg.add_node("dc");
1277 let d = tg.add_node("dd");
1278 tg.add_call_edge(a, b);
1279 tg.add_call_edge(a, c);
1280 tg.add_call_edge(b, d);
1281 tg.add_call_edge(c, d);
1282
1283 let snapshot = tg.snapshot();
1284 let config = TraversalConfig {
1285 direction: TraversalDirection::Outgoing,
1286 edge_filter: EdgeFilter::calls_only(),
1287 limits: TraversalLimits {
1288 max_depth: 5,
1289 max_nodes: None,
1290 max_edges: None,
1291 max_paths: Some(10),
1292 },
1293 };
1294
1295 let mut strategy = SimplePathStrategy::new(d, 0.0, true);
1296 let result = traverse(&snapshot, &[a], &config, Some(&mut strategy));
1297
1298 let paths = result.paths.as_ref().unwrap();
1299 assert_eq!(paths.len(), 2, "expected 2 paths in diamond, got {paths:?}");
1301 }
1302
1303 #[test]
1304 fn path_enumeration_handles_cycles() {
1305 let mut tg = TestGraph::new();
1307 let a = tg.add_node("ca");
1308 let b = tg.add_node("cb");
1309 let c = tg.add_node("cc");
1310 tg.add_call_edge(a, b);
1311 tg.add_call_edge(b, c);
1312 tg.add_call_edge(c, a); let snapshot = tg.snapshot();
1315 let config = TraversalConfig {
1316 direction: TraversalDirection::Outgoing,
1317 edge_filter: EdgeFilter::calls_only(),
1318 limits: TraversalLimits {
1319 max_depth: 10,
1320 max_nodes: None,
1321 max_edges: None,
1322 max_paths: Some(10),
1323 },
1324 };
1325
1326 let mut strategy = SimplePathStrategy::new(c, 0.0, true);
1327 let result = traverse(&snapshot, &[a], &config, Some(&mut strategy));
1328
1329 let paths = result.paths.as_ref().unwrap();
1330 assert_eq!(paths.len(), 1);
1332 assert_eq!(paths[0].len(), 3);
1333 }
1334
1335 #[test]
1336 fn path_enumeration_no_path() {
1337 let mut tg = TestGraph::new();
1339 let a = tg.add_node("na");
1340 let b = tg.add_node("nb");
1341 let c = tg.add_node("nc");
1342 tg.add_call_edge(a, b);
1343
1344 let snapshot = tg.snapshot();
1345 let config = TraversalConfig {
1346 direction: TraversalDirection::Outgoing,
1347 edge_filter: EdgeFilter::calls_only(),
1348 limits: TraversalLimits {
1349 max_depth: 5,
1350 max_nodes: None,
1351 max_edges: None,
1352 max_paths: Some(10),
1353 },
1354 };
1355
1356 let mut strategy = SimplePathStrategy::new(c, 0.0, true);
1357 let result = traverse(&snapshot, &[a], &config, Some(&mut strategy));
1358
1359 let paths = result.paths.as_ref().unwrap();
1360 assert!(paths.is_empty(), "expected no paths, got {paths:?}");
1361 }
1362
1363 #[test]
1364 fn path_limit_truncation() {
1365 let mut tg = TestGraph::new();
1367 let a = tg.add_node("la");
1368 let b1 = tg.add_node("lb1");
1369 let b2 = tg.add_node("lb2");
1370 let b3 = tg.add_node("lb3");
1371 let c = tg.add_node("lc");
1372 tg.add_call_edge(a, b1);
1373 tg.add_call_edge(a, b2);
1374 tg.add_call_edge(a, b3);
1375 tg.add_call_edge(b1, c);
1376 tg.add_call_edge(b2, c);
1377 tg.add_call_edge(b3, c);
1378
1379 let snapshot = tg.snapshot();
1380 let config = TraversalConfig {
1381 direction: TraversalDirection::Outgoing,
1382 edge_filter: EdgeFilter::calls_only(),
1383 limits: TraversalLimits {
1384 max_depth: 5,
1385 max_nodes: None,
1386 max_edges: None,
1387 max_paths: Some(2),
1388 },
1389 };
1390
1391 let mut strategy = SimplePathStrategy::new(c, 0.0, true);
1392 let result = traverse(&snapshot, &[a], &config, Some(&mut strategy));
1393
1394 let paths = result.paths.as_ref().unwrap();
1395 assert_eq!(paths.len(), 2, "expected exactly 2 paths (limited)");
1396 assert_eq!(
1397 result.metadata.truncation,
1398 Some(TruncationReason::PathLimit)
1399 );
1400 }
1401
1402 #[test]
1403 fn path_bfs_node_limit_truncation() {
1404 let mut tg = TestGraph::new();
1406 let a = tg.add_node("pna");
1407 let b = tg.add_node("pnb");
1408 let c = tg.add_node("pnc");
1409 let d = tg.add_node("pnd");
1410 tg.add_call_edge(a, b);
1411 tg.add_call_edge(b, c);
1412 tg.add_call_edge(c, d);
1413
1414 let snapshot = tg.snapshot();
1415 let config = TraversalConfig {
1416 direction: TraversalDirection::Outgoing,
1417 edge_filter: EdgeFilter::calls_only(),
1418 limits: TraversalLimits {
1419 max_depth: 10,
1420 max_nodes: Some(2),
1421 max_edges: None,
1422 max_paths: Some(10),
1423 },
1424 };
1425
1426 let mut strategy = SimplePathStrategy::new(d, 0.0, true);
1427 let result = traverse(&snapshot, &[a], &config, Some(&mut strategy));
1428
1429 assert!(
1430 result.nodes.len() <= 2,
1431 "node limit violated: {} nodes",
1432 result.nodes.len()
1433 );
1434 assert_eq!(
1435 result.metadata.truncation,
1436 Some(TruncationReason::NodeLimit)
1437 );
1438 }
1439
1440 #[test]
1441 fn path_bfs_edge_limit_truncation() {
1442 let mut tg = TestGraph::new();
1444 let a = tg.add_node("pea");
1445 let b = tg.add_node("peb");
1446 let c = tg.add_node("pec");
1447 let d = tg.add_node("ped");
1448 tg.add_call_edge(a, b);
1449 tg.add_call_edge(b, c);
1450 tg.add_call_edge(c, d);
1451
1452 let snapshot = tg.snapshot();
1453 let config = TraversalConfig {
1454 direction: TraversalDirection::Outgoing,
1455 edge_filter: EdgeFilter::calls_only(),
1456 limits: TraversalLimits {
1457 max_depth: 10,
1458 max_nodes: None,
1459 max_edges: Some(1),
1460 max_paths: Some(10),
1461 },
1462 };
1463
1464 let mut strategy = SimplePathStrategy::new(d, 0.0, true);
1465 let result = traverse(&snapshot, &[a], &config, Some(&mut strategy));
1466
1467 assert_eq!(
1468 result.metadata.truncation,
1469 Some(TruncationReason::EdgeLimit)
1470 );
1471 }
1472
1473 #[test]
1474 fn path_bfs_leaf_enumeration_no_target() {
1475 let mut tg = TestGraph::new();
1477 let a = tg.add_node("la");
1478 let b = tg.add_node("lb");
1479 let c = tg.add_node("lc");
1480 let d = tg.add_node("ld");
1481 tg.add_call_edge(a, b);
1482 tg.add_call_edge(b, c);
1483 tg.add_call_edge(a, d);
1484
1485 let snapshot = tg.snapshot();
1486 let config = TraversalConfig {
1487 direction: TraversalDirection::Outgoing,
1488 edge_filter: EdgeFilter::calls_only(),
1489 limits: TraversalLimits {
1490 max_depth: 10,
1491 max_nodes: None,
1492 max_edges: None,
1493 max_paths: Some(10),
1494 },
1495 };
1496
1497 let mut strategy = LeafPathStrategy;
1499 let result = traverse(&snapshot, &[a], &config, Some(&mut strategy));
1500
1501 let paths = result.paths.as_ref().unwrap();
1502 assert!(
1504 paths.len() >= 2,
1505 "expected at least 2 leaf paths, got {}",
1506 paths.len()
1507 );
1508 }
1509
1510 struct LeafPathStrategy;
1512
1513 impl TraversalStrategy for LeafPathStrategy {
1514 fn frontier_mode(&self) -> FrontierMode {
1515 FrontierMode::PathEnumeration
1516 }
1517
1518 fn visited_policy(&self) -> VisitedPolicy {
1519 VisitedPolicy::PathLocal
1520 }
1521
1522 fn path_target(&self) -> Option<NodeId> {
1523 None
1524 }
1525 }
1526
1527 #[test]
1528 fn is_followable_edge_confidence_filtering() {
1529 let calls = EdgeKind::Calls {
1531 argument_count: 0,
1532 is_async: false,
1533 };
1534 assert!(is_followable_edge(&calls, 0.0));
1535 assert!(is_followable_edge(&calls, 1.0));
1536
1537 let ffi = EdgeKind::FfiCall {
1539 convention: crate::graph::unified::edge::kind::FfiConvention::C,
1540 };
1541 assert!(is_followable_edge(&ffi, 0.5));
1542 assert!(is_followable_edge(&ffi, 0.6));
1543 assert!(!is_followable_edge(&ffi, 0.7));
1544
1545 assert!(is_followable_edge(&EdgeKind::Defines, 0.3));
1547 assert!(!is_followable_edge(&EdgeKind::Defines, 0.4));
1548 }
1549}