use std::collections::HashMap;
use std::fmt;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use crate::confidence::ConfidenceMetadata;
use crate::graph::unified::edge::bidirectional::BidirectionalEdgeStore;
use crate::graph::unified::storage::arena::NodeArena;
use crate::graph::unified::storage::indices::AuxiliaryIndices;
use crate::graph::unified::storage::interner::StringInterner;
use crate::graph::unified::storage::metadata::NodeMetadataStore;
use crate::graph::unified::storage::registry::FileRegistry;
#[derive(Clone)]
pub struct CodeGraph {
nodes: Arc<NodeArena>,
edges: Arc<BidirectionalEdgeStore>,
strings: Arc<StringInterner>,
files: Arc<FileRegistry>,
indices: Arc<AuxiliaryIndices>,
macro_metadata: Arc<NodeMetadataStore>,
epoch: u64,
confidence: HashMap<String, ConfidenceMetadata>,
}
impl CodeGraph {
#[must_use]
pub fn new() -> Self {
Self {
nodes: Arc::new(NodeArena::new()),
edges: Arc::new(BidirectionalEdgeStore::new()),
strings: Arc::new(StringInterner::new()),
files: Arc::new(FileRegistry::new()),
indices: Arc::new(AuxiliaryIndices::new()),
macro_metadata: Arc::new(NodeMetadataStore::new()),
epoch: 0,
confidence: HashMap::new(),
}
}
#[must_use]
pub fn from_components(
nodes: NodeArena,
edges: BidirectionalEdgeStore,
strings: StringInterner,
files: FileRegistry,
indices: AuxiliaryIndices,
macro_metadata: NodeMetadataStore,
) -> Self {
Self {
nodes: Arc::new(nodes),
edges: Arc::new(edges),
strings: Arc::new(strings),
files: Arc::new(files),
indices: Arc::new(indices),
macro_metadata: Arc::new(macro_metadata),
epoch: 0,
confidence: HashMap::new(),
}
}
#[must_use]
pub fn snapshot(&self) -> GraphSnapshot {
GraphSnapshot {
nodes: Arc::clone(&self.nodes),
edges: Arc::clone(&self.edges),
strings: Arc::clone(&self.strings),
files: Arc::clone(&self.files),
indices: Arc::clone(&self.indices),
macro_metadata: Arc::clone(&self.macro_metadata),
epoch: self.epoch,
}
}
#[inline]
#[must_use]
pub fn nodes(&self) -> &NodeArena {
&self.nodes
}
#[inline]
#[must_use]
pub fn edges(&self) -> &BidirectionalEdgeStore {
&self.edges
}
#[inline]
#[must_use]
pub fn strings(&self) -> &StringInterner {
&self.strings
}
#[inline]
#[must_use]
pub fn files(&self) -> &FileRegistry {
&self.files
}
#[inline]
#[must_use]
pub fn indices(&self) -> &AuxiliaryIndices {
&self.indices
}
#[inline]
#[must_use]
pub fn macro_metadata(&self) -> &NodeMetadataStore {
&self.macro_metadata
}
#[inline]
#[must_use]
pub fn epoch(&self) -> u64 {
self.epoch
}
#[inline]
pub fn nodes_mut(&mut self) -> &mut NodeArena {
Arc::make_mut(&mut self.nodes)
}
#[inline]
pub fn edges_mut(&mut self) -> &mut BidirectionalEdgeStore {
Arc::make_mut(&mut self.edges)
}
#[inline]
pub fn strings_mut(&mut self) -> &mut StringInterner {
Arc::make_mut(&mut self.strings)
}
#[inline]
pub fn files_mut(&mut self) -> &mut FileRegistry {
Arc::make_mut(&mut self.files)
}
#[inline]
pub fn indices_mut(&mut self) -> &mut AuxiliaryIndices {
Arc::make_mut(&mut self.indices)
}
#[inline]
pub fn macro_metadata_mut(&mut self) -> &mut NodeMetadataStore {
Arc::make_mut(&mut self.macro_metadata)
}
#[inline]
pub fn nodes_and_strings_mut(&mut self) -> (&mut NodeArena, &mut StringInterner) {
(
Arc::make_mut(&mut self.nodes),
Arc::make_mut(&mut self.strings),
)
}
pub fn rebuild_indices(&mut self) {
let nodes = &self.nodes;
Arc::make_mut(&mut self.indices).build_from_arena(nodes);
}
#[inline]
pub fn bump_epoch(&mut self) -> u64 {
self.epoch = self.epoch.wrapping_add(1);
self.epoch
}
#[inline]
pub fn set_epoch(&mut self, epoch: u64) {
self.epoch = epoch;
}
#[inline]
#[must_use]
pub fn node_count(&self) -> usize {
self.nodes.len()
}
#[inline]
#[must_use]
pub fn edge_count(&self) -> usize {
let stats = self.edges.stats();
stats.forward.csr_edge_count + stats.forward.delta_edge_count
}
#[inline]
#[must_use]
pub fn is_empty(&self) -> bool {
self.nodes.is_empty()
}
#[inline]
pub fn indexed_files(
&self,
) -> impl Iterator<Item = (crate::graph::unified::file::FileId, &std::path::Path)> {
self.files
.iter()
.map(|(id, arc_path)| (id, arc_path.as_ref()))
}
#[inline]
#[must_use]
pub fn confidence(&self) -> &HashMap<String, ConfidenceMetadata> {
&self.confidence
}
pub fn merge_confidence(&mut self, language: &str, metadata: ConfidenceMetadata) {
use crate::confidence::ConfidenceLevel;
self.confidence
.entry(language.to_string())
.and_modify(|existing| {
let new_level = match (&existing.level, &metadata.level) {
(ConfidenceLevel::Verified, other) | (other, ConfidenceLevel::Verified) => {
*other
}
(ConfidenceLevel::Partial, ConfidenceLevel::AstOnly)
| (ConfidenceLevel::AstOnly, ConfidenceLevel::Partial) => {
ConfidenceLevel::AstOnly
}
(level, _) => *level,
};
existing.level = new_level;
for limitation in &metadata.limitations {
if !existing.limitations.contains(limitation) {
existing.limitations.push(limitation.clone());
}
}
for feature in &metadata.unavailable_features {
if !existing.unavailable_features.contains(feature) {
existing.unavailable_features.push(feature.clone());
}
}
})
.or_insert(metadata);
}
pub fn set_confidence(&mut self, confidence: HashMap<String, ConfidenceMetadata>) {
self.confidence = confidence;
}
}
impl Default for CodeGraph {
fn default() -> Self {
Self::new()
}
}
impl fmt::Debug for CodeGraph {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CodeGraph")
.field("nodes", &self.nodes.len())
.field("epoch", &self.epoch)
.finish_non_exhaustive()
}
}
pub struct ConcurrentCodeGraph {
inner: RwLock<CodeGraph>,
epoch: AtomicU64,
}
impl ConcurrentCodeGraph {
#[must_use]
pub fn new() -> Self {
Self {
inner: RwLock::new(CodeGraph::new()),
epoch: AtomicU64::new(0),
}
}
#[must_use]
pub fn from_graph(graph: CodeGraph) -> Self {
let epoch = graph.epoch();
Self {
inner: RwLock::new(graph),
epoch: AtomicU64::new(epoch),
}
}
#[inline]
pub fn read(&self) -> RwLockReadGuard<'_, CodeGraph> {
self.inner.read()
}
#[inline]
pub fn write(&self) -> RwLockWriteGuard<'_, CodeGraph> {
self.epoch.fetch_add(1, Ordering::SeqCst);
let mut guard = self.inner.write();
guard.set_epoch(self.epoch.load(Ordering::SeqCst));
guard
}
#[inline]
#[must_use]
pub fn epoch(&self) -> u64 {
self.epoch.load(Ordering::SeqCst)
}
#[must_use]
pub fn snapshot(&self) -> GraphSnapshot {
self.inner.read().snapshot()
}
#[inline]
#[must_use]
pub fn try_read(&self) -> Option<RwLockReadGuard<'_, CodeGraph>> {
self.inner.try_read()
}
#[inline]
pub fn try_write(&self) -> Option<RwLockWriteGuard<'_, CodeGraph>> {
self.inner.try_write().map(|mut guard| {
self.epoch.fetch_add(1, Ordering::SeqCst);
guard.set_epoch(self.epoch.load(Ordering::SeqCst));
guard
})
}
}
impl Default for ConcurrentCodeGraph {
fn default() -> Self {
Self::new()
}
}
impl fmt::Debug for ConcurrentCodeGraph {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ConcurrentCodeGraph")
.field("epoch", &self.epoch.load(Ordering::SeqCst))
.finish_non_exhaustive()
}
}
#[derive(Clone)]
pub struct GraphSnapshot {
nodes: Arc<NodeArena>,
edges: Arc<BidirectionalEdgeStore>,
strings: Arc<StringInterner>,
files: Arc<FileRegistry>,
indices: Arc<AuxiliaryIndices>,
macro_metadata: Arc<NodeMetadataStore>,
epoch: u64,
}
impl GraphSnapshot {
#[inline]
#[must_use]
pub fn nodes(&self) -> &NodeArena {
&self.nodes
}
#[inline]
#[must_use]
pub fn edges(&self) -> &BidirectionalEdgeStore {
&self.edges
}
#[inline]
#[must_use]
pub fn strings(&self) -> &StringInterner {
&self.strings
}
#[inline]
#[must_use]
pub fn files(&self) -> &FileRegistry {
&self.files
}
#[inline]
#[must_use]
pub fn indices(&self) -> &AuxiliaryIndices {
&self.indices
}
#[inline]
#[must_use]
pub fn macro_metadata(&self) -> &NodeMetadataStore {
&self.macro_metadata
}
#[inline]
#[must_use]
pub fn epoch(&self) -> u64 {
self.epoch
}
#[inline]
#[must_use]
pub fn epoch_matches(&self, other_epoch: u64) -> bool {
self.epoch == other_epoch
}
#[must_use]
pub fn find_by_pattern(&self, pattern: &str) -> Vec<crate::graph::unified::node::NodeId> {
let mut matches = Vec::new();
for (str_id, s) in self.strings.iter() {
if s.contains(pattern) {
matches.extend_from_slice(self.indices.by_qualified_name(str_id));
matches.extend_from_slice(self.indices.by_name(str_id));
}
}
matches.sort_unstable();
matches.dedup();
matches
}
#[must_use]
pub fn get_callees(
&self,
node: crate::graph::unified::node::NodeId,
) -> Vec<crate::graph::unified::node::NodeId> {
use crate::graph::unified::edge::EdgeKind;
self.edges
.edges_from(node)
.into_iter()
.filter(|edge| matches!(edge.kind, EdgeKind::Calls { .. }))
.map(|edge| edge.target)
.collect()
}
#[must_use]
pub fn get_callers(
&self,
node: crate::graph::unified::node::NodeId,
) -> Vec<crate::graph::unified::node::NodeId> {
use crate::graph::unified::edge::EdgeKind;
self.edges
.edges_to(node)
.into_iter()
.filter(|edge| matches!(edge.kind, EdgeKind::Calls { .. }))
.map(|edge| edge.source)
.collect()
}
pub fn iter_nodes(
&self,
) -> impl Iterator<
Item = (
crate::graph::unified::node::NodeId,
&crate::graph::unified::storage::arena::NodeEntry,
),
> {
self.nodes.iter()
}
pub fn iter_edges(
&self,
) -> impl Iterator<
Item = (
crate::graph::unified::node::NodeId,
crate::graph::unified::node::NodeId,
crate::graph::unified::edge::EdgeKind,
),
> + '_ {
self.nodes.iter().flat_map(move |(node_id, _entry)| {
self.edges
.edges_from(node_id)
.into_iter()
.map(move |edge| (node_id, edge.target, edge.kind))
})
}
#[must_use]
pub fn get_node(
&self,
id: crate::graph::unified::node::NodeId,
) -> Option<&crate::graph::unified::storage::arena::NodeEntry> {
self.nodes.get(id)
}
}
impl fmt::Debug for GraphSnapshot {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("GraphSnapshot")
.field("nodes", &self.nodes.len())
.field("epoch", &self.epoch)
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::unified::{
FileScope, NodeId, ResolutionMode, SymbolCandidateOutcome, SymbolQuery,
SymbolResolutionOutcome,
};
fn resolve_symbol_strict(snapshot: &GraphSnapshot, symbol: &str) -> Option<NodeId> {
match snapshot.resolve_symbol(&SymbolQuery {
symbol,
file_scope: FileScope::Any,
mode: ResolutionMode::Strict,
}) {
SymbolResolutionOutcome::Resolved(node_id) => Some(node_id),
SymbolResolutionOutcome::NotFound
| SymbolResolutionOutcome::FileNotIndexed
| SymbolResolutionOutcome::Ambiguous(_) => None,
}
}
fn candidate_nodes(snapshot: &GraphSnapshot, symbol: &str) -> Vec<NodeId> {
match snapshot.find_symbol_candidates(&SymbolQuery {
symbol,
file_scope: FileScope::Any,
mode: ResolutionMode::AllowSuffixCandidates,
}) {
SymbolCandidateOutcome::Candidates(candidates) => candidates,
SymbolCandidateOutcome::NotFound | SymbolCandidateOutcome::FileNotIndexed => Vec::new(),
}
}
#[test]
fn test_code_graph_new() {
let graph = CodeGraph::new();
assert_eq!(graph.epoch(), 0);
assert_eq!(graph.nodes().len(), 0);
}
#[test]
fn test_code_graph_default() {
let graph = CodeGraph::default();
assert_eq!(graph.epoch(), 0);
}
#[test]
fn test_code_graph_snapshot() {
let graph = CodeGraph::new();
let snapshot = graph.snapshot();
assert_eq!(snapshot.epoch(), 0);
assert_eq!(snapshot.nodes().len(), 0);
}
#[test]
fn test_code_graph_bump_epoch() {
let mut graph = CodeGraph::new();
assert_eq!(graph.epoch(), 0);
assert_eq!(graph.bump_epoch(), 1);
assert_eq!(graph.epoch(), 1);
assert_eq!(graph.bump_epoch(), 2);
assert_eq!(graph.epoch(), 2);
}
#[test]
fn test_code_graph_set_epoch() {
let mut graph = CodeGraph::new();
graph.set_epoch(42);
assert_eq!(graph.epoch(), 42);
}
#[test]
fn test_code_graph_from_components() {
let nodes = NodeArena::new();
let edges = BidirectionalEdgeStore::new();
let strings = StringInterner::new();
let files = FileRegistry::new();
let indices = AuxiliaryIndices::new();
let macro_metadata = NodeMetadataStore::new();
let graph =
CodeGraph::from_components(nodes, edges, strings, files, indices, macro_metadata);
assert_eq!(graph.epoch(), 0);
}
#[test]
fn test_code_graph_mut_accessors() {
let mut graph = CodeGraph::new();
let _nodes = graph.nodes_mut();
let _edges = graph.edges_mut();
let _strings = graph.strings_mut();
let _files = graph.files_mut();
let _indices = graph.indices_mut();
}
#[test]
fn test_code_graph_snapshot_isolation() {
let mut graph = CodeGraph::new();
let snapshot1 = graph.snapshot();
graph.bump_epoch();
let snapshot2 = graph.snapshot();
assert_eq!(snapshot1.epoch(), 0);
assert_eq!(snapshot2.epoch(), 1);
}
#[test]
fn test_code_graph_debug() {
let graph = CodeGraph::new();
let debug_str = format!("{graph:?}");
assert!(debug_str.contains("CodeGraph"));
assert!(debug_str.contains("epoch"));
}
#[test]
fn test_concurrent_code_graph_new() {
let graph = ConcurrentCodeGraph::new();
assert_eq!(graph.epoch(), 0);
}
#[test]
fn test_concurrent_code_graph_default() {
let graph = ConcurrentCodeGraph::default();
assert_eq!(graph.epoch(), 0);
}
#[test]
fn test_concurrent_code_graph_from_graph() {
let mut inner = CodeGraph::new();
inner.set_epoch(10);
let graph = ConcurrentCodeGraph::from_graph(inner);
assert_eq!(graph.epoch(), 10);
}
#[test]
fn test_concurrent_code_graph_read() {
let graph = ConcurrentCodeGraph::new();
let guard = graph.read();
assert_eq!(guard.epoch(), 0);
assert_eq!(guard.nodes().len(), 0);
}
#[test]
fn test_concurrent_code_graph_write_increments_epoch() {
let graph = ConcurrentCodeGraph::new();
assert_eq!(graph.epoch(), 0);
{
let guard = graph.write();
assert_eq!(guard.epoch(), 1);
}
assert_eq!(graph.epoch(), 1);
{
let _guard = graph.write();
}
assert_eq!(graph.epoch(), 2);
}
#[test]
fn test_concurrent_code_graph_snapshot() {
let graph = ConcurrentCodeGraph::new();
{
let _guard = graph.write();
}
let snapshot = graph.snapshot();
assert_eq!(snapshot.epoch(), 1);
}
#[test]
fn test_concurrent_code_graph_try_read() {
let graph = ConcurrentCodeGraph::new();
let guard = graph.try_read();
assert!(guard.is_some());
}
#[test]
fn test_concurrent_code_graph_try_write() {
let graph = ConcurrentCodeGraph::new();
let guard = graph.try_write();
assert!(guard.is_some());
assert_eq!(graph.epoch(), 1);
}
#[test]
fn test_concurrent_code_graph_debug() {
let graph = ConcurrentCodeGraph::new();
let debug_str = format!("{graph:?}");
assert!(debug_str.contains("ConcurrentCodeGraph"));
assert!(debug_str.contains("epoch"));
}
#[test]
fn test_graph_snapshot_accessors() {
let graph = CodeGraph::new();
let snapshot = graph.snapshot();
let _nodes = snapshot.nodes();
let _edges = snapshot.edges();
let _strings = snapshot.strings();
let _files = snapshot.files();
let _indices = snapshot.indices();
let _epoch = snapshot.epoch();
}
#[test]
fn test_graph_snapshot_epoch_matches() {
let graph = CodeGraph::new();
let snapshot = graph.snapshot();
assert!(snapshot.epoch_matches(0));
assert!(!snapshot.epoch_matches(1));
}
#[test]
fn test_graph_snapshot_clone() {
let graph = CodeGraph::new();
let snapshot1 = graph.snapshot();
let snapshot2 = snapshot1.clone();
assert_eq!(snapshot1.epoch(), snapshot2.epoch());
}
#[test]
fn test_graph_snapshot_debug() {
let graph = CodeGraph::new();
let snapshot = graph.snapshot();
let debug_str = format!("{snapshot:?}");
assert!(debug_str.contains("GraphSnapshot"));
assert!(debug_str.contains("epoch"));
}
#[test]
fn test_multiple_readers() {
let graph = ConcurrentCodeGraph::new();
let guard1 = graph.read();
let guard2 = graph.read();
let guard3 = graph.read();
assert_eq!(guard1.epoch(), 0);
assert_eq!(guard2.epoch(), 0);
assert_eq!(guard3.epoch(), 0);
}
#[test]
fn test_code_graph_clone() {
let mut graph = CodeGraph::new();
graph.bump_epoch();
let cloned = graph.clone();
assert_eq!(cloned.epoch(), 1);
}
#[test]
fn test_epoch_wrapping() {
let mut graph = CodeGraph::new();
graph.set_epoch(u64::MAX);
let new_epoch = graph.bump_epoch();
assert_eq!(new_epoch, 0); }
#[test]
fn test_snapshot_resolve_symbol() {
use crate::graph::unified::node::NodeKind;
use crate::graph::unified::storage::arena::NodeEntry;
use std::path::Path;
let mut graph = CodeGraph::new();
let name_id = graph.strings_mut().intern("test_func").unwrap();
let qual_name_id = graph.strings_mut().intern("module::test_func").unwrap();
let file_id = graph.files_mut().register(Path::new("test.rs")).unwrap();
let entry =
NodeEntry::new(NodeKind::Function, name_id, file_id).with_qualified_name(qual_name_id);
let node_id = graph.nodes_mut().alloc(entry).unwrap();
graph.indices_mut().add(
node_id,
NodeKind::Function,
name_id,
Some(qual_name_id),
file_id,
);
let snapshot = graph.snapshot();
let found = resolve_symbol_strict(&snapshot, "module::test_func");
assert_eq!(found, Some(node_id));
let found2 = resolve_symbol_strict(&snapshot, "test_func");
assert_eq!(found2, Some(node_id));
assert!(resolve_symbol_strict(&snapshot, "nonexistent").is_none());
}
#[test]
fn test_snapshot_find_by_pattern() {
use crate::graph::unified::node::NodeKind;
use crate::graph::unified::storage::arena::NodeEntry;
use std::path::Path;
let mut graph = CodeGraph::new();
let name1 = graph.strings_mut().intern("foo_bar").unwrap();
let name2 = graph.strings_mut().intern("baz_bar").unwrap();
let name3 = graph.strings_mut().intern("qux_test").unwrap();
let file_id = graph.files_mut().register(Path::new("test.rs")).unwrap();
let node1 = graph
.nodes_mut()
.alloc(NodeEntry::new(NodeKind::Function, name1, file_id))
.unwrap();
let node2 = graph
.nodes_mut()
.alloc(NodeEntry::new(NodeKind::Function, name2, file_id))
.unwrap();
let node3 = graph
.nodes_mut()
.alloc(NodeEntry::new(NodeKind::Function, name3, file_id))
.unwrap();
graph
.indices_mut()
.add(node1, NodeKind::Function, name1, None, file_id);
graph
.indices_mut()
.add(node2, NodeKind::Function, name2, None, file_id);
graph
.indices_mut()
.add(node3, NodeKind::Function, name3, None, file_id);
let snapshot = graph.snapshot();
let matches = snapshot.find_by_pattern("bar");
assert_eq!(matches.len(), 2);
assert!(matches.contains(&node1));
assert!(matches.contains(&node2));
let matches = snapshot.find_by_pattern("qux");
assert_eq!(matches.len(), 1);
assert_eq!(matches[0], node3);
let matches = snapshot.find_by_pattern("nonexistent");
assert!(matches.is_empty());
}
#[test]
fn test_snapshot_get_callees() {
use crate::graph::unified::edge::EdgeKind;
use crate::graph::unified::node::NodeKind;
use crate::graph::unified::storage::arena::NodeEntry;
use std::path::Path;
let mut graph = CodeGraph::new();
let caller_name = graph.strings_mut().intern("caller").unwrap();
let callee1_name = graph.strings_mut().intern("callee1").unwrap();
let callee2_name = graph.strings_mut().intern("callee2").unwrap();
let file_id = graph.files_mut().register(Path::new("test.rs")).unwrap();
let caller_id = graph
.nodes_mut()
.alloc(NodeEntry::new(NodeKind::Function, caller_name, file_id))
.unwrap();
let callee1_id = graph
.nodes_mut()
.alloc(NodeEntry::new(NodeKind::Function, callee1_name, file_id))
.unwrap();
let callee2_id = graph
.nodes_mut()
.alloc(NodeEntry::new(NodeKind::Function, callee2_name, file_id))
.unwrap();
graph.edges_mut().add_edge(
caller_id,
callee1_id,
EdgeKind::Calls {
argument_count: 0,
is_async: false,
},
file_id,
);
graph.edges_mut().add_edge(
caller_id,
callee2_id,
EdgeKind::Calls {
argument_count: 0,
is_async: false,
},
file_id,
);
let snapshot = graph.snapshot();
let callees = snapshot.get_callees(caller_id);
assert_eq!(callees.len(), 2);
assert!(callees.contains(&callee1_id));
assert!(callees.contains(&callee2_id));
let callees = snapshot.get_callees(callee1_id);
assert!(callees.is_empty());
}
#[test]
fn test_snapshot_get_callers() {
use crate::graph::unified::edge::EdgeKind;
use crate::graph::unified::node::NodeKind;
use crate::graph::unified::storage::arena::NodeEntry;
use std::path::Path;
let mut graph = CodeGraph::new();
let caller1_name = graph.strings_mut().intern("caller1").unwrap();
let caller2_name = graph.strings_mut().intern("caller2").unwrap();
let callee_name = graph.strings_mut().intern("callee").unwrap();
let file_id = graph.files_mut().register(Path::new("test.rs")).unwrap();
let caller1_id = graph
.nodes_mut()
.alloc(NodeEntry::new(NodeKind::Function, caller1_name, file_id))
.unwrap();
let caller2_id = graph
.nodes_mut()
.alloc(NodeEntry::new(NodeKind::Function, caller2_name, file_id))
.unwrap();
let callee_id = graph
.nodes_mut()
.alloc(NodeEntry::new(NodeKind::Function, callee_name, file_id))
.unwrap();
graph.edges_mut().add_edge(
caller1_id,
callee_id,
EdgeKind::Calls {
argument_count: 0,
is_async: false,
},
file_id,
);
graph.edges_mut().add_edge(
caller2_id,
callee_id,
EdgeKind::Calls {
argument_count: 0,
is_async: false,
},
file_id,
);
let snapshot = graph.snapshot();
let callers = snapshot.get_callers(callee_id);
assert_eq!(callers.len(), 2);
assert!(callers.contains(&caller1_id));
assert!(callers.contains(&caller2_id));
let callers = snapshot.get_callers(caller1_id);
assert!(callers.is_empty());
}
#[test]
fn test_snapshot_find_symbol_candidates() {
use crate::graph::unified::node::NodeKind;
use crate::graph::unified::storage::arena::NodeEntry;
use std::path::Path;
let mut graph = CodeGraph::new();
let symbol_name = graph.strings_mut().intern("test").unwrap();
let file_id = graph.files_mut().register(Path::new("test.rs")).unwrap();
let node1 = graph
.nodes_mut()
.alloc(NodeEntry::new(NodeKind::Function, symbol_name, file_id))
.unwrap();
let node2 = graph
.nodes_mut()
.alloc(NodeEntry::new(NodeKind::Method, symbol_name, file_id))
.unwrap();
let other_name = graph.strings_mut().intern("other").unwrap();
let node3 = graph
.nodes_mut()
.alloc(NodeEntry::new(NodeKind::Function, other_name, file_id))
.unwrap();
graph
.indices_mut()
.add(node1, NodeKind::Function, symbol_name, None, file_id);
graph
.indices_mut()
.add(node2, NodeKind::Method, symbol_name, None, file_id);
graph
.indices_mut()
.add(node3, NodeKind::Function, other_name, None, file_id);
let snapshot = graph.snapshot();
let matches = candidate_nodes(&snapshot, "test");
assert_eq!(matches.len(), 2);
assert!(matches.contains(&node1));
assert!(matches.contains(&node2));
let matches = candidate_nodes(&snapshot, "other");
assert_eq!(matches.len(), 1);
assert_eq!(matches[0], node3);
let matches = candidate_nodes(&snapshot, "nonexistent");
assert!(matches.is_empty());
}
#[test]
fn test_snapshot_iter_nodes() {
use crate::graph::unified::node::NodeKind;
use crate::graph::unified::storage::arena::NodeEntry;
use std::path::Path;
let mut graph = CodeGraph::new();
let name1 = graph.strings_mut().intern("func1").unwrap();
let name2 = graph.strings_mut().intern("func2").unwrap();
let file_id = graph.files_mut().register(Path::new("test.rs")).unwrap();
let node1 = graph
.nodes_mut()
.alloc(NodeEntry::new(NodeKind::Function, name1, file_id))
.unwrap();
let node2 = graph
.nodes_mut()
.alloc(NodeEntry::new(NodeKind::Function, name2, file_id))
.unwrap();
let snapshot = graph.snapshot();
let snapshot_nodes: Vec<_> = snapshot.iter_nodes().collect();
assert_eq!(snapshot_nodes.len(), 2);
let node_ids: Vec<_> = snapshot_nodes.iter().map(|(id, _)| *id).collect();
assert!(node_ids.contains(&node1));
assert!(node_ids.contains(&node2));
}
#[test]
fn test_snapshot_iter_edges() {
use crate::graph::unified::edge::EdgeKind;
use crate::graph::unified::node::NodeKind;
use crate::graph::unified::storage::arena::NodeEntry;
use std::path::Path;
let mut graph = CodeGraph::new();
let name1 = graph.strings_mut().intern("func1").unwrap();
let name2 = graph.strings_mut().intern("func2").unwrap();
let file_id = graph.files_mut().register(Path::new("test.rs")).unwrap();
let node1 = graph
.nodes_mut()
.alloc(NodeEntry::new(NodeKind::Function, name1, file_id))
.unwrap();
let node2 = graph
.nodes_mut()
.alloc(NodeEntry::new(NodeKind::Function, name2, file_id))
.unwrap();
graph.edges_mut().add_edge(
node1,
node2,
EdgeKind::Calls {
argument_count: 0,
is_async: false,
},
file_id,
);
let snapshot = graph.snapshot();
let edges: Vec<_> = snapshot.iter_edges().collect();
assert_eq!(edges.len(), 1);
let (src, tgt, kind) = &edges[0];
assert_eq!(*src, node1);
assert_eq!(*tgt, node2);
assert!(matches!(
kind,
EdgeKind::Calls {
argument_count: 0,
is_async: false
}
));
}
#[test]
fn test_snapshot_get_node() {
use crate::graph::unified::node::NodeId;
use crate::graph::unified::node::NodeKind;
use crate::graph::unified::storage::arena::NodeEntry;
use std::path::Path;
let mut graph = CodeGraph::new();
let name = graph.strings_mut().intern("test_func").unwrap();
let file_id = graph.files_mut().register(Path::new("test.rs")).unwrap();
let node_id = graph
.nodes_mut()
.alloc(NodeEntry::new(NodeKind::Function, name, file_id))
.unwrap();
let snapshot = graph.snapshot();
let entry = snapshot.get_node(node_id);
assert!(entry.is_some());
assert_eq!(entry.unwrap().kind, NodeKind::Function);
let invalid_id = NodeId::INVALID;
assert!(snapshot.get_node(invalid_id).is_none());
}
#[test]
fn test_snapshot_query_empty_graph() {
use crate::graph::unified::node::NodeId;
let graph = CodeGraph::new();
let snapshot = graph.snapshot();
assert!(resolve_symbol_strict(&snapshot, "test").is_none());
assert!(snapshot.find_by_pattern("test").is_empty());
assert!(candidate_nodes(&snapshot, "test").is_empty());
let dummy_id = NodeId::new(0, 1);
assert!(snapshot.get_callees(dummy_id).is_empty());
assert!(snapshot.get_callers(dummy_id).is_empty());
assert_eq!(snapshot.iter_nodes().count(), 0);
assert_eq!(snapshot.iter_edges().count(), 0);
}
#[test]
fn test_snapshot_edge_filtering_by_kind() {
use crate::graph::unified::edge::EdgeKind;
use crate::graph::unified::node::NodeKind;
use crate::graph::unified::storage::arena::NodeEntry;
use std::path::Path;
let mut graph = CodeGraph::new();
let name1 = graph.strings_mut().intern("func1").unwrap();
let name2 = graph.strings_mut().intern("func2").unwrap();
let file_id = graph.files_mut().register(Path::new("test.rs")).unwrap();
let node1 = graph
.nodes_mut()
.alloc(NodeEntry::new(NodeKind::Function, name1, file_id))
.unwrap();
let node2 = graph
.nodes_mut()
.alloc(NodeEntry::new(NodeKind::Function, name2, file_id))
.unwrap();
graph.edges_mut().add_edge(
node1,
node2,
EdgeKind::Calls {
argument_count: 0,
is_async: false,
},
file_id,
);
graph
.edges_mut()
.add_edge(node1, node2, EdgeKind::References, file_id);
let snapshot = graph.snapshot();
let callees = snapshot.get_callees(node1);
assert_eq!(callees.len(), 1);
assert_eq!(callees[0], node2);
let edges: Vec<_> = snapshot.iter_edges().collect();
assert_eq!(edges.len(), 2);
}
}