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::memory::{GraphMemorySize, HASHMAP_ENTRY_OVERHEAD};
use crate::graph::unified::storage::arena::NodeArena;
use crate::graph::unified::storage::edge_provenance::{EdgeProvenance, EdgeProvenanceStore};
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::node_provenance::{NodeProvenance, NodeProvenanceStore};
use crate::graph::unified::storage::registry::{FileProvenanceView, FileRegistry};
use crate::graph::unified::string::id::StringId;
#[derive(Clone)]
pub struct CodeGraph {
nodes: Arc<NodeArena>,
edges: Arc<BidirectionalEdgeStore>,
strings: Arc<StringInterner>,
files: Arc<FileRegistry>,
indices: Arc<AuxiliaryIndices>,
macro_metadata: Arc<NodeMetadataStore>,
node_provenance: Arc<NodeProvenanceStore>,
edge_provenance: Arc<EdgeProvenanceStore>,
fact_epoch: u64,
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()),
node_provenance: Arc::new(NodeProvenanceStore::new()),
edge_provenance: Arc::new(EdgeProvenanceStore::new()),
fact_epoch: 0,
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),
node_provenance: Arc::new(NodeProvenanceStore::new()),
edge_provenance: Arc::new(EdgeProvenanceStore::new()),
fact_epoch: 0,
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),
node_provenance: Arc::clone(&self.node_provenance),
edge_provenance: Arc::clone(&self.edge_provenance),
fact_epoch: self.fact_epoch,
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 fact_epoch(&self) -> u64 {
self.fact_epoch
}
#[inline]
#[must_use]
pub fn node_provenance(
&self,
id: crate::graph::unified::node::id::NodeId,
) -> Option<&NodeProvenance> {
self.node_provenance.lookup(id)
}
#[inline]
#[must_use]
pub fn edge_provenance(
&self,
id: crate::graph::unified::edge::id::EdgeId,
) -> Option<&EdgeProvenance> {
self.edge_provenance.lookup(id)
}
#[inline]
#[must_use]
pub fn file_provenance(
&self,
id: crate::graph::unified::file::id::FileId,
) -> Option<FileProvenanceView<'_>> {
self.files.file_provenance(id)
}
pub(crate) fn set_provenance(
&mut self,
node_provenance: NodeProvenanceStore,
edge_provenance: EdgeProvenanceStore,
fact_epoch: u64,
) {
self.node_provenance = Arc::new(node_provenance);
self.edge_provenance = Arc::new(edge_provenance);
self.fact_epoch = fact_epoch;
}
#[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()
}
}
impl GraphMemorySize for CodeGraph {
fn heap_bytes(&self) -> usize {
let mut confidence_bytes = self.confidence.capacity()
* (std::mem::size_of::<String>()
+ std::mem::size_of::<ConfidenceMetadata>()
+ HASHMAP_ENTRY_OVERHEAD);
for (key, meta) in &self.confidence {
confidence_bytes += key.capacity();
confidence_bytes += meta.limitations.capacity() * std::mem::size_of::<String>();
for s in &meta.limitations {
confidence_bytes += s.capacity();
}
confidence_bytes +=
meta.unavailable_features.capacity() * std::mem::size_of::<String>();
for s in &meta.unavailable_features {
confidence_bytes += s.capacity();
}
}
self.nodes.heap_bytes()
+ self.edges.heap_bytes()
+ self.strings.heap_bytes()
+ self.files.heap_bytes()
+ self.indices.heap_bytes()
+ self.macro_metadata.heap_bytes()
+ self.node_provenance.heap_bytes()
+ self.edge_provenance.heap_bytes()
+ confidence_bytes
}
}
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()
}
#[must_use]
pub fn fact_epoch(&self) -> u64 {
self.inner.read().fact_epoch()
}
#[must_use]
pub fn node_provenance(
&self,
id: crate::graph::unified::node::id::NodeId,
) -> Option<NodeProvenance> {
self.inner.read().node_provenance(id).copied()
}
#[must_use]
pub fn edge_provenance(
&self,
id: crate::graph::unified::edge::id::EdgeId,
) -> Option<EdgeProvenance> {
self.inner.read().edge_provenance(id).copied()
}
#[must_use]
pub fn file_provenance(
&self,
id: crate::graph::unified::file::id::FileId,
) -> Option<OwnedFileProvenanceView> {
let guard = self.inner.read();
guard.file_provenance(id).map(|v| OwnedFileProvenanceView {
content_hash: *v.content_hash,
indexed_at: v.indexed_at,
source_uri: v.source_uri,
is_external: v.is_external,
})
}
#[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(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OwnedFileProvenanceView {
pub content_hash: [u8; 32],
pub indexed_at: u64,
pub source_uri: Option<StringId>,
pub is_external: bool,
}
#[derive(Clone)]
pub struct GraphSnapshot {
nodes: Arc<NodeArena>,
edges: Arc<BidirectionalEdgeStore>,
strings: Arc<StringInterner>,
files: Arc<FileRegistry>,
indices: Arc<AuxiliaryIndices>,
macro_metadata: Arc<NodeMetadataStore>,
node_provenance: Arc<NodeProvenanceStore>,
edge_provenance: Arc<EdgeProvenanceStore>,
fact_epoch: u64,
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 fact_epoch(&self) -> u64 {
self.fact_epoch
}
#[inline]
#[must_use]
pub fn node_provenance(
&self,
id: crate::graph::unified::node::id::NodeId,
) -> Option<&NodeProvenance> {
self.node_provenance.lookup(id)
}
#[inline]
#[must_use]
pub fn edge_provenance(
&self,
id: crate::graph::unified::edge::id::EdgeId,
) -> Option<&EdgeProvenance> {
self.edge_provenance.lookup(id)
}
#[inline]
#[must_use]
pub fn file_provenance(
&self,
id: crate::graph::unified::file::id::FileId,
) -> Option<FileProvenanceView<'_>> {
self.files.file_provenance(id)
}
#[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_codegraph_heap_bytes_counts_confidence_inner_strings() {
let mut graph = CodeGraph::new();
graph.set_confidence({
let mut m = HashMap::with_capacity(8);
m.insert("seed".to_string(), ConfidenceMetadata::default());
m
});
let before = graph.heap_bytes();
let before_cap = graph.confidence.capacity();
let lim1 = String::from("no type inference");
let lim2 = String::from("no generic specialization");
let feat1 = String::from("rust-analyzer");
let l1 = lim1.capacity();
let l2 = lim2.capacity();
let f1 = feat1.capacity();
let limitations = vec![lim1, lim2];
let lim_vec_cap = limitations.capacity();
let unavailable_features = vec![feat1];
let feat_vec_cap = unavailable_features.capacity();
let key = String::from("rust");
let key_cap = key.capacity();
graph.confidence.insert(
key,
ConfidenceMetadata {
limitations,
unavailable_features,
..Default::default()
},
);
assert_eq!(
graph.confidence.capacity(),
before_cap,
"prerequisite: confidence HashMap must not rehash during the test insert",
);
let after = graph.heap_bytes();
let expected_inner = key_cap
+ lim_vec_cap * std::mem::size_of::<String>()
+ l1
+ l2
+ feat_vec_cap * std::mem::size_of::<String>()
+ f1;
assert_eq!(
after - before,
expected_inner,
"CodeGraph::heap_bytes must count ConfidenceMetadata inner Vec<String> capacity exactly",
);
}
#[test]
fn test_codegraph_heap_bytes_grows_with_content() {
use crate::graph::unified::node::NodeKind;
use crate::graph::unified::storage::arena::NodeEntry;
use std::path::Path;
let empty = CodeGraph::new();
let empty_bytes = empty.heap_bytes();
assert!(
empty_bytes < 100 * 1024 * 1024,
"empty graph heap_bytes should be <100 MiB, got {empty_bytes}"
);
let mut graph = CodeGraph::new();
for i in 0..32u32 {
let name = format!("sym_{i}");
let qual = format!("module::sym_{i}");
let file = format!("file_{i}.rs");
let name_id = graph.strings_mut().intern(&name).unwrap();
let qual_id = graph.strings_mut().intern(&qual).unwrap();
let file_id = graph.files_mut().register(Path::new(&file)).unwrap();
let entry =
NodeEntry::new(NodeKind::Function, name_id, file_id).with_qualified_name(qual_id);
let node_id = graph.nodes_mut().alloc(entry).unwrap();
graph
.indices_mut()
.add(node_id, NodeKind::Function, name_id, Some(qual_id), file_id);
}
let populated_bytes = graph.heap_bytes();
assert!(
populated_bytes > 0,
"populated graph should report nonzero heap bytes"
);
assert!(
populated_bytes > empty_bytes,
"populated graph ({populated_bytes}) should exceed empty graph ({empty_bytes})"
);
assert!(
populated_bytes < 100 * 1024 * 1024,
"test graph heap_bytes should be <100 MiB, got {populated_bytes}"
);
}
#[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);
}
}