use petgraph::stable_graph::StableGraph;
use petgraph::visit::EdgeRef;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet, VecDeque};
use std::sync::{Arc, Mutex};
use crate::graph::trigram::TrigramIndex;
pub type NodeId = petgraph::stable_graph::NodeIndex;
pub type EdgeId = petgraph::stable_graph::EdgeIndex;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Node {
pub id: String,
pub node_type: NodeType,
pub name: String,
pub file_path: Arc<str>,
pub byte_range: (usize, usize),
pub complexity: u32,
pub language: String,
}
impl Node {
pub fn new_file_summary(file_path: &str, language: &str) -> Self {
let stem = std::path::Path::new(file_path)
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("file")
.to_string();
Self {
id: format!("{}::file_summary", file_path),
node_type: NodeType::FileSummary,
name: stem,
file_path: std::sync::Arc::from(file_path),
byte_range: (0, 0),
complexity: 0,
language: language.to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum NodeType {
Function,
Class,
Method,
Variable,
Module,
External,
FileSummary,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum EdgeType {
Call,
DataDependency,
Inheritance,
Import,
Containment,
StateTransition,
CommandArgument,
Environment,
Stdin,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Edge {
pub edge_type: EdgeType,
pub metadata: EdgeMetadata,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EdgeMetadata {
pub call_count: Option<usize>,
pub variable_name: Option<String>,
pub confidence: Option<f32>,
#[serde(default)]
pub channel: Option<String>,
#[serde(default)]
pub position: Option<usize>,
}
impl EdgeMetadata {
pub fn empty() -> Self {
Self {
call_count: None,
variable_name: None,
confidence: None,
channel: None,
position: None,
}
}
pub fn with_confidence(confidence: f32) -> Self {
Self {
call_count: None,
variable_name: None,
confidence: Some(confidence),
channel: None,
position: None,
}
}
pub fn with_variable(name: String) -> Self {
Self {
call_count: None,
variable_name: Some(name),
confidence: None,
channel: None,
position: None,
}
}
}
#[derive(Debug, Clone)]
pub struct TraversalConfig {
pub max_depth: Option<usize>,
pub max_nodes: Option<usize>,
pub allowed_edge_types: Option<&'static [EdgeType]>,
pub excluded_node_types: Option<Vec<NodeType>>,
pub min_complexity: Option<u32>,
pub min_edge_confidence: f32,
}
impl TraversalConfig {
pub fn for_llm_context() -> Self {
Self {
max_depth: Some(3),
max_nodes: Some(50),
allowed_edge_types: Some(&[
EdgeType::Call,
EdgeType::DataDependency,
EdgeType::StateTransition,
EdgeType::CommandArgument,
EdgeType::Environment,
EdgeType::Stdin,
]),
excluded_node_types: Some(vec![NodeType::Module]),
min_complexity: None,
min_edge_confidence: 0.5,
}
}
pub fn for_semantic_analysis() -> Self {
Self {
max_depth: Some(5),
max_nodes: Some(150),
allowed_edge_types: Some(&[
EdgeType::Call,
EdgeType::DataDependency,
EdgeType::Inheritance,
EdgeType::StateTransition,
EdgeType::CommandArgument,
EdgeType::Environment,
EdgeType::Stdin,
]),
excluded_node_types: None,
min_complexity: None,
min_edge_confidence: 0.4,
}
}
pub fn for_impact_analysis() -> Self {
Self {
max_depth: None,
max_nodes: Some(500),
allowed_edge_types: Some(&[
EdgeType::Call,
EdgeType::DataDependency,
EdgeType::Inheritance,
EdgeType::StateTransition,
EdgeType::CommandArgument,
EdgeType::Environment,
EdgeType::Stdin,
]),
excluded_node_types: None,
min_complexity: None,
min_edge_confidence: 0.0,
}
}
pub fn for_import_graph() -> Self {
Self {
max_depth: Some(10),
max_nodes: Some(1000),
allowed_edge_types: Some(&[EdgeType::Import]),
excluded_node_types: None,
min_complexity: None,
min_edge_confidence: 0.0,
}
}
fn edge_allowed(&self, edge: &Edge) -> bool {
let type_ok = self
.allowed_edge_types
.as_ref()
.map(|types| types.contains(&edge.edge_type))
.unwrap_or(true);
let confidence_ok = edge
.metadata
.confidence
.map(|c| c >= self.min_edge_confidence)
.unwrap_or(true);
type_ok && confidence_ok
}
fn node_should_collect(&self, node: &Node) -> bool {
let type_ok = self
.excluded_node_types
.as_ref()
.map(|excluded| !excluded.contains(&node.node_type))
.unwrap_or(true);
let complexity_ok = self
.min_complexity
.map(|min| node.complexity >= min)
.unwrap_or(true);
type_ok && complexity_ok
}
}
#[derive(Debug, Default, Clone)]
pub struct EmbeddingStore {
pub(crate) embeddings: HashMap<String, Vec<f32>>, }
impl EmbeddingStore {
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, node_id: &str, embedding: Vec<f32>) {
self.embeddings.insert(node_id.to_string(), embedding);
}
pub fn get(&self, node_id: &str) -> Option<&Vec<f32>> {
self.embeddings.get(node_id)
}
pub fn remove(&mut self, node_id: &str) {
self.embeddings.remove(node_id);
}
pub fn len(&self) -> usize {
self.embeddings.len()
}
pub fn is_empty(&self) -> bool {
self.embeddings.is_empty()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct SerializableNode {
index: u32,
node: Node,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct SerializableEdge {
source: u32,
target: u32,
edge: Edge,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct SerializablePDG {
nodes: Vec<SerializableNode>,
edges: Vec<SerializableEdge>,
symbol_index: HashMap<String, u32>,
file_index: HashMap<String, Vec<u32>>,
#[serde(default)]
name_index: HashMap<String, Vec<u32>>,
#[serde(default)]
name_lower_index: HashMap<String, Vec<u32>>,
#[serde(default)]
embeddings: HashMap<String, Vec<f32>>,
}
impl SerializablePDG {
fn from_pdg(pdg: &ProgramDependenceGraph) -> Self {
let nodes = pdg
.graph
.node_indices()
.map(|idx| SerializableNode {
index: idx.index() as u32,
node: pdg.graph[idx].clone(),
})
.collect();
let edges = pdg
.graph
.edge_indices()
.map(|eidx| {
let (source, target) = pdg
.graph
.edge_endpoints(eidx)
.expect("Edge endpoints must exist");
SerializableEdge {
source: source.index() as u32,
target: target.index() as u32,
edge: pdg.graph[eidx].clone(),
}
})
.collect();
let symbol_index = pdg
.symbol_index
.iter()
.map(|(k, v)| (k.clone(), v.index() as u32))
.collect();
let file_index = pdg
.file_index
.iter()
.map(|(k, v)| (k.clone(), v.iter().map(|id| id.index() as u32).collect()))
.collect();
let name_index = pdg
.name_index
.iter()
.map(|(k, v)| (k.clone(), v.iter().map(|id| id.index() as u32).collect()))
.collect();
let name_lower_index = pdg
.name_lower_index
.iter()
.map(|(k, v)| (k.clone(), v.iter().map(|id| id.index() as u32).collect()))
.collect();
Self {
nodes,
edges,
symbol_index,
file_index,
name_index,
name_lower_index,
embeddings: pdg.embedding_store.embeddings.clone(),
}
}
fn to_pdg(&self) -> Result<ProgramDependenceGraph, String> {
let mut pdg = ProgramDependenceGraph::new();
let index_map = self.restore_nodes(&mut pdg);
self.restore_indexes(&mut pdg, &index_map);
self.restore_edges(&mut pdg, &index_map)?;
self.restore_embeddings(&mut pdg);
Self::rebuild_name_file_index(&mut pdg);
if pdg.trigram_index.is_empty() {
pdg.rebuild_trigram_index();
}
Ok(pdg)
}
fn restore_nodes(&self, pdg: &mut ProgramDependenceGraph) -> HashMap<u32, NodeId> {
self.nodes
.iter()
.map(|serialized| {
let node_id = pdg.graph.add_node(serialized.node.clone());
(serialized.index, node_id)
})
.collect()
}
fn restore_indexes(&self, pdg: &mut ProgramDependenceGraph, index_map: &HashMap<u32, NodeId>) {
Self::restore_symbol_index(&mut pdg.symbol_index, &self.symbol_index, index_map);
Self::restore_node_index(&mut pdg.file_index, &self.file_index, index_map);
Self::restore_node_index(&mut pdg.name_index, &self.name_index, index_map);
Self::restore_node_index(&mut pdg.name_lower_index, &self.name_lower_index, index_map);
if pdg.name_index.is_empty() {
Self::rebuild_name_indexes(pdg);
}
}
fn restore_symbol_index(
destination: &mut HashMap<String, NodeId>,
source: &HashMap<String, u32>,
index_map: &HashMap<u32, NodeId>,
) {
for (symbol, old_index) in source {
if let Some(&node_id) = index_map.get(old_index) {
destination.insert(symbol.clone(), node_id);
}
}
}
fn restore_node_index(
destination: &mut HashMap<String, Vec<NodeId>>,
source: &HashMap<String, Vec<u32>>,
index_map: &HashMap<u32, NodeId>,
) {
for (name, old_indices) in source {
let node_ids: Vec<NodeId> = old_indices
.iter()
.filter_map(|index| index_map.get(index).copied())
.collect();
if !node_ids.is_empty() {
destination.insert(name.clone(), node_ids);
}
}
}
fn rebuild_name_indexes(pdg: &mut ProgramDependenceGraph) {
for node_id in pdg.graph.node_indices() {
if let Some(node) = pdg.graph.node_weight(node_id) {
pdg.name_index
.entry(node.name.clone())
.or_default()
.push(node_id);
pdg.name_lower_index
.entry(node.name.to_lowercase())
.or_default()
.push(node_id);
}
}
}
fn restore_edges(
&self,
pdg: &mut ProgramDependenceGraph,
index_map: &HashMap<u32, NodeId>,
) -> Result<(), String> {
for serialized in &self.edges {
let source = index_map
.get(&serialized.source)
.ok_or_else(|| format!("Missing source {}", serialized.source))?;
let target = index_map
.get(&serialized.target)
.ok_or_else(|| format!("Missing target {}", serialized.target))?;
pdg.graph
.add_edge(*source, *target, serialized.edge.clone());
}
Ok(())
}
fn restore_embeddings(&self, pdg: &mut ProgramDependenceGraph) {
for (node_id, embedding) in &self.embeddings {
pdg.embedding_store.insert(node_id, embedding.clone());
}
}
fn rebuild_name_file_index(pdg: &mut ProgramDependenceGraph) {
for node_id in pdg.graph.node_indices() {
if let Some(node) = pdg.graph.node_weight(node_id) {
pdg.name_file_index
.insert((node.name.clone(), node.file_path.to_string()), node_id);
}
}
}
}
pub struct ProgramDependenceGraph {
pub(crate) graph: StableGraph<Node, Edge>,
pub(crate) symbol_index: HashMap<String, NodeId>,
pub(crate) file_index: HashMap<String, Vec<NodeId>>,
pub(crate) name_index: HashMap<String, Vec<NodeId>>,
pub(crate) name_lower_index: HashMap<String, Vec<NodeId>>,
pub embedding_store: EmbeddingStore,
name_file_index: HashMap<(String, String), NodeId>,
bfs_scratch: Mutex<Vec<NodeId>>,
trigram_index: TrigramIndex,
}
impl Clone for ProgramDependenceGraph {
fn clone(&self) -> Self {
Self {
graph: self.graph.clone(),
symbol_index: self.symbol_index.clone(),
file_index: self.file_index.clone(),
name_index: self.name_index.clone(),
name_lower_index: self.name_lower_index.clone(),
embedding_store: self.embedding_store.clone(),
name_file_index: self.name_file_index.clone(),
bfs_scratch: Mutex::new(Vec::new()),
trigram_index: self.trigram_index.clone(),
}
}
}
impl ProgramDependenceGraph {
pub fn new() -> Self {
Self {
graph: StableGraph::new(),
symbol_index: HashMap::new(),
file_index: HashMap::new(),
name_index: HashMap::new(),
name_lower_index: HashMap::new(),
embedding_store: EmbeddingStore::new(),
name_file_index: HashMap::new(),
bfs_scratch: Mutex::new(Vec::new()),
trigram_index: TrigramIndex::new(),
}
}
pub fn ensure_file_summary_nodes(&mut self) {
use std::collections::{HashMap, HashSet};
let mut file_lang: HashMap<String, String> = HashMap::new();
let mut have_summary: HashSet<String> = HashSet::new();
for ni in self.node_indices() {
if let Some(n) = self.get_node(ni) {
let fp = n.file_path.to_string();
if matches!(n.node_type, NodeType::FileSummary) {
have_summary.insert(fp);
} else {
file_lang.entry(fp).or_insert_with(|| n.language.clone());
}
}
}
for (fp, lang) in file_lang {
if !have_summary.contains(&fp) {
self.add_node(Node::new_file_summary(&fp, &lang));
}
}
}
pub fn add_node(&mut self, node: Node) -> NodeId {
let id = self.graph.add_node(node.clone());
self.symbol_index.insert(node.id.clone(), id);
self.file_index
.entry(node.file_path.to_string())
.or_default()
.push(id);
self.name_index
.entry(node.name.clone())
.or_default()
.push(id);
self.name_lower_index
.entry(node.name.to_lowercase())
.or_default()
.push(id);
self.name_file_index
.insert((node.name.clone(), node.file_path.to_string()), id);
self.trigram_index
.add_node(id, &node.name, &node.id, &node.file_path);
id
}
pub fn add_edge(&mut self, from: NodeId, to: NodeId, edge: Edge) -> EdgeId {
debug_assert!(
self.graph.contains_node(from) && self.graph.contains_node(to),
"add_edge called with invalid NodeId(s): from={:?} to={:?}",
from,
to
);
self.graph.add_edge(from, to, edge)
}
pub fn remove_node(&mut self, node_id: NodeId) -> Option<Node> {
if let Some(node) = self.graph.remove_node(node_id) {
self.symbol_index.remove(&node.id);
self.embedding_store.remove(&node.id);
let remove_file_entry = if let Some(v) = self.file_index.get_mut(&*node.file_path) {
v.retain(|&id| id != node_id);
v.is_empty()
} else {
false
};
if remove_file_entry {
self.file_index.remove(&*node.file_path);
}
let remove_name_entry = if let Some(v) = self.name_index.get_mut(&node.name) {
v.retain(|&id| id != node_id);
v.is_empty()
} else {
false
};
if remove_name_entry {
self.name_index.remove(&node.name);
}
let lower_name = node.name.to_lowercase();
let remove_lower_name_entry =
if let Some(v) = self.name_lower_index.get_mut(&lower_name) {
v.retain(|&id| id != node_id);
v.is_empty()
} else {
false
};
if remove_lower_name_entry {
self.name_lower_index.remove(&lower_name);
}
self.name_file_index
.remove(&(node.name.clone(), node.file_path.to_string()));
self.trigram_index
.remove_node(node_id, &node.name, &node.id, &node.file_path);
Some(node)
} else {
None
}
}
pub fn remove_edge(&mut self, id: EdgeId) -> Option<Edge> {
self.graph.remove_edge(id)
}
pub fn remove_file(&mut self, file_path: &str) {
let ids = self.nodes_in_file(file_path);
for id in ids {
self.remove_node(id);
}
self.file_index.remove(file_path);
}
pub fn get_node(&self, id: NodeId) -> Option<&Node> {
self.graph.node_weight(id)
}
pub fn get_node_mut(&mut self, id: NodeId) -> Option<&mut Node> {
self.graph.node_weight_mut(id)
}
pub fn node_weights_mut(&mut self) -> impl Iterator<Item = &mut Node> {
self.graph.node_weights_mut()
}
pub fn get_edge(&self, id: EdgeId) -> Option<&Edge> {
self.graph.edge_weight(id)
}
pub fn node_count(&self) -> usize {
self.graph.node_count()
}
pub fn edge_count(&self) -> usize {
self.graph.edge_count()
}
pub fn file_count(&self) -> usize {
self.file_index
.values()
.filter(|node_ids| !node_ids.is_empty())
.count()
}
pub fn node_indices(&self) -> impl Iterator<Item = NodeId> + '_ {
self.graph.node_indices()
}
pub fn edge_indices(&self) -> impl Iterator<Item = EdgeId> + '_ {
self.graph.edge_indices()
}
pub fn edge_endpoints(&self, edge_id: EdgeId) -> Option<(NodeId, NodeId)> {
self.graph.edge_endpoints(edge_id)
}
pub fn neighbors(&self, node_id: NodeId) -> Vec<NodeId> {
self.graph.neighbors(node_id).collect()
}
pub fn predecessors(&self, node_id: NodeId) -> Vec<NodeId> {
use petgraph::Direction;
self.graph
.neighbors_directed(node_id, Direction::Incoming)
.collect()
}
pub fn predecessor_count(&self, node_id: NodeId) -> usize {
use petgraph::Direction;
self.graph
.neighbors_directed(node_id, Direction::Incoming)
.count()
}
pub fn find_by_symbol(&self, symbol: &str) -> Option<NodeId> {
self.symbol_index.get(symbol).copied()
}
pub fn find_by_id(&self, node_id: &str) -> Option<NodeId> {
self.symbol_index.get(node_id).copied()
}
pub fn nodes_in_file(&self, file_path: &str) -> Vec<NodeId> {
self.file_index.get(file_path).cloned().unwrap_or_default()
}
pub fn find_by_name(&self, name: &str) -> Option<NodeId> {
self.name_index
.get(name)
.and_then(|ids| ids.first().copied())
}
pub fn find_all_by_name(&self, name: &str) -> Vec<NodeId> {
self.name_index.get(name).cloned().unwrap_or_default()
}
pub fn find_by_name_in_file(&self, name: &str, file_hint: Option<&str>) -> Option<NodeId> {
if let Some(file_path) = file_hint {
if let Some(&node_id) = self
.name_file_index
.get(&(name.to_string(), file_path.to_string()))
{
return Some(node_id);
}
}
if let Some(node_id) = self
.name_index
.get(name)
.and_then(|candidates| self.select_name_candidate(candidates, file_hint))
{
return Some(node_id);
}
let name_lower = name.to_lowercase();
self.name_lower_index
.get(&name_lower)
.and_then(|candidates| self.select_name_candidate(candidates, file_hint))
.or_else(|| self.find_substring_name_match(&name_lower, file_hint))
}
fn select_name_candidate(
&self,
candidates: &[NodeId],
file_hint: Option<&str>,
) -> Option<NodeId> {
if let Some(file_path) = file_hint {
if let Some(node_id) = candidates.iter().copied().find(|node_id| {
self.get_node(*node_id)
.is_some_and(|node| node.file_path.as_ref() == file_path)
}) {
return Some(node_id);
}
}
candidates.first().copied()
}
fn find_substring_name_match(
&self,
name_lower: &str,
file_hint: Option<&str>,
) -> Option<NodeId> {
match file_hint {
Some(file_path) => self
.nodes_in_file(file_path)
.into_iter()
.find(|node_id| self.node_contains_name(*node_id, name_lower)),
None => self
.graph
.node_indices()
.find(|node_id| self.node_contains_name(*node_id, name_lower)),
}
}
fn node_contains_name(&self, node_id: NodeId, name_lower: &str) -> bool {
self.graph.node_weight(node_id).is_some_and(|node| {
node.name.to_lowercase().contains(name_lower)
|| node.id.to_lowercase().contains(name_lower)
})
}
pub fn trigram_index(&self) -> &TrigramIndex {
&self.trigram_index
}
pub fn rebuild_trigram_index(&mut self) {
self.trigram_index = TrigramIndex::build_from_pdg(self);
}
pub fn set_trigram_index(&mut self, index: TrigramIndex) {
self.trigram_index = index;
}
pub fn add_call_edges(&mut self, calls: Vec<(NodeId, NodeId)>) {
for (from, to) in calls {
self.add_edge(
from,
to,
Edge {
edge_type: EdgeType::Call,
metadata: EdgeMetadata::empty(),
},
);
}
}
pub fn add_data_flow_edges(&mut self, flows: Vec<(NodeId, NodeId, String, f32)>) {
for (from, to, var_name, confidence) in flows {
self.add_edge(
from,
to,
Edge {
edge_type: EdgeType::DataDependency,
metadata: EdgeMetadata {
call_count: None,
variable_name: Some(var_name),
confidence: Some(confidence),
channel: None,
position: None,
},
},
);
}
}
pub fn add_inheritance_edges(&mut self, edges: Vec<(NodeId, NodeId, f32)>) {
for (child, parent, confidence) in edges {
self.add_edge(
child,
parent,
Edge {
edge_type: EdgeType::Inheritance,
metadata: EdgeMetadata::with_confidence(confidence),
},
);
}
}
pub fn add_containment_edges(&mut self, edges: Vec<(NodeId, NodeId)>) {
for (container, contained) in edges {
self.add_edge(
container,
contained,
Edge {
edge_type: EdgeType::Containment,
metadata: EdgeMetadata::empty(),
},
);
}
}
pub fn add_import_edges(&mut self, imports: Vec<(NodeId, NodeId)>) {
for (importer, imported) in imports {
self.add_edge(
importer,
imported,
Edge {
edge_type: EdgeType::Import,
metadata: EdgeMetadata::empty(),
},
);
}
}
pub fn set_embedding(&mut self, node_id: &str, embedding: Vec<f32>) {
self.embedding_store.insert(node_id, embedding);
}
pub fn get_embedding(&self, node_id: &str) -> Option<&Vec<f32>> {
self.embedding_store.get(node_id)
}
pub fn embedding_count(&self) -> usize {
self.embedding_store.len()
}
pub fn forward_impact(&self, start: NodeId, config: &TraversalConfig) -> Vec<NodeId> {
self.bfs_directed(start, config, Direction::Forward)
}
pub fn forward_impact_multi_source(
&self,
starts: &HashSet<NodeId>,
config: &TraversalConfig,
) -> Vec<NodeId> {
let mut visited = starts.clone();
let mut ordered_starts = starts.iter().copied().collect::<Vec<_>>();
ordered_starts.sort_by_key(|id| id.index());
let mut queue: VecDeque<(NodeId, usize)> =
ordered_starts.into_iter().map(|id| (id, 0)).collect();
let mut result = Vec::new();
while let Some((current, depth)) = queue.pop_front() {
if let Some(max_nodes) = config.max_nodes {
if result.len() >= max_nodes {
break;
}
}
if !starts.contains(¤t)
&& self
.graph
.node_weight(current)
.is_some_and(|node| config.node_should_collect(node))
{
result.push(current);
}
if config.max_depth.is_some_and(|max_depth| depth >= max_depth) {
continue;
}
let mut scratch = self.bfs_scratch.lock().unwrap();
scratch.clear();
scratch.extend(
self.graph
.edges(current)
.filter(|edge| config.edge_allowed(edge.weight()))
.map(|edge| edge.target()),
);
for &neighbor in scratch.iter() {
if visited.insert(neighbor) {
queue.push_back((neighbor, depth + 1));
}
}
}
result
}
pub fn backward_impact(&self, start: NodeId, config: &TraversalConfig) -> Vec<NodeId> {
self.bfs_directed(start, config, Direction::Backward)
}
pub fn bidirectional_impact(&self, start: NodeId, config: &TraversalConfig) -> Vec<NodeId> {
let forward = self.bfs_directed(start, config, Direction::Forward);
let backward = self.bfs_directed(start, config, Direction::Backward);
let mut combined: HashSet<NodeId> = forward.into_iter().collect();
combined.extend(backward);
combined.remove(&start);
combined.into_iter().collect()
}
fn bfs_directed(&self, start: NodeId, config: &TraversalConfig, dir: Direction) -> Vec<NodeId> {
let mut visited: HashSet<NodeId> = HashSet::new();
let mut queue: VecDeque<(NodeId, usize)> = VecDeque::new();
let mut result: Vec<NodeId> = Vec::new();
visited.insert(start);
queue.push_back((start, 0));
while let Some((current, depth)) = queue.pop_front() {
if let Some(max_n) = config.max_nodes {
if result.len() >= max_n {
break;
}
}
if current != start {
if let Some(node) = self.graph.node_weight(current) {
if config.node_should_collect(node) {
result.push(current);
}
}
}
if let Some(max_d) = config.max_depth {
if depth >= max_d {
continue;
}
}
let mut scratch = self.bfs_scratch.lock().unwrap();
scratch.clear();
match dir {
Direction::Forward => {
scratch.extend(
self.graph
.edges(current)
.filter(|e| config.edge_allowed(e.weight()))
.map(|e| e.target()),
);
}
Direction::Backward => {
use petgraph::Direction as PD;
scratch.extend(
self.graph
.edges_directed(current, PD::Incoming)
.filter(|e| config.edge_allowed(e.weight()))
.map(|e| e.source()),
);
}
}
for &neighbor in scratch.iter() {
if visited.insert(neighbor) {
queue.push_back((neighbor, depth + 1));
}
}
}
result
}
pub fn serialize(&self) -> Result<Vec<u8>, String> {
bincode::serialize(&SerializablePDG::from_pdg(self))
.map_err(|e| format!("Serialize failed: {}", e))
}
pub fn deserialize(data: &[u8]) -> Result<Self, String> {
bincode::deserialize::<SerializablePDG>(data)
.map_err(|e| format!("Deserialize failed: {}", e))
.and_then(|s| s.to_pdg())
}
#[deprecated(
since = "2.0.0",
note = "Use forward_impact with TraversalConfig instead"
)]
pub fn get_forward_impact(&self, node_id: NodeId) -> Vec<NodeId> {
self.forward_impact(node_id, &TraversalConfig::for_impact_analysis())
}
#[deprecated(
since = "2.0.0",
note = "Use backward_impact with TraversalConfig instead"
)]
pub fn get_backward_impact(&self, node_id: NodeId) -> Vec<NodeId> {
self.backward_impact(node_id, &TraversalConfig::for_impact_analysis())
}
#[deprecated(
since = "2.0.0",
note = "Use forward_impact with TraversalConfig instead"
)]
pub fn get_forward_impact_bounded(&self, start: NodeId, max_depth: usize) -> Vec<NodeId> {
let config = TraversalConfig {
max_depth: Some(max_depth),
max_nodes: Some(500),
allowed_edge_types: Some(&[
EdgeType::Call,
EdgeType::DataDependency,
EdgeType::Inheritance,
]),
excluded_node_types: None,
min_complexity: None,
min_edge_confidence: 0.0,
};
self.forward_impact(start, &config)
}
#[deprecated(
since = "2.0.0",
note = "Use backward_impact with TraversalConfig instead"
)]
pub fn get_backward_impact_bounded(&self, start: NodeId, max_depth: usize) -> Vec<NodeId> {
let config = TraversalConfig {
max_depth: Some(max_depth),
max_nodes: Some(500),
allowed_edge_types: Some(&[
EdgeType::Call,
EdgeType::DataDependency,
EdgeType::Inheritance,
]),
excluded_node_types: None,
min_complexity: None,
min_edge_confidence: 0.0,
};
self.backward_impact(start, &config)
}
pub fn add_call_graph_edges(&mut self, calls: Vec<(NodeId, NodeId)>) {
self.add_call_edges(calls);
}
}
impl Default for ProgramDependenceGraph {
fn default() -> Self {
Self::new()
}
}
enum Direction {
Forward,
Backward,
}
#[cfg(test)]
#[path = "pdg_test.rs"]
mod tests;