use super::ProgramDependenceGraph;
use crate::graph::pdg::NodeType;
use anyhow::Result;
use tracing::{info, warn};
pub(crate) fn merge_pdgs(target: &mut ProgramDependenceGraph, source: ProgramDependenceGraph) {
let mut id_map: std::collections::HashMap<
petgraph::graph::NodeIndex,
petgraph::graph::NodeIndex,
> = std::collections::HashMap::with_capacity(source.node_count());
let (nodes_iter, edges_iter) = source.graph.into_nodes_edges_iters();
for mut node in nodes_iter {
if node.weight.node_type == NodeType::External {
node.weight.file_path =
std::sync::Arc::from(crate::graph::pdg::EXTERNAL_NODE_FILE_PATH);
}
let new_idx = match target.find_by_id(&node.weight.id) {
Some(existing_idx) => {
if node.weight.node_type != NodeType::External {
warn!(
id = %node.weight.id,
"merge_pdgs folded a non-external duplicate node id (first wins)"
);
}
existing_idx
}
None => target.add_node(node.weight),
};
id_map.insert(node.index, new_idx);
}
for edge in edges_iter {
if let (Some(&si), Some(&ti)) = (id_map.get(&edge.source), id_map.get(&edge.target)) {
target.add_edge(si, ti, edge.weight);
}
}
}
pub(crate) fn remove_file_from_pdg(
pdg: &mut ProgramDependenceGraph,
file_path: &str,
) -> Result<()> {
pdg.remove_file(file_path);
Ok(())
}
pub(crate) fn normalize_external_nodes(pdg: &mut ProgramDependenceGraph) {
let to_migrate: Vec<petgraph::graph::NodeIndex> = pdg
.node_indices()
.filter(|idx| {
let Some(node) = pdg.get_node(*idx) else {
return false;
};
let is_external = node.language == "external" || node.language.starts_with("external:");
is_external && node.node_type != NodeType::External
})
.collect();
for idx in &to_migrate {
if let Some(node) = pdg.get_node_mut(*idx) {
node.node_type = NodeType::External;
}
}
let migrated =
pdg.repath_nodes_to_file_path(&to_migrate, crate::graph::pdg::EXTERNAL_NODE_FILE_PATH);
if migrated > 0 {
info!(
"Normalized {} external nodes to NodeType::External",
migrated
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::pdg::{Edge, EdgeMetadata, Node};
fn function(id: &str, file: &str, name: &str, language: &str) -> Node {
Node {
id: id.to_string(),
node_type: NodeType::Function,
name: name.to_string(),
file_path: std::sync::Arc::from(file),
byte_range: (0, 0),
complexity: 0,
language: language.to_string(),
}
}
#[test]
fn test_normalize_external_nodes_repaths_migrated_nodes() {
let mut pdg = ProgramDependenceGraph::new();
let idx = pdg.add_node(function(
"a.rs:ext_thing",
"a.rs",
"ext_thing",
"external:crate::thing",
));
let real = pdg.add_node(function("a.rs:real", "a.rs", "real", "rust"));
normalize_external_nodes(&mut pdg);
let node = pdg.get_node(idx).unwrap();
assert_eq!(node.node_type, NodeType::External);
assert_eq!(
node.file_path.as_ref(),
crate::graph::pdg::EXTERNAL_NODE_FILE_PATH,
"migrated externals leave the per-file namespace"
);
assert!(
!pdg.nodes_in_file("a.rs").contains(&idx),
"file_index must drop the migrated placeholder"
);
assert!(
pdg.nodes_in_file(crate::graph::pdg::EXTERNAL_NODE_FILE_PATH)
.contains(&idx),
"file_index must admit it under the external vocabulary"
);
pdg.remove_file("a.rs");
assert!(
pdg.get_node(idx).is_some(),
"placeholder survives remove_file"
);
assert!(pdg.get_node(real).is_none(), "the real node is gone");
assert_eq!(
pdg.find_by_name_in_file(
"ext_thing",
Some(crate::graph::pdg::EXTERNAL_NODE_FILE_PATH)
),
Some(idx),
);
}
#[test]
fn test_merge_pdgs_canonicalizes_external_paths() {
let mut target = ProgramDependenceGraph::new();
let mut source = ProgramDependenceGraph::new();
let mut external = function("external::log", "a.rs", "log", "rust");
external.node_type = NodeType::External;
source.add_node(external);
merge_pdgs(&mut target, source);
let node = target
.node_indices()
.find_map(|idx| {
let node = target.get_node(idx)?;
(node.id == "external::log").then_some(node)
})
.unwrap();
assert_eq!(
node.file_path.as_ref(),
crate::graph::pdg::EXTERNAL_NODE_FILE_PATH
);
let mut target = ProgramDependenceGraph::new();
target.add_node(function("b.rs:caller", "b.rs", "caller", "rust"));
let mut source = ProgramDependenceGraph::new();
let mut external = function("external::log", "a.rs", "log", "rust");
external.node_type = NodeType::External;
let ext_idx = source.add_node(external);
let caller_idx = source.add_node(function("a.rs:main", "a.rs", "main", "rust"));
source.add_edge(
caller_idx,
ext_idx,
Edge {
edge_type: crate::graph::pdg::EdgeType::Call,
metadata: EdgeMetadata::empty(),
},
);
merge_pdgs(&mut target, source);
assert_eq!(target.node_count(), 3);
}
}