use crate::graph::unified::concurrent::CodeGraph;
use crate::graph::unified::node::NodeKind as UnifiedNodeKind;
use std::path::PathBuf;
pub struct GraphQueryAdapter<'a> {
graph: &'a CodeGraph,
}
#[derive(Debug, Clone)]
pub struct ScopeInfo {
pub node_id: crate::graph::unified::node::NodeId,
pub scope_type: String,
pub name: String,
}
#[derive(Debug, Clone)]
pub struct ReferenceInfo {
pub source_node_id: crate::graph::unified::node::NodeId,
pub reference_kind: crate::graph::unified::edge::EdgeKind,
pub file_path: PathBuf,
pub line: usize,
}
impl<'a> GraphQueryAdapter<'a> {
#[must_use]
pub fn new(graph: &'a CodeGraph) -> Self {
Self { graph }
}
#[must_use]
pub fn get_parent_scope(
&self,
node_id: crate::graph::unified::node::NodeId,
) -> Option<crate::graph::unified::node::NodeId> {
use crate::graph::unified::edge::EdgeKind;
let edges = self.graph.edges();
for edge in edges.edges_to(node_id) {
if matches!(edge.kind, EdgeKind::Contains) {
return Some(edge.source);
}
}
None
}
#[must_use]
pub fn get_ancestor_scopes(
&self,
node_id: crate::graph::unified::node::NodeId,
) -> Vec<crate::graph::unified::node::NodeId> {
let mut ancestors = Vec::new();
let mut current = node_id;
while let Some(parent) = self.get_parent_scope(current) {
ancestors.push(parent);
current = parent;
}
ancestors
}
#[must_use]
pub fn get_scope_info(
&self,
node_id: crate::graph::unified::node::NodeId,
) -> Option<ScopeInfo> {
let arena = self.graph.nodes();
let strings = self.graph.strings();
let entry = arena.get(node_id)?;
let name = strings.resolve(entry.name)?.to_string();
let scope_type = node_kind_to_scope_type(entry.kind);
Some(ScopeInfo {
node_id,
scope_type,
name,
})
}
#[must_use]
pub fn get_containing_scope_info(
&self,
node_id: crate::graph::unified::node::NodeId,
) -> Option<ScopeInfo> {
let parent_id = self.get_parent_scope(node_id)?;
self.get_scope_info(parent_id)
}
#[must_use]
pub fn get_references_to(
&self,
node_id: crate::graph::unified::node::NodeId,
) -> Vec<ReferenceInfo> {
use crate::graph::unified::edge::EdgeKind;
let edges = self.graph.edges();
let arena = self.graph.nodes();
let files = self.graph.files();
let mut refs = Vec::new();
for edge in edges.edges_to(node_id) {
let is_reference = matches!(
&edge.kind,
EdgeKind::References
| EdgeKind::Calls { .. }
| EdgeKind::Imports { .. }
| EdgeKind::FfiCall { .. }
);
if is_reference {
let (file_path, line) = arena
.get(edge.source)
.map(|entry| {
let path = files
.resolve(entry.file)
.map(|s| PathBuf::from(s.as_ref()))
.unwrap_or_default();
(path, entry.start_line as usize)
})
.unwrap_or_default();
refs.push(ReferenceInfo {
source_node_id: edge.source,
reference_kind: edge.kind.clone(),
file_path,
line,
});
}
}
refs
}
#[must_use]
pub fn node_has_references(&self, node_id: crate::graph::unified::node::NodeId) -> bool {
use crate::graph::unified::edge::EdgeKind;
let edges = self.graph.edges();
edges.edges_to(node_id).iter().any(|edge| {
matches!(
&edge.kind,
EdgeKind::References
| EdgeKind::Calls { .. }
| EdgeKind::Imports { .. }
| EdgeKind::FfiCall { .. }
)
})
}
#[must_use]
pub fn find_references_to_symbol(&self, symbol_name: &str) -> Vec<ReferenceInfo> {
let indices = self.graph.indices();
let arena = self.graph.nodes();
let strings = self.graph.strings();
let mut all_refs = Vec::new();
if let Some(string_id) = strings.get(symbol_name) {
for &node_id in indices.by_name(string_id) {
all_refs.extend(self.get_references_to(node_id));
}
}
let suffix = format!("::{symbol_name}");
for (node_id, entry) in arena.iter() {
if let Some(name) = strings.resolve(entry.name)
&& name.ends_with(&suffix)
{
all_refs.extend(self.get_references_to(node_id));
}
}
all_refs
}
#[must_use]
pub fn graph(&self) -> &CodeGraph {
self.graph
}
}
fn node_kind_to_scope_type(kind: UnifiedNodeKind) -> String {
match kind {
UnifiedNodeKind::Function | UnifiedNodeKind::Test => "function".to_string(),
UnifiedNodeKind::Method => "method".to_string(),
UnifiedNodeKind::Class | UnifiedNodeKind::Service => "class".to_string(),
UnifiedNodeKind::Interface | UnifiedNodeKind::Trait => "interface".to_string(),
UnifiedNodeKind::Struct => "struct".to_string(),
UnifiedNodeKind::Enum => "enum".to_string(),
UnifiedNodeKind::Module => "module".to_string(),
UnifiedNodeKind::Macro => "macro".to_string(),
UnifiedNodeKind::Component => "component".to_string(),
UnifiedNodeKind::Resource | UnifiedNodeKind::Endpoint => "resource".to_string(),
UnifiedNodeKind::Variable
| UnifiedNodeKind::Constant
| UnifiedNodeKind::Parameter
| UnifiedNodeKind::Property
| UnifiedNodeKind::EnumVariant
| UnifiedNodeKind::Type
| UnifiedNodeKind::Import
| UnifiedNodeKind::Export
| UnifiedNodeKind::CallSite
| UnifiedNodeKind::Other
| UnifiedNodeKind::Lifetime
| UnifiedNodeKind::StyleRule
| UnifiedNodeKind::StyleAtRule
| UnifiedNodeKind::StyleVariable => kind.as_str().to_lowercase(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::unified::concurrent::CodeGraph;
use crate::graph::unified::edge::BidirectionalEdgeStore;
use crate::graph::unified::edge::{EdgeKind, FfiConvention};
use crate::graph::unified::node::NodeKind;
use crate::graph::unified::storage::{AuxiliaryIndices, FileRegistry};
use crate::graph::unified::storage::{NodeArena, NodeEntry, StringInterner};
use std::path::Path;
fn build_graph_with_ffi_edge() -> (
CodeGraph,
crate::graph::unified::node::NodeId,
crate::graph::unified::node::NodeId,
) {
let mut arena = NodeArena::new();
let edges = BidirectionalEdgeStore::new();
let mut strings = StringInterner::new();
let mut files = FileRegistry::new();
let mut indices = AuxiliaryIndices::new();
let caller_name = strings.intern("caller_fn").unwrap();
let target_name = strings.intern("ffi_target").unwrap();
let file_id = files.register(Path::new("test.r")).unwrap();
let caller_id = arena
.alloc(NodeEntry {
kind: NodeKind::Function,
name: caller_name,
file: file_id,
start_byte: 0,
end_byte: 100,
start_line: 1,
start_column: 0,
end_line: 5,
end_column: 0,
signature: None,
doc: None,
qualified_name: None,
visibility: None,
is_async: false,
is_static: false,
is_unsafe: false,
body_hash: None,
})
.unwrap();
let target_id = arena
.alloc(NodeEntry {
kind: NodeKind::Function,
name: target_name,
file: file_id,
start_byte: 200,
end_byte: 300,
start_line: 10,
start_column: 0,
end_line: 15,
end_column: 0,
signature: None,
doc: None,
qualified_name: None,
visibility: None,
is_async: false,
is_static: false,
is_unsafe: false,
body_hash: None,
})
.unwrap();
indices.add(caller_id, NodeKind::Function, caller_name, None, file_id);
indices.add(target_id, NodeKind::Function, target_name, None, file_id);
edges.add_edge(
caller_id,
target_id,
EdgeKind::FfiCall {
convention: FfiConvention::C,
},
file_id,
);
let graph = CodeGraph::from_components(
arena,
edges,
strings,
files,
indices,
crate::graph::unified::NodeMetadataStore::new(),
);
(graph, caller_id, target_id)
}
#[test]
fn test_ffi_call_edge_in_get_references_to() {
let (graph, caller_id, target_id) = build_graph_with_ffi_edge();
let adapter = GraphQueryAdapter::new(&graph);
let refs = adapter.get_references_to(target_id);
assert_eq!(
refs.len(),
1,
"FfiCall edge should be included in references"
);
assert_eq!(refs[0].source_node_id, caller_id);
assert!(
matches!(
&refs[0].reference_kind,
EdgeKind::FfiCall {
convention: FfiConvention::C
}
),
"reference kind should be FfiCall with C convention"
);
}
#[test]
fn test_ffi_call_edge_in_node_has_references() {
let (graph, _caller_id, target_id) = build_graph_with_ffi_edge();
let adapter = GraphQueryAdapter::new(&graph);
assert!(
adapter.node_has_references(target_id),
"node_has_references should return true for FfiCall target"
);
}
}