use std::collections::{HashMap, HashSet, VecDeque};
use std::ops::ControlFlow;
use super::concurrent::GraphSnapshot;
use super::edge::kind::EdgeKind;
use super::edge::store::StoreEdgeRef;
use super::materialize::materialize_node;
use super::node::id::NodeId;
use super::traversal::{
EdgeClassification, MaterializedEdge, TraversalMetadata, TraversalResult, TruncationReason,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TraversalDirection {
Outgoing,
Incoming,
Both,
}
#[derive(Debug, Clone)]
#[allow(clippy::struct_excessive_bools)] pub struct EdgeFilter {
pub include_calls: bool,
pub include_imports: bool,
pub include_references: bool,
pub include_inheritance: bool,
pub include_structural: bool,
pub include_type_edges: bool,
pub include_database: bool,
pub include_service: bool,
}
impl EdgeFilter {
#[must_use]
pub const fn all() -> Self {
Self {
include_calls: true,
include_imports: true,
include_references: true,
include_inheritance: true,
include_structural: true,
include_type_edges: true,
include_database: true,
include_service: true,
}
}
#[must_use]
pub const fn calls_only() -> Self {
Self {
include_calls: true,
include_imports: false,
include_references: false,
include_inheritance: false,
include_structural: false,
include_type_edges: false,
include_database: false,
include_service: false,
}
}
#[must_use]
pub const fn calls_and_imports() -> Self {
Self {
include_calls: true,
include_imports: true,
include_references: false,
include_inheritance: false,
include_structural: false,
include_type_edges: false,
include_database: false,
include_service: false,
}
}
#[must_use]
pub const fn dependency_edges() -> Self {
Self {
include_calls: true,
include_imports: true,
include_references: true,
include_inheritance: true,
include_structural: false,
include_type_edges: false,
include_database: false,
include_service: false,
}
}
#[must_use]
pub fn accepts(&self, classification: &EdgeClassification) -> bool {
match classification {
EdgeClassification::Call { .. } => self.include_calls,
EdgeClassification::Import { .. } | EdgeClassification::Export { .. } => {
self.include_imports
}
EdgeClassification::Reference => self.include_references,
EdgeClassification::Inherits | EdgeClassification::Implements => {
self.include_inheritance
}
EdgeClassification::Contains | EdgeClassification::Defines => self.include_structural,
EdgeClassification::TypeOf => self.include_type_edges,
EdgeClassification::DatabaseAccess => self.include_database,
EdgeClassification::ServiceInteraction => self.include_service,
}
}
}
#[derive(Debug, Clone)]
pub struct TraversalLimits {
pub max_depth: u32,
pub max_nodes: Option<usize>,
pub max_edges: Option<usize>,
pub max_paths: Option<usize>,
}
impl TraversalLimits {
#[must_use]
pub const fn default_subgraph() -> Self {
Self {
max_depth: 2,
max_nodes: Some(50),
max_edges: None,
max_paths: None,
}
}
#[must_use]
pub const fn default_export() -> Self {
Self {
max_depth: 2,
max_nodes: None,
max_edges: Some(1000),
max_paths: None,
}
}
#[must_use]
pub const fn default_impact() -> Self {
Self {
max_depth: 3,
max_nodes: Some(500),
max_edges: None,
max_paths: None,
}
}
#[must_use]
pub const fn default_trace() -> Self {
Self {
max_depth: 5,
max_nodes: None,
max_edges: None,
max_paths: Some(5),
}
}
}
#[derive(Debug, Clone)]
pub struct TraversalConfig {
pub direction: TraversalDirection,
pub edge_filter: EdgeFilter,
pub limits: TraversalLimits,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FrontierMode {
Standard,
PathEnumeration,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VisitedPolicy {
Global,
PathLocal,
}
pub trait TraversalStrategy {
fn accept_edge(&mut self, _edge: &StoreEdgeRef, _depth: u32) -> bool {
true
}
fn should_enqueue(
&mut self,
_node_id: NodeId,
_from: NodeId,
_edge: &EdgeKind,
_depth: u32,
) -> bool {
true
}
fn on_path_complete(&mut self, _path: &[NodeId]) -> ControlFlow<()> {
ControlFlow::Continue(())
}
fn frontier_mode(&self) -> FrontierMode {
FrontierMode::Standard
}
fn visited_policy(&self) -> VisitedPolicy {
VisitedPolicy::Global
}
fn path_target(&self) -> Option<NodeId> {
None
}
}
struct DefaultStrategy;
impl TraversalStrategy for DefaultStrategy {}
#[must_use]
pub fn is_followable_edge(kind: &EdgeKind, min_confidence: f64) -> bool {
let confidence = match kind {
EdgeKind::Calls { .. } | EdgeKind::TraitMethodBinding { .. } => 1.0,
EdgeKind::Inherits | EdgeKind::Implements | EdgeKind::SealedPermit => 0.9,
EdgeKind::Imports { .. } | EdgeKind::Exports { .. } => 0.8,
EdgeKind::References => 0.7,
EdgeKind::FfiCall { .. }
| EdgeKind::HttpRequest { .. }
| EdgeKind::GrpcCall { .. }
| EdgeKind::WebAssemblyCall => 0.6,
EdgeKind::MacroExpansion { .. } => 0.5,
_ => 0.3,
};
confidence >= min_confidence
}
pub struct SimplePathStrategy {
target: NodeId,
min_confidence: f64,
allow_cross_language: bool,
}
impl SimplePathStrategy {
#[must_use]
pub fn new(target: NodeId, min_confidence: f64, allow_cross_language: bool) -> Self {
Self {
target,
min_confidence,
allow_cross_language,
}
}
}
impl TraversalStrategy for SimplePathStrategy {
fn accept_edge(&mut self, edge: &StoreEdgeRef, _depth: u32) -> bool {
is_followable_edge(&edge.kind, self.min_confidence)
&& (self.allow_cross_language || !edge.kind.is_cross_boundary())
}
fn frontier_mode(&self) -> FrontierMode {
FrontierMode::PathEnumeration
}
fn visited_policy(&self) -> VisitedPolicy {
VisitedPolicy::PathLocal
}
fn path_target(&self) -> Option<NodeId> {
Some(self.target)
}
}
pub struct SccPathStrategy<'a> {
scc_data: &'a super::analysis::scc::SccData,
cond_dag: &'a super::analysis::condensation::CondensationDag,
target: NodeId,
target_scc: Option<u32>,
min_confidence: f64,
allow_cross_language: bool,
}
impl<'a> SccPathStrategy<'a> {
#[must_use]
pub fn new(
scc_data: &'a super::analysis::scc::SccData,
cond_dag: &'a super::analysis::condensation::CondensationDag,
target: NodeId,
min_confidence: f64,
allow_cross_language: bool,
) -> Self {
let target_scc = scc_data.scc_of(target);
Self {
scc_data,
cond_dag,
target,
target_scc,
min_confidence,
allow_cross_language,
}
}
}
impl TraversalStrategy for SccPathStrategy<'_> {
fn accept_edge(&mut self, edge: &StoreEdgeRef, _depth: u32) -> bool {
is_followable_edge(&edge.kind, self.min_confidence)
&& (self.allow_cross_language || !edge.kind.is_cross_boundary())
}
fn should_enqueue(
&mut self,
node_id: NodeId,
_from: NodeId,
_edge: &EdgeKind,
_depth: u32,
) -> bool {
let Some(target_scc) = self.target_scc else {
return true;
};
let Some(node_scc) = self.scc_data.scc_of(node_id) else {
return true;
};
self.cond_dag.can_reach(node_scc, target_scc)
}
fn on_path_complete(&mut self, _path: &[NodeId]) -> ControlFlow<()> {
ControlFlow::Continue(())
}
fn frontier_mode(&self) -> FrontierMode {
FrontierMode::PathEnumeration
}
fn visited_policy(&self) -> VisitedPolicy {
VisitedPolicy::PathLocal
}
fn path_target(&self) -> Option<NodeId> {
Some(self.target)
}
}
#[must_use]
pub fn traverse(
snapshot: &GraphSnapshot,
seeds: &[NodeId],
config: &TraversalConfig,
strategy: Option<&mut dyn TraversalStrategy>,
) -> TraversalResult {
let mut default_strategy = DefaultStrategy;
let strategy = strategy.unwrap_or(&mut default_strategy);
let frontier_mode = strategy.frontier_mode();
let visited_policy = strategy.visited_policy();
match (frontier_mode, visited_policy) {
(FrontierMode::Standard, _) => {
run_standard_bfs(snapshot, seeds, config, strategy)
}
(FrontierMode::PathEnumeration, VisitedPolicy::PathLocal) => {
run_path_bfs(snapshot, seeds, config, strategy)
}
(FrontierMode::PathEnumeration, VisitedPolicy::Global) => {
run_path_bfs(snapshot, seeds, config, strategy)
}
}
}
struct RawEdge {
source: NodeId,
target: NodeId,
kind: EdgeKind,
depth: u32,
}
fn run_standard_bfs(
snapshot: &GraphSnapshot,
seeds: &[NodeId],
config: &TraversalConfig,
strategy: &mut dyn TraversalStrategy,
) -> TraversalResult {
let mut visited: HashSet<NodeId> = HashSet::new();
let mut queue: VecDeque<(NodeId, u32)> = VecDeque::new();
let mut raw_edges: Vec<RawEdge> = Vec::new();
let mut discovered_order: Vec<NodeId> = Vec::new();
let mut truncation: Option<TruncationReason> = None;
let mut max_depth_reached = false;
let mut nodes_visited: usize = 0;
for &seed in seeds {
if visited.insert(seed) {
discovered_order.push(seed);
queue.push_back((seed, 0));
}
}
'bfs: while let Some((current, depth)) = queue.pop_front() {
nodes_visited += 1;
if depth >= config.limits.max_depth {
max_depth_reached = true;
continue;
}
if let Some(max_nodes) = config.limits.max_nodes
&& discovered_order.len() >= max_nodes
{
truncation = Some(TruncationReason::NodeLimit);
break;
}
let edges = collect_edges(snapshot, current, config.direction);
for edge_ref in &edges {
if let Some(max_edges) = config.limits.max_edges
&& raw_edges.len() >= max_edges
{
truncation = Some(TruncationReason::EdgeLimit);
break 'bfs;
}
let classification = EdgeClassification::from(&edge_ref.kind);
if !config.edge_filter.accepts(&classification) {
continue;
}
if !strategy.accept_edge(edge_ref, depth) {
continue;
}
let next = neighbor_of(edge_ref, current);
raw_edges.push(RawEdge {
source: edge_ref.source,
target: edge_ref.target,
kind: edge_ref.kind.clone(),
depth: depth + 1,
});
if visited.insert(next)
&& strategy.should_enqueue(next, current, &edge_ref.kind, depth + 1)
{
if let Some(max_nodes) = config.limits.max_nodes
&& discovered_order.len() >= max_nodes
{
truncation = Some(TruncationReason::NodeLimit);
break 'bfs;
}
discovered_order.push(next);
queue.push_back((next, depth + 1));
}
}
}
materialize_result(
snapshot,
&discovered_order,
&raw_edges,
None,
truncation,
max_depth_reached,
seeds.len(),
nodes_visited,
)
}
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)] fn run_path_bfs(
snapshot: &GraphSnapshot,
seeds: &[NodeId],
config: &TraversalConfig,
strategy: &mut dyn TraversalStrategy,
) -> TraversalResult {
let target = strategy.path_target();
let mut collected_paths: Vec<Vec<NodeId>> = Vec::new();
let mut discovered_order: Vec<NodeId> = Vec::new();
let mut seen: HashSet<NodeId> = HashSet::new();
let mut raw_edges: Vec<RawEdge> = Vec::new();
let mut truncation: Option<TruncationReason> = None;
let mut max_depth_reached = false;
let mut nodes_visited: usize = 0;
let mut queue: VecDeque<(NodeId, Vec<NodeId>, u32)> = VecDeque::new();
for &seed in seeds {
queue.push_back((seed, vec![seed], 0));
if seen.insert(seed) {
discovered_order.push(seed);
}
}
let use_global_visited = strategy.visited_policy() == VisitedPolicy::Global;
let mut global_visited: HashSet<NodeId> = if use_global_visited {
seeds.iter().copied().collect()
} else {
HashSet::new()
};
'bfs: while let Some((current, path, depth)) = queue.pop_front() {
nodes_visited += 1;
if let Some(t) = target
&& current == t
&& path.len() > 1
{
for &node in &path {
if seen.insert(node) {
discovered_order.push(node);
}
}
let control = strategy.on_path_complete(&path);
collected_paths.push(path);
if let Some(max_paths) = config.limits.max_paths
&& collected_paths.len() >= max_paths
{
truncation = Some(TruncationReason::PathLimit);
break 'bfs;
}
if control.is_break() {
break 'bfs;
}
continue;
}
if depth >= config.limits.max_depth {
max_depth_reached = true;
continue;
}
if let Some(max_nodes) = config.limits.max_nodes
&& discovered_order.len() >= max_nodes
{
truncation = Some(TruncationReason::NodeLimit);
break;
}
let edges = collect_edges(snapshot, current, config.direction);
let mut has_followable_successor = false;
for edge_ref in &edges {
if let Some(max_edges) = config.limits.max_edges
&& raw_edges.len() >= max_edges
{
truncation = Some(TruncationReason::EdgeLimit);
break 'bfs;
}
let classification = EdgeClassification::from(&edge_ref.kind);
if !config.edge_filter.accepts(&classification) {
continue;
}
if !strategy.accept_edge(edge_ref, depth) {
continue;
}
let next = neighbor_of(edge_ref, current);
if use_global_visited {
if !global_visited.insert(next) {
continue;
}
} else if path.contains(&next) {
continue;
}
if !strategy.should_enqueue(next, current, &edge_ref.kind, depth + 1) {
continue;
}
has_followable_successor = true;
raw_edges.push(RawEdge {
source: edge_ref.source,
target: edge_ref.target,
kind: edge_ref.kind.clone(),
depth: depth + 1,
});
if let Some(max_nodes) = config.limits.max_nodes
&& discovered_order.len() >= max_nodes
{
truncation = Some(TruncationReason::NodeLimit);
break 'bfs;
}
if seen.insert(next) {
discovered_order.push(next);
}
let mut new_path = path.clone();
new_path.push(next);
queue.push_back((next, new_path, depth + 1));
}
if target.is_none() && !has_followable_successor && path.len() > 1 {
for &node in &path {
if seen.insert(node) {
discovered_order.push(node);
}
}
let control = strategy.on_path_complete(&path);
collected_paths.push(path);
if let Some(max_paths) = config.limits.max_paths
&& collected_paths.len() >= max_paths
{
truncation = Some(TruncationReason::PathLimit);
break 'bfs;
}
if control.is_break() {
break 'bfs;
}
}
}
let paths_for_result = Some(collected_paths);
materialize_result(
snapshot,
&discovered_order,
&raw_edges,
paths_for_result,
truncation,
max_depth_reached,
seeds.len(),
nodes_visited,
)
}
fn collect_edges(
snapshot: &GraphSnapshot,
node: NodeId,
direction: TraversalDirection,
) -> Vec<StoreEdgeRef> {
match direction {
TraversalDirection::Outgoing => snapshot.edges().edges_from(node),
TraversalDirection::Incoming => snapshot.edges().edges_to(node),
TraversalDirection::Both => {
let mut edges = snapshot.edges().edges_from(node);
edges.extend(snapshot.edges().edges_to(node));
edges
}
}
}
fn neighbor_of(edge_ref: &StoreEdgeRef, current: NodeId) -> NodeId {
if edge_ref.source == current {
edge_ref.target
} else {
edge_ref.source
}
}
#[allow(clippy::too_many_arguments)]
fn materialize_result(
snapshot: &GraphSnapshot,
discovered_order: &[NodeId],
raw_edges: &[RawEdge],
raw_paths: Option<Vec<Vec<NodeId>>>,
truncation: Option<TruncationReason>,
max_depth_reached: bool,
seed_count: usize,
nodes_visited: usize,
) -> TraversalResult {
let mut nodes = Vec::with_capacity(discovered_order.len());
let mut node_index: HashMap<NodeId, usize> = HashMap::with_capacity(discovered_order.len());
for &node_id in discovered_order {
if node_index.contains_key(&node_id) {
continue;
}
if let Some(materialized) = materialize_node(snapshot, node_id) {
let idx = nodes.len();
node_index.insert(node_id, idx);
nodes.push(materialized);
}
}
let mut edges = Vec::with_capacity(raw_edges.len());
for raw in raw_edges {
if let (Some(&source_idx), Some(&target_idx)) =
(node_index.get(&raw.source), node_index.get(&raw.target))
{
edges.push(MaterializedEdge {
source_idx,
target_idx,
classification: EdgeClassification::from(&raw.kind),
raw_kind: raw.kind.clone(),
depth: raw.depth,
});
}
}
edges.dedup_by(|a, b| {
a.source_idx == b.source_idx
&& a.target_idx == b.target_idx
&& a.classification == b.classification
});
let paths = raw_paths.map(|path_list| {
path_list
.iter()
.filter_map(|path| {
let converted: Vec<usize> = path
.iter()
.filter_map(|node_id| node_index.get(node_id).copied())
.collect();
if converted.len() == path.len() {
Some(converted)
} else {
None
}
})
.collect()
});
TraversalResult {
metadata: TraversalMetadata {
truncation,
max_depth_reached,
seed_count,
nodes_visited,
total_nodes: nodes.len(),
total_edges: edges.len(),
},
nodes,
edges,
paths,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::node::Language;
use crate::graph::unified::concurrent::CodeGraph;
use crate::graph::unified::node::kind::NodeKind;
use crate::graph::unified::storage::arena::NodeEntry;
use crate::graph::unified::file::FileId;
struct TestGraph {
graph: CodeGraph,
file_id: Option<FileId>,
}
impl TestGraph {
fn new() -> Self {
Self {
graph: CodeGraph::new(),
file_id: None,
}
}
fn ensure_file_id(&mut self) -> FileId {
if let Some(fid) = self.file_id {
return fid;
}
let file_path = std::path::PathBuf::from("/kernel-tests/test.rs");
let fid = self
.graph
.files_mut()
.register_with_language(&file_path, Some(Language::Rust))
.unwrap();
self.file_id = Some(fid);
fid
}
fn add_node(&mut self, name: &str) -> NodeId {
self.add_node_with_kind(name, NodeKind::Function)
}
fn add_node_with_kind(&mut self, name: &str, kind: NodeKind) -> NodeId {
let file_id = self.ensure_file_id();
let name_id = self.graph.strings_mut().intern(name).unwrap();
let qn_id = self
.graph
.strings_mut()
.intern(&format!("test::{name}"))
.unwrap();
let entry = NodeEntry::new(kind, name_id, file_id)
.with_qualified_name(qn_id)
.with_location(1, 0, 10, 0);
let node_id = self.graph.nodes_mut().alloc(entry).unwrap();
self.graph
.indices_mut()
.add(node_id, kind, name_id, Some(qn_id), file_id);
node_id
}
fn add_call_edge(&mut self, source: NodeId, target: NodeId) {
let file_id = self.ensure_file_id();
self.graph.edges_mut().add_edge(
source,
target,
EdgeKind::Calls {
argument_count: 0,
is_async: false,
},
file_id,
);
}
fn add_edge(&mut self, source: NodeId, target: NodeId, kind: EdgeKind) {
let file_id = self.ensure_file_id();
self.graph
.edges_mut()
.add_edge(source, target, kind, file_id);
}
fn snapshot(&self) -> GraphSnapshot {
self.graph.snapshot()
}
}
fn calls_config(depth: u32) -> TraversalConfig {
TraversalConfig {
direction: TraversalDirection::Outgoing,
edge_filter: EdgeFilter::calls_only(),
limits: TraversalLimits {
max_depth: depth,
max_nodes: None,
max_edges: None,
max_paths: None,
},
}
}
#[test]
fn standard_outgoing_bfs() {
let mut tg = TestGraph::new();
let a = tg.add_node("a");
let b = tg.add_node("b");
let c = tg.add_node("c");
tg.add_call_edge(a, b);
tg.add_call_edge(b, c);
let snapshot = tg.snapshot();
let result = traverse(&snapshot, &[a], &calls_config(3), None);
assert_eq!(result.nodes.len(), 3);
assert_eq!(result.edges.len(), 2);
assert!(result.metadata.truncation.is_none());
assert_eq!(result.nodes[0].node_id, a);
}
#[test]
fn depth_limit() {
let mut tg = TestGraph::new();
let a = tg.add_node("a");
let b = tg.add_node("b");
let c = tg.add_node("c");
tg.add_call_edge(a, b);
tg.add_call_edge(b, c);
let snapshot = tg.snapshot();
let result = traverse(&snapshot, &[a], &calls_config(1), None);
assert_eq!(result.nodes.len(), 2);
assert_eq!(result.edges.len(), 1);
assert!(result.metadata.max_depth_reached);
}
#[test]
fn node_limit_truncation() {
let mut tg = TestGraph::new();
let a = tg.add_node("a");
let b = tg.add_node("b");
let c = tg.add_node("c");
let d = tg.add_node("d");
tg.add_call_edge(a, b);
tg.add_call_edge(a, c);
tg.add_call_edge(a, d);
let snapshot = tg.snapshot();
let config = TraversalConfig {
direction: TraversalDirection::Outgoing,
edge_filter: EdgeFilter::calls_only(),
limits: TraversalLimits {
max_depth: 5,
max_nodes: Some(2),
max_edges: None,
max_paths: None,
},
};
let result = traverse(&snapshot, &[a], &config, None);
assert_eq!(
result.metadata.truncation,
Some(TruncationReason::NodeLimit)
);
}
#[test]
fn empty_seeds() {
let tg = TestGraph::new();
let snapshot = tg.snapshot();
let result = traverse(&snapshot, &[], &calls_config(3), None);
assert!(result.nodes.is_empty());
assert!(result.edges.is_empty());
assert!(result.metadata.truncation.is_none());
assert_eq!(result.metadata.seed_count, 0);
}
#[test]
fn incoming_bfs() {
let mut tg = TestGraph::new();
let a = tg.add_node("a");
let b = tg.add_node("b");
let c = tg.add_node("c");
tg.add_call_edge(a, b);
tg.add_call_edge(c, b);
let snapshot = tg.snapshot();
let config = TraversalConfig {
direction: TraversalDirection::Incoming,
edge_filter: EdgeFilter::calls_only(),
limits: TraversalLimits {
max_depth: 3,
max_nodes: None,
max_edges: None,
max_paths: None,
},
};
let result = traverse(&snapshot, &[b], &config, None);
assert_eq!(result.nodes.len(), 3);
}
#[test]
fn bidirectional_bfs() {
let mut tg = TestGraph::new();
let a = tg.add_node("a");
let b = tg.add_node("b");
let c = tg.add_node("c");
tg.add_call_edge(a, b);
tg.add_call_edge(b, c);
let snapshot = tg.snapshot();
let config = TraversalConfig {
direction: TraversalDirection::Both,
edge_filter: EdgeFilter::calls_only(),
limits: TraversalLimits {
max_depth: 3,
max_nodes: None,
max_edges: None,
max_paths: None,
},
};
let result = traverse(&snapshot, &[b], &config, None);
assert_eq!(result.nodes.len(), 3);
}
#[test]
fn edge_filtering() {
let mut tg = TestGraph::new();
let a = tg.add_node("a");
let b = tg.add_node("b");
let c = tg.add_node("c");
tg.add_call_edge(a, b);
tg.add_edge(
a,
c,
EdgeKind::Imports {
alias: None,
is_wildcard: false,
},
);
let snapshot = tg.snapshot();
let result = traverse(&snapshot, &[a], &calls_config(3), None);
assert_eq!(result.nodes.len(), 2);
let config = TraversalConfig {
direction: TraversalDirection::Outgoing,
edge_filter: EdgeFilter::calls_and_imports(),
limits: TraversalLimits {
max_depth: 3,
max_nodes: None,
max_edges: None,
max_paths: None,
},
};
let result = traverse(&snapshot, &[a], &config, None);
assert_eq!(result.nodes.len(), 3); }
#[test]
fn cycle_handling_standard() {
let mut tg = TestGraph::new();
let a = tg.add_node("a");
let b = tg.add_node("b");
tg.add_call_edge(a, b);
tg.add_call_edge(b, a);
let snapshot = tg.snapshot();
let result = traverse(&snapshot, &[a], &calls_config(10), None);
assert_eq!(result.nodes.len(), 2);
}
#[test]
fn edge_limit_truncation() {
let mut tg = TestGraph::new();
let a = tg.add_node("a");
let b = tg.add_node("b");
let c = tg.add_node("c");
let d = tg.add_node("d");
tg.add_call_edge(a, b);
tg.add_call_edge(a, c);
tg.add_call_edge(a, d);
let snapshot = tg.snapshot();
let config = TraversalConfig {
direction: TraversalDirection::Outgoing,
edge_filter: EdgeFilter::calls_only(),
limits: TraversalLimits {
max_depth: 5,
max_nodes: None,
max_edges: Some(2),
max_paths: None,
},
};
let result = traverse(&snapshot, &[a], &config, None);
assert_eq!(
result.metadata.truncation,
Some(TruncationReason::EdgeLimit)
);
}
#[test]
fn index_invariants_hold() {
let mut tg = TestGraph::new();
let a = tg.add_node("a");
let b = tg.add_node("b");
let c = tg.add_node("c");
tg.add_call_edge(a, b);
tg.add_call_edge(b, c);
let snapshot = tg.snapshot();
let result = traverse(&snapshot, &[a], &calls_config(5), None);
for edge in &result.edges {
assert!(edge.source_idx < result.nodes.len());
assert!(edge.target_idx < result.nodes.len());
}
assert_eq!(result.metadata.total_nodes, result.nodes.len());
assert_eq!(result.metadata.total_edges, result.edges.len());
}
#[test]
fn all_filter_includes_structural() {
let mut tg = TestGraph::new();
let a = tg.add_node("a");
let b = tg.add_node("b");
tg.add_edge(a, b, EdgeKind::Defines);
let snapshot = tg.snapshot();
let config = TraversalConfig {
direction: TraversalDirection::Outgoing,
edge_filter: EdgeFilter::all(),
limits: TraversalLimits {
max_depth: 3,
max_nodes: None,
max_edges: None,
max_paths: None,
},
};
let result = traverse(&snapshot, &[a], &config, None);
assert_eq!(result.nodes.len(), 2);
assert_eq!(result.edges.len(), 1);
assert_eq!(result.edges[0].classification, EdgeClassification::Defines);
}
#[test]
fn path_enumeration_finds_path() {
let mut tg = TestGraph::new();
let a = tg.add_node("pa");
let b = tg.add_node("pb");
let c = tg.add_node("pc");
tg.add_call_edge(a, b);
tg.add_call_edge(b, c);
let snapshot = tg.snapshot();
let config = TraversalConfig {
direction: TraversalDirection::Outgoing,
edge_filter: EdgeFilter::calls_only(),
limits: TraversalLimits {
max_depth: 5,
max_nodes: None,
max_edges: None,
max_paths: Some(10),
},
};
let mut strategy = SimplePathStrategy::new(c, 0.0, true);
let result = traverse(&snapshot, &[a], &config, Some(&mut strategy));
assert!(result.paths.is_some());
let paths = result.paths.as_ref().unwrap();
assert_eq!(paths.len(), 1, "expected 1 path from a→c");
assert_eq!(paths[0].len(), 3);
}
#[test]
fn path_local_allows_shared_nodes_in_diamond() {
let mut tg = TestGraph::new();
let a = tg.add_node("da");
let b = tg.add_node("db");
let c = tg.add_node("dc");
let d = tg.add_node("dd");
tg.add_call_edge(a, b);
tg.add_call_edge(a, c);
tg.add_call_edge(b, d);
tg.add_call_edge(c, d);
let snapshot = tg.snapshot();
let config = TraversalConfig {
direction: TraversalDirection::Outgoing,
edge_filter: EdgeFilter::calls_only(),
limits: TraversalLimits {
max_depth: 5,
max_nodes: None,
max_edges: None,
max_paths: Some(10),
},
};
let mut strategy = SimplePathStrategy::new(d, 0.0, true);
let result = traverse(&snapshot, &[a], &config, Some(&mut strategy));
let paths = result.paths.as_ref().unwrap();
assert_eq!(paths.len(), 2, "expected 2 paths in diamond, got {paths:?}");
}
#[test]
fn path_enumeration_handles_cycles() {
let mut tg = TestGraph::new();
let a = tg.add_node("ca");
let b = tg.add_node("cb");
let c = tg.add_node("cc");
tg.add_call_edge(a, b);
tg.add_call_edge(b, c);
tg.add_call_edge(c, a);
let snapshot = tg.snapshot();
let config = TraversalConfig {
direction: TraversalDirection::Outgoing,
edge_filter: EdgeFilter::calls_only(),
limits: TraversalLimits {
max_depth: 10,
max_nodes: None,
max_edges: None,
max_paths: Some(10),
},
};
let mut strategy = SimplePathStrategy::new(c, 0.0, true);
let result = traverse(&snapshot, &[a], &config, Some(&mut strategy));
let paths = result.paths.as_ref().unwrap();
assert_eq!(paths.len(), 1);
assert_eq!(paths[0].len(), 3);
}
#[test]
fn path_enumeration_no_path() {
let mut tg = TestGraph::new();
let a = tg.add_node("na");
let b = tg.add_node("nb");
let c = tg.add_node("nc");
tg.add_call_edge(a, b);
let snapshot = tg.snapshot();
let config = TraversalConfig {
direction: TraversalDirection::Outgoing,
edge_filter: EdgeFilter::calls_only(),
limits: TraversalLimits {
max_depth: 5,
max_nodes: None,
max_edges: None,
max_paths: Some(10),
},
};
let mut strategy = SimplePathStrategy::new(c, 0.0, true);
let result = traverse(&snapshot, &[a], &config, Some(&mut strategy));
let paths = result.paths.as_ref().unwrap();
assert!(paths.is_empty(), "expected no paths, got {paths:?}");
}
#[test]
fn path_limit_truncation() {
let mut tg = TestGraph::new();
let a = tg.add_node("la");
let b1 = tg.add_node("lb1");
let b2 = tg.add_node("lb2");
let b3 = tg.add_node("lb3");
let c = tg.add_node("lc");
tg.add_call_edge(a, b1);
tg.add_call_edge(a, b2);
tg.add_call_edge(a, b3);
tg.add_call_edge(b1, c);
tg.add_call_edge(b2, c);
tg.add_call_edge(b3, c);
let snapshot = tg.snapshot();
let config = TraversalConfig {
direction: TraversalDirection::Outgoing,
edge_filter: EdgeFilter::calls_only(),
limits: TraversalLimits {
max_depth: 5,
max_nodes: None,
max_edges: None,
max_paths: Some(2),
},
};
let mut strategy = SimplePathStrategy::new(c, 0.0, true);
let result = traverse(&snapshot, &[a], &config, Some(&mut strategy));
let paths = result.paths.as_ref().unwrap();
assert_eq!(paths.len(), 2, "expected exactly 2 paths (limited)");
assert_eq!(
result.metadata.truncation,
Some(TruncationReason::PathLimit)
);
}
#[test]
fn path_bfs_node_limit_truncation() {
let mut tg = TestGraph::new();
let a = tg.add_node("pna");
let b = tg.add_node("pnb");
let c = tg.add_node("pnc");
let d = tg.add_node("pnd");
tg.add_call_edge(a, b);
tg.add_call_edge(b, c);
tg.add_call_edge(c, d);
let snapshot = tg.snapshot();
let config = TraversalConfig {
direction: TraversalDirection::Outgoing,
edge_filter: EdgeFilter::calls_only(),
limits: TraversalLimits {
max_depth: 10,
max_nodes: Some(2),
max_edges: None,
max_paths: Some(10),
},
};
let mut strategy = SimplePathStrategy::new(d, 0.0, true);
let result = traverse(&snapshot, &[a], &config, Some(&mut strategy));
assert!(
result.nodes.len() <= 2,
"node limit violated: {} nodes",
result.nodes.len()
);
assert_eq!(
result.metadata.truncation,
Some(TruncationReason::NodeLimit)
);
}
#[test]
fn path_bfs_edge_limit_truncation() {
let mut tg = TestGraph::new();
let a = tg.add_node("pea");
let b = tg.add_node("peb");
let c = tg.add_node("pec");
let d = tg.add_node("ped");
tg.add_call_edge(a, b);
tg.add_call_edge(b, c);
tg.add_call_edge(c, d);
let snapshot = tg.snapshot();
let config = TraversalConfig {
direction: TraversalDirection::Outgoing,
edge_filter: EdgeFilter::calls_only(),
limits: TraversalLimits {
max_depth: 10,
max_nodes: None,
max_edges: Some(1),
max_paths: Some(10),
},
};
let mut strategy = SimplePathStrategy::new(d, 0.0, true);
let result = traverse(&snapshot, &[a], &config, Some(&mut strategy));
assert_eq!(
result.metadata.truncation,
Some(TruncationReason::EdgeLimit)
);
}
#[test]
fn path_bfs_leaf_enumeration_no_target() {
let mut tg = TestGraph::new();
let a = tg.add_node("la");
let b = tg.add_node("lb");
let c = tg.add_node("lc");
let d = tg.add_node("ld");
tg.add_call_edge(a, b);
tg.add_call_edge(b, c);
tg.add_call_edge(a, d);
let snapshot = tg.snapshot();
let config = TraversalConfig {
direction: TraversalDirection::Outgoing,
edge_filter: EdgeFilter::calls_only(),
limits: TraversalLimits {
max_depth: 10,
max_nodes: None,
max_edges: None,
max_paths: Some(10),
},
};
let mut strategy = LeafPathStrategy;
let result = traverse(&snapshot, &[a], &config, Some(&mut strategy));
let paths = result.paths.as_ref().unwrap();
assert!(
paths.len() >= 2,
"expected at least 2 leaf paths, got {}",
paths.len()
);
}
struct LeafPathStrategy;
impl TraversalStrategy for LeafPathStrategy {
fn frontier_mode(&self) -> FrontierMode {
FrontierMode::PathEnumeration
}
fn visited_policy(&self) -> VisitedPolicy {
VisitedPolicy::PathLocal
}
fn path_target(&self) -> Option<NodeId> {
None
}
}
#[test]
fn is_followable_edge_confidence_filtering() {
let calls = EdgeKind::Calls {
argument_count: 0,
is_async: false,
};
assert!(is_followable_edge(&calls, 0.0));
assert!(is_followable_edge(&calls, 1.0));
let ffi = EdgeKind::FfiCall {
convention: crate::graph::unified::edge::kind::FfiConvention::C,
};
assert!(is_followable_edge(&ffi, 0.5));
assert!(is_followable_edge(&ffi, 0.6));
assert!(!is_followable_edge(&ffi, 0.7));
assert!(is_followable_edge(&EdgeKind::Defines, 0.3));
assert!(!is_followable_edge(&EdgeKind::Defines, 0.4));
}
}