use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Instant;
use anyhow::{Result, bail};
use serde_json::{Map, Value, json};
use sqry_core::graph::unified::concurrent::GraphSnapshot;
use sqry_core::graph::unified::edge::EdgeKind;
use sqry_core::graph::unified::edge::kind::TypeOfContext;
use sqry_core::graph::unified::materialize::find_nodes_by_name;
use sqry_core::graph::unified::node::NodeId;
use sqry_core::graph::unified::node::NodeKind;
use crate::engine::{canonicalize_in_workspace, engine_for_workspace};
use crate::tools::{CallHierarchyArgs, CallHierarchyDirection, RelationQueryArgs, RelationType};
use crate::execution::graph_builders::build_graph_metadata;
use crate::execution::location::node_location_for_reporting_snapshot;
use crate::execution::types::{
CallHierarchyData, CallHierarchyNode, NodeRefData, PositionData, RangeData, RelationEdgeData,
RelationQueryData, ToolExecution,
};
use crate::execution::utils::{duration_to_ms, paginate};
fn resolve_workspace_path(path: &str) -> Option<PathBuf> {
if path == "." {
None
} else {
Some(PathBuf::from(path))
}
}
pub fn execute_relation_query(
args: &RelationQueryArgs,
) -> Result<ToolExecution<RelationQueryData>> {
let workspace_path = resolve_workspace_path(&args.path);
let engine = engine_for_workspace(workspace_path.as_ref())?;
let workspace_root = engine.workspace_root().to_path_buf();
let _search_root = canonicalize_in_workspace(&args.path, &workspace_root)?;
let graph = engine.ensure_graph()?;
let ctx = crate::daemon_adapter::WorkspaceContext {
workspace_root,
graph,
executor: engine.executor_arc(),
};
inner::execute_relation_query(&ctx, args)
}
pub(crate) mod inner {
use super::{
RelationQueryArgs, RelationQueryData, Result, ToolExecution, build_graph_metadata,
collect_relation_edges_unified, duration_to_ms, paginate,
};
use crate::daemon_adapter::WorkspaceContext;
use std::sync::Arc;
use std::time::Instant;
pub(crate) fn execute_relation_query(
ctx: &WorkspaceContext,
args: &RelationQueryArgs,
) -> Result<ToolExecution<RelationQueryData>> {
let snapshot = Arc::new(ctx.graph.snapshot());
let start = Instant::now();
tracing::debug!(
symbol = %args.symbol,
relation = %args.relation.as_str(),
max_depth = args.max_depth,
max_results = args.max_results,
path = %args.path,
"Executing relation_query tool"
);
let edges =
collect_relation_edges_unified(&snapshot, &ctx.workspace_root, args, args.max_results)?;
let total = edges.len();
let (page_slice, next_page_token) = paginate(&edges, &args.pagination);
let relations = page_slice.to_vec();
let graph_metadata = build_graph_metadata(Some(&ctx.workspace_root), Some(&snapshot), None);
Ok(ToolExecution {
data: RelationQueryData {
relation_type: args.relation.as_str().to_string(),
relations,
total: total as u64,
},
used_index: false,
used_graph: true,
graph_metadata: Some(graph_metadata),
execution_ms: duration_to_ms(start.elapsed()),
next_page_token,
total: Some(total as u64),
truncated: Some(total > args.max_results),
candidates_scanned: None,
workspace_path: crate::execution::symbol_utils::path_to_forward_slash(
&ctx.workspace_root,
),
})
}
}
fn is_definition_kind(kind: NodeKind) -> bool {
matches!(
kind,
NodeKind::Function
| NodeKind::Method
| NodeKind::Class
| NodeKind::Interface
| NodeKind::Trait
| NodeKind::Module
| NodeKind::Struct
| NodeKind::Enum
| NodeKind::EnumVariant
| NodeKind::Macro
| NodeKind::Variable
| NodeKind::Constant
| NodeKind::Component
| NodeKind::Service
| NodeKind::Resource
| NodeKind::Endpoint
| NodeKind::Test
)
}
fn collect_relation_edges_unified(
snapshot: &Arc<GraphSnapshot>,
workspace_root: &Path,
args: &RelationQueryArgs,
max_results: usize,
) -> Result<Vec<RelationEdgeData>> {
let start_nodes = find_nodes_by_name(snapshot, &args.symbol);
if start_nodes.is_empty() {
bail!("Symbol '{}' not found in graph", args.symbol);
}
match args.relation {
RelationType::Callers | RelationType::Callees => Ok(collect_call_relation_via_db(
snapshot,
workspace_root,
&start_nodes,
&args.symbol,
args.relation,
args.max_depth,
max_results,
)),
RelationType::Imports | RelationType::Exports | RelationType::Returns => {
Ok(collect_structural_relation(
snapshot,
workspace_root,
&start_nodes,
args.relation,
max_results,
))
}
}
}
fn collect_call_relation_via_db(
snapshot: &Arc<GraphSnapshot>,
workspace_root: &Path,
start_nodes: &[NodeId],
_symbol: &str,
relation: RelationType,
max_depth: usize,
max_results: usize,
) -> Vec<RelationEdgeData> {
debug_assert!(matches!(
relation,
RelationType::Callers | RelationType::Callees
));
let mut results = Vec::new();
let start_set: HashSet<NodeId> = start_nodes.iter().copied().collect();
let mut depth_one_anchors: Vec<NodeId> = Vec::new();
let mut depth_one_anchor_set: HashSet<NodeId> = HashSet::new();
for &start_node in start_nodes {
if results.len() >= max_results {
return results;
}
let edges = match relation {
RelationType::Callers => snapshot.edges().edges_to(start_node),
RelationType::Callees => snapshot.edges().edges_from(start_node),
_ => unreachable!("guarded by debug_assert above"),
};
for edge in edges {
if results.len() >= max_results {
return results;
}
if !matches!(edge.kind, EdgeKind::Calls { .. }) {
continue;
}
let counterpart = match relation {
RelationType::Callers => edge.source,
RelationType::Callees => edge.target,
_ => unreachable!(),
};
let (from_id, to_id) = match relation {
RelationType::Callers => (counterpart, start_node),
RelationType::Callees => (start_node, counterpart),
_ => unreachable!(),
};
let from_ref = build_node_ref(snapshot, from_id, workspace_root);
let to_ref = build_node_ref(snapshot, to_id, workspace_root);
let metadata = match &edge.kind {
EdgeKind::Calls {
argument_count,
is_async,
} => Some(json!({
"argument_count": argument_count,
"is_async": is_async,
})),
_ => None,
};
results.push(RelationEdgeData {
from: Some(from_ref),
to: Some(to_ref),
relation_type: relation.as_str().to_string(),
depth: 1,
metadata,
});
if depth_one_anchor_set.insert(counterpart) {
depth_one_anchors.push(counterpart);
}
}
}
if max_depth <= 1 {
return results;
}
let mut visited: HashSet<NodeId> = start_set;
visited.extend(&depth_one_anchor_set);
let mut current_frontier: Vec<NodeId> = depth_one_anchors;
for depth in 1..max_depth {
if results.len() >= max_results {
return results;
}
let mut next_frontier: Vec<NodeId> = Vec::new();
for &node in ¤t_frontier {
let neighbours = match relation {
RelationType::Callers => snapshot.get_callers(node),
RelationType::Callees => snapshot.get_callees(node),
_ => unreachable!("relation enum guarded by debug_assert above"),
};
for next in neighbours {
if !visited.insert(next) {
continue;
}
let neighbour_set: HashSet<NodeId> = std::iter::once(node).collect();
let edges = collect_call_edges_between(
snapshot,
next,
&neighbour_set,
relation,
depth,
workspace_root,
);
for edge in edges {
if results.len() >= max_results {
return results;
}
results.push(edge);
}
next_frontier.push(next);
}
}
if next_frontier.is_empty() {
break;
}
current_frontier = next_frontier;
}
results
}
fn collect_call_edges_between(
snapshot: &GraphSnapshot,
frontier_node: NodeId,
start_set: &HashSet<NodeId>,
relation: RelationType,
depth: usize,
workspace_root: &Path,
) -> Vec<RelationEdgeData> {
let mut emitted = Vec::new();
let edges = match relation {
RelationType::Callers => snapshot.edges().edges_from(frontier_node),
RelationType::Callees => snapshot.edges().edges_to(frontier_node),
_ => unreachable!("only Callers/Callees route here"),
};
for edge in edges {
if !matches!(edge.kind, EdgeKind::Calls { .. }) {
continue;
}
let counterpart = match relation {
RelationType::Callers => edge.target,
RelationType::Callees => edge.source,
_ => unreachable!(),
};
if !start_set.contains(&counterpart) {
continue;
}
let (from_id, to_id) = match relation {
RelationType::Callers => (frontier_node, counterpart),
RelationType::Callees => (counterpart, frontier_node),
_ => unreachable!(),
};
let from_ref = build_node_ref(snapshot, from_id, workspace_root);
let to_ref = build_node_ref(snapshot, to_id, workspace_root);
let metadata = match &edge.kind {
EdgeKind::Calls {
argument_count,
is_async,
} => Some(json!({
"argument_count": argument_count,
"is_async": is_async,
})),
_ => None,
};
emitted.push(RelationEdgeData {
from: Some(from_ref),
to: Some(to_ref),
relation_type: relation.as_str().to_string(),
depth: u32::try_from(depth).unwrap_or(u32::MAX).saturating_add(1),
metadata,
});
}
emitted
}
fn collect_structural_relation(
snapshot: &Arc<GraphSnapshot>,
workspace_root: &Path,
start_nodes: &[NodeId],
relation: RelationType,
max_results: usize,
) -> Vec<RelationEdgeData> {
debug_assert!(matches!(
relation,
RelationType::Imports | RelationType::Exports | RelationType::Returns
));
let mut results = Vec::new();
for &start in start_nodes {
if results.len() >= max_results {
break;
}
let edges = match relation {
RelationType::Imports => collect_imports(snapshot, start, 0, workspace_root),
RelationType::Exports => collect_exports(snapshot, start, 0, workspace_root),
RelationType::Returns => collect_returns(snapshot, start, 0, workspace_root),
_ => unreachable!("guarded by debug_assert"),
};
for edge in edges {
if results.len() >= max_results {
break;
}
results.push(edge);
}
}
results
}
fn collect_imports(
snapshot: &GraphSnapshot,
node: NodeId,
depth: usize,
workspace_root: &Path,
) -> Vec<RelationEdgeData> {
let mut results = Vec::new();
let strings = snapshot.strings();
for edge in snapshot.edges().edges_from(node) {
let EdgeKind::Imports { alias, is_wildcard } = &edge.kind else {
continue;
};
let from_ref = build_node_ref(snapshot, node, workspace_root);
let to_ref = build_node_ref(snapshot, edge.target, workspace_root);
let mut map = Map::new();
map.insert("is_wildcard".to_string(), Value::Bool(*is_wildcard));
if let Some(alias_id) = alias
&& let Some(alias_str) = strings.resolve(*alias_id)
{
map.insert("alias".to_string(), Value::String(alias_str.to_string()));
}
results.push(RelationEdgeData {
from: Some(from_ref),
to: Some(to_ref),
relation_type: "imports".to_string(),
depth: depth.try_into().unwrap_or(u32::MAX).saturating_add(1),
metadata: Some(Value::Object(map)),
});
}
results
}
fn collect_exports(
snapshot: &GraphSnapshot,
node: NodeId,
depth: usize,
workspace_root: &Path,
) -> Vec<RelationEdgeData> {
let mut results = Vec::new();
let strings = snapshot.strings();
for edge in snapshot.edges().edges_from(node) {
let EdgeKind::Exports { kind, alias } = &edge.kind else {
continue;
};
let from_ref = build_node_ref(snapshot, node, workspace_root);
let to_ref = build_node_ref(snapshot, edge.target, workspace_root);
let mut map = Map::new();
map.insert("kind".to_string(), Value::String(format!("{kind:?}")));
if let Some(alias_id) = alias
&& let Some(alias_str) = strings.resolve(*alias_id)
{
map.insert("alias".to_string(), Value::String(alias_str.to_string()));
}
results.push(RelationEdgeData {
from: Some(from_ref),
to: Some(to_ref),
relation_type: "exports".to_string(),
depth: depth.try_into().unwrap_or(u32::MAX).saturating_add(1),
metadata: Some(Value::Object(map)),
});
}
results
}
fn collect_returns(
snapshot: &GraphSnapshot,
node: NodeId,
depth: usize,
workspace_root: &Path,
) -> Vec<RelationEdgeData> {
let mut results = Vec::new();
if snapshot.get_node(node).is_none() {
return results;
}
for edge in snapshot.edges().edges_from(node) {
if !matches!(
edge.kind,
EdgeKind::TypeOf {
context: Some(TypeOfContext::Return),
..
}
) {
continue;
}
if snapshot.get_node(edge.target).is_none() {
continue;
}
let from_ref = build_node_ref(snapshot, node, workspace_root);
let to_ref = build_node_ref(snapshot, edge.target, workspace_root);
results.push(RelationEdgeData {
from: Some(from_ref),
to: Some(to_ref),
relation_type: "returns".to_string(),
depth: depth.try_into().unwrap_or(u32::MAX).saturating_add(1),
metadata: None,
});
}
results
}
fn build_node_ref(snapshot: &GraphSnapshot, node_id: NodeId, workspace_root: &Path) -> NodeRefData {
use sqry_core::graph::unified::node::NodeKind;
let Some(entry) = snapshot.get_node(node_id) else {
return fallback_ref("unknown", workspace_root);
};
let strings = snapshot.strings();
let files = snapshot.files();
let name = strings
.resolve(entry.name)
.map_or_else(|| "unknown".to_string(), |s| s.to_string());
let qualified_name =
crate::execution::symbol_utils::display_entry_qualified_name(entry, strings, files, &name);
let kind = match entry.kind {
NodeKind::Class => "class",
NodeKind::Module => "module",
NodeKind::Variable => "variable",
NodeKind::Constant => "constant",
NodeKind::Interface => "interface",
NodeKind::Trait => "trait",
NodeKind::Method => "method",
NodeKind::Struct => "struct",
NodeKind::Enum => "enum",
NodeKind::Type => "type",
_ => "function",
};
let loc = node_location_for_reporting_snapshot(snapshot, node_id, workspace_root);
let language = loc
.as_ref()
.and_then(|l| l.language.clone())
.or_else(|| files.language_for_file(entry.file).map(|l| l.to_string()))
.unwrap_or_else(|| "unknown".to_string());
let file_path = loc
.as_ref()
.filter(|l| !l.file_path.is_empty())
.map(|l| workspace_root.join(&l.file_path))
.or_else(|| {
files
.resolve(entry.file)
.map(|arc_path| workspace_root.join(arc_path.as_ref()))
})
.unwrap_or_default();
let file_uri = url::Url::from_file_path(&file_path).ok().map_or_else(
|| crate::execution::symbol_utils::path_to_forward_slash(&file_path),
Into::into,
);
let resolution_source = loc.as_ref().map(|l| format!("{:?}", l.resolution_source));
NodeRefData {
name,
qualified_name,
kind: kind.to_string(),
language,
file_uri,
range: RangeData {
start: PositionData {
line: loc.as_ref().map_or(entry.start_line, |l| l.line),
character: loc.as_ref().map_or(entry.start_column, |l| l.column),
},
end: PositionData {
line: loc.as_ref().map_or(entry.end_line, |l| l.end_line),
character: loc.as_ref().map_or(entry.end_column, |l| l.end_column),
},
},
metadata: None,
resolution_source,
}
}
fn fallback_ref(name: &str, workspace_root: &Path) -> NodeRefData {
NodeRefData {
name: name.to_string(),
qualified_name: name.to_string(),
kind: "unknown".to_string(),
language: "unknown".to_string(),
file_uri: crate::execution::symbol_utils::path_to_forward_slash(workspace_root),
range: RangeData {
start: PositionData {
line: 0,
character: 0,
},
end: PositionData {
line: 0,
character: 0,
},
},
metadata: None,
resolution_source: None,
}
}
pub fn execute_call_hierarchy(
args: &CallHierarchyArgs,
) -> Result<ToolExecution<CallHierarchyData>> {
let workspace_path = resolve_workspace_path(&args.path);
let engine = engine_for_workspace(workspace_path.as_ref())?;
let workspace_root = engine.workspace_root().to_path_buf();
let _search_root = canonicalize_in_workspace(&args.path, &workspace_root)?;
let graph = engine.ensure_graph()?;
let snapshot = graph.snapshot();
let start = Instant::now();
tracing::debug!(
symbol = %args.symbol,
direction = %args.direction.as_str(),
max_depth = args.max_depth,
max_results = args.max_results,
file_path = ?args.file_path,
"Executing call_hierarchy tool"
);
let root_nodes = find_nodes_by_name(&snapshot, &args.symbol);
let file_filtered: Vec<NodeId> = if let Some(ref file_path) = args.file_path {
let files = snapshot.files();
root_nodes
.into_iter()
.filter(|&node_id| {
if let Some(entry) = snapshot.get_node(node_id) {
files
.resolve(entry.file)
.is_some_and(|p| p.as_ref().ends_with(file_path))
} else {
false
}
})
.collect()
} else {
root_nodes
};
if file_filtered.is_empty() {
bail!(
"Symbol '{}' not found{}",
args.symbol,
args.file_path
.as_ref()
.map_or(String::new(), |f| format!(" in file '{f}'"))
);
}
let definition_nodes: Vec<NodeId> = file_filtered
.iter()
.copied()
.filter(|&node_id| {
snapshot
.get_node(node_id)
.is_some_and(|entry| is_definition_kind(entry.kind))
})
.collect();
let candidates = if definition_nodes.is_empty() {
&file_filtered
} else {
&definition_nodes
};
let root_node_id = candidates[0];
let root_ref = build_node_ref(&snapshot, root_node_id, &workspace_root);
let direction = args.direction;
let items = collect_call_hierarchy_items(
&snapshot,
root_node_id,
direction,
args.max_depth,
args.max_results,
&workspace_root,
);
let total = items.len();
let (page_slice, next_page_token) = paginate(&items, &args.pagination);
let graph_metadata = build_graph_metadata(Some(&workspace_root), Some(&snapshot), None);
Ok(ToolExecution {
data: CallHierarchyData {
root: root_ref,
direction: direction.as_str().to_string(),
items: page_slice.to_vec(),
total: total as u64,
},
used_index: false,
used_graph: true,
graph_metadata: Some(graph_metadata),
execution_ms: duration_to_ms(start.elapsed()),
next_page_token,
total: Some(total as u64),
truncated: Some(total > args.max_results),
candidates_scanned: None,
workspace_path: crate::execution::symbol_utils::path_to_forward_slash(workspace_root),
})
}
fn collect_call_hierarchy_items(
snapshot: &GraphSnapshot,
root_node: NodeId,
direction: CallHierarchyDirection,
max_depth: usize,
max_results: usize,
workspace_root: &Path,
) -> Vec<CallHierarchyNode> {
let mut items = Vec::new();
let mut visited: HashSet<NodeId> = HashSet::new();
visited.insert(root_node);
let edges = match direction {
CallHierarchyDirection::Incoming => snapshot.edges().edges_to(root_node),
CallHierarchyDirection::Outgoing => snapshot.edges().edges_from(root_node),
};
for edge in edges {
if items.len() >= max_results {
break;
}
if !matches!(edge.kind, EdgeKind::Calls { .. }) {
continue;
}
let related_node = match direction {
CallHierarchyDirection::Incoming => edge.source,
CallHierarchyDirection::Outgoing => edge.target,
};
if visited.contains(&related_node) {
continue;
}
visited.insert(related_node);
let node_ref = build_node_ref(snapshot, related_node, workspace_root);
let call_ranges: Vec<RangeData> = edge
.spans
.iter()
.map(|span| RangeData {
start: PositionData {
line: u32::try_from(span.start.line).unwrap_or(u32::MAX),
character: u32::try_from(span.start.column).unwrap_or(u32::MAX),
},
end: PositionData {
line: u32::try_from(span.end.line).unwrap_or(u32::MAX),
character: u32::try_from(span.end.column).unwrap_or(u32::MAX),
},
})
.collect();
let children = if max_depth > 1 {
collect_call_hierarchy_items(
snapshot,
related_node,
direction,
max_depth - 1,
max_results.saturating_sub(items.len()),
workspace_root,
)
} else {
Vec::new()
};
items.push(CallHierarchyNode {
symbol: node_ref,
children,
call_ranges,
});
}
items
}
#[cfg(test)]
mod tests {
use super::*;
use sqry_core::graph::unified::concurrent::CodeGraph;
use sqry_core::graph::unified::edge::EdgeKind;
use sqry_core::graph::unified::node::NodeKind;
use sqry_core::graph::unified::storage::arena::NodeEntry;
use std::path::{Path, PathBuf};
fn workspace_root() -> PathBuf {
PathBuf::from("/tmp/test_workspace")
}
fn make_graph_with_call_edge() -> (CodeGraph, NodeId, NodeId) {
let mut graph = CodeGraph::new();
let file_id = graph.files_mut().register(Path::new("src/lib.rs")).unwrap();
let name_a = graph.strings_mut().intern("caller_fn").unwrap();
let name_b = graph.strings_mut().intern("callee_fn").unwrap();
let entry_a = NodeEntry::new(NodeKind::Function, name_a, file_id);
let entry_b = NodeEntry::new(NodeKind::Function, name_b, file_id);
let node_a = graph.nodes_mut().alloc(entry_a).unwrap();
let node_b = graph.nodes_mut().alloc(entry_b).unwrap();
graph
.indices_mut()
.add(node_a, NodeKind::Function, name_a, None, file_id);
graph
.indices_mut()
.add(node_b, NodeKind::Function, name_b, None, file_id);
let call_kind = EdgeKind::Calls {
argument_count: 2,
is_async: false,
};
graph
.edges_mut()
.add_edge(node_a, node_b, call_kind, file_id);
(graph, node_a, node_b)
}
#[test]
fn resolve_workspace_path_dot_returns_none() {
assert!(resolve_workspace_path(".").is_none());
}
#[test]
fn resolve_workspace_path_explicit_returns_some() {
let result = resolve_workspace_path("/workspace/project");
assert!(result.is_some());
assert_eq!(result.unwrap(), PathBuf::from("/workspace/project"));
}
#[test]
fn resolve_workspace_path_empty_string_returns_some() {
let result = resolve_workspace_path("");
assert!(result.is_some());
}
#[test]
fn is_definition_kind_returns_true_for_definition_kinds() {
let definition_kinds = [
NodeKind::Function,
NodeKind::Method,
NodeKind::Class,
NodeKind::Interface,
NodeKind::Trait,
NodeKind::Module,
NodeKind::Struct,
NodeKind::Enum,
NodeKind::EnumVariant,
NodeKind::Macro,
NodeKind::Variable,
NodeKind::Constant,
NodeKind::Component,
NodeKind::Service,
NodeKind::Resource,
NodeKind::Endpoint,
NodeKind::Test,
];
for kind in definition_kinds {
assert!(
is_definition_kind(kind),
"Expected {kind:?} to be a definition kind"
);
}
}
#[test]
fn is_definition_kind_returns_false_for_reference_kinds() {
let reference_kinds = [
NodeKind::CallSite,
NodeKind::Import,
NodeKind::Parameter,
NodeKind::Property,
];
for kind in reference_kinds {
assert!(
!is_definition_kind(kind),
"Expected {kind:?} to NOT be a definition kind"
);
}
}
#[test]
fn fallback_ref_creates_unknown_node() {
let ws = PathBuf::from("/workspace");
let r = fallback_ref("mystery", &ws);
assert_eq!(r.name, "mystery");
assert_eq!(r.qualified_name, "mystery");
assert_eq!(r.kind, "unknown");
assert_eq!(r.language, "unknown");
assert_eq!(r.range.start.line, 0);
assert_eq!(r.range.end.line, 0);
assert!(r.metadata.is_none());
}
#[test]
fn build_node_ref_for_existing_node() {
let (graph, node_a, _node_b) = make_graph_with_call_edge();
let snapshot = graph.snapshot();
let ws = workspace_root();
let node_ref = build_node_ref(&snapshot, node_a, &ws);
assert_eq!(node_ref.name, "caller_fn");
assert_eq!(node_ref.kind, "function");
}
#[test]
fn build_node_ref_for_invalid_node_returns_fallback() {
let graph = CodeGraph::new();
let snapshot = graph.snapshot();
let ws = workspace_root();
let fake_node = NodeId::new(9999, 0);
let node_ref = build_node_ref(&snapshot, fake_node, &ws);
assert_eq!(node_ref.name, "unknown");
assert_eq!(node_ref.kind, "unknown");
}
#[test]
fn build_node_ref_maps_class_kind() {
let mut graph = CodeGraph::new();
let file_id = graph.files_mut().register(Path::new("src/a.rs")).unwrap();
let nm = graph.strings_mut().intern("MyClass").unwrap();
let entry = NodeEntry::new(NodeKind::Class, nm, file_id);
let nid = graph.nodes_mut().alloc(entry).unwrap();
let snapshot = graph.snapshot();
let r = build_node_ref(&snapshot, nid, &workspace_root());
assert_eq!(r.kind, "class");
}
#[test]
fn build_node_ref_maps_struct_kind() {
let mut graph = CodeGraph::new();
let file_id = graph.files_mut().register(Path::new("src/a.rs")).unwrap();
let nm = graph.strings_mut().intern("MyStruct").unwrap();
let entry = NodeEntry::new(NodeKind::Struct, nm, file_id);
let nid = graph.nodes_mut().alloc(entry).unwrap();
let snapshot = graph.snapshot();
let r = build_node_ref(&snapshot, nid, &workspace_root());
assert_eq!(r.kind, "struct");
}
#[test]
fn build_node_ref_maps_module_kind() {
let mut graph = CodeGraph::new();
let file_id = graph.files_mut().register(Path::new("src/a.rs")).unwrap();
let nm = graph.strings_mut().intern("mymod").unwrap();
let entry = NodeEntry::new(NodeKind::Module, nm, file_id);
let nid = graph.nodes_mut().alloc(entry).unwrap();
let snapshot = graph.snapshot();
let r = build_node_ref(&snapshot, nid, &workspace_root());
assert_eq!(r.kind, "module");
}
#[test]
fn build_node_ref_maps_method_kind() {
let mut graph = CodeGraph::new();
let file_id = graph.files_mut().register(Path::new("src/a.rs")).unwrap();
let nm = graph.strings_mut().intern("do_thing").unwrap();
let entry = NodeEntry::new(NodeKind::Method, nm, file_id);
let nid = graph.nodes_mut().alloc(entry).unwrap();
let snapshot = graph.snapshot();
let r = build_node_ref(&snapshot, nid, &workspace_root());
assert_eq!(r.kind, "method");
}
#[test]
fn build_node_ref_maps_enum_kind() {
let mut graph = CodeGraph::new();
let file_id = graph.files_mut().register(Path::new("src/a.rs")).unwrap();
let nm = graph.strings_mut().intern("Color").unwrap();
let entry = NodeEntry::new(NodeKind::Enum, nm, file_id);
let nid = graph.nodes_mut().alloc(entry).unwrap();
let snapshot = graph.snapshot();
let r = build_node_ref(&snapshot, nid, &workspace_root());
assert_eq!(r.kind, "enum");
}
#[test]
fn build_node_ref_maps_variable_kind() {
let mut graph = CodeGraph::new();
let file_id = graph.files_mut().register(Path::new("src/a.rs")).unwrap();
let nm = graph.strings_mut().intern("MY_VAR").unwrap();
let entry = NodeEntry::new(NodeKind::Variable, nm, file_id);
let nid = graph.nodes_mut().alloc(entry).unwrap();
let snapshot = graph.snapshot();
let r = build_node_ref(&snapshot, nid, &workspace_root());
assert_eq!(r.kind, "variable");
}
#[test]
fn build_node_ref_maps_constant_kind() {
let mut graph = CodeGraph::new();
let file_id = graph.files_mut().register(Path::new("src/a.rs")).unwrap();
let nm = graph.strings_mut().intern("MAX").unwrap();
let entry = NodeEntry::new(NodeKind::Constant, nm, file_id);
let nid = graph.nodes_mut().alloc(entry).unwrap();
let snapshot = graph.snapshot();
let r = build_node_ref(&snapshot, nid, &workspace_root());
assert_eq!(r.kind, "constant");
}
#[test]
fn build_node_ref_maps_interface_kind() {
let mut graph = CodeGraph::new();
let file_id = graph.files_mut().register(Path::new("src/a.rs")).unwrap();
let nm = graph.strings_mut().intern("Readable").unwrap();
let entry = NodeEntry::new(NodeKind::Interface, nm, file_id);
let nid = graph.nodes_mut().alloc(entry).unwrap();
let snapshot = graph.snapshot();
let r = build_node_ref(&snapshot, nid, &workspace_root());
assert_eq!(r.kind, "interface");
}
#[test]
fn build_node_ref_maps_trait_kind() {
let mut graph = CodeGraph::new();
let file_id = graph.files_mut().register(Path::new("src/a.rs")).unwrap();
let nm = graph.strings_mut().intern("Display").unwrap();
let entry = NodeEntry::new(NodeKind::Trait, nm, file_id);
let nid = graph.nodes_mut().alloc(entry).unwrap();
let snapshot = graph.snapshot();
let r = build_node_ref(&snapshot, nid, &workspace_root());
assert_eq!(r.kind, "trait");
}
#[test]
fn build_node_ref_maps_type_kind() {
let mut graph = CodeGraph::new();
let file_id = graph.files_mut().register(Path::new("src/a.rs")).unwrap();
let nm = graph.strings_mut().intern("Alias").unwrap();
let entry = NodeEntry::new(NodeKind::Type, nm, file_id);
let nid = graph.nodes_mut().alloc(entry).unwrap();
let snapshot = graph.snapshot();
let r = build_node_ref(&snapshot, nid, &workspace_root());
assert_eq!(r.kind, "type");
}
#[test]
fn build_node_ref_fallback_arm_maps_unrecognized_kinds_to_function() {
let fallback_kinds = [NodeKind::CallSite, NodeKind::Other];
for kind in fallback_kinds {
let mut graph = CodeGraph::new();
let file_id = graph.files_mut().register(Path::new("src/a.rs")).unwrap();
let nm = graph.strings_mut().intern("some_sym").unwrap();
let entry = NodeEntry::new(kind, nm, file_id);
let nid = graph.nodes_mut().alloc(entry).unwrap();
let snapshot = graph.snapshot();
let r = build_node_ref(&snapshot, nid, &workspace_root());
assert_eq!(
r.kind, "function",
"Expected fallback kind 'function' for NodeKind::{kind:?}"
);
}
}
#[test]
fn collect_call_edges_between_emits_callee_edge_under_callees_relation() {
let (graph, node_a, node_b) = make_graph_with_call_edge();
let snapshot = graph.snapshot();
let ws = workspace_root();
let start_set: HashSet<NodeId> = std::iter::once(node_a).collect();
let edges = collect_call_edges_between(
&snapshot,
node_b,
&start_set,
RelationType::Callees,
0,
&ws,
);
assert_eq!(edges.len(), 1);
assert_eq!(edges[0].relation_type, "callees");
assert_eq!(edges[0].depth, 1);
assert_eq!(edges[0].from.as_ref().unwrap().name, "caller_fn");
assert_eq!(edges[0].to.as_ref().unwrap().name, "callee_fn");
let meta = edges[0].metadata.as_ref().unwrap();
assert!(meta.get("argument_count").is_some());
}
#[test]
fn collect_call_edges_between_emits_caller_edge_under_callers_relation() {
let (graph, node_a, node_b) = make_graph_with_call_edge();
let snapshot = graph.snapshot();
let ws = workspace_root();
let start_set: HashSet<NodeId> = std::iter::once(node_b).collect();
let edges = collect_call_edges_between(
&snapshot,
node_a,
&start_set,
RelationType::Callers,
0,
&ws,
);
assert_eq!(edges.len(), 1);
assert_eq!(edges[0].relation_type, "callers");
assert_eq!(edges[0].from.as_ref().unwrap().name, "caller_fn");
assert_eq!(edges[0].to.as_ref().unwrap().name, "callee_fn");
let meta = edges[0].metadata.as_ref().unwrap();
assert!(meta.get("is_async").is_some());
}
#[test]
fn collect_call_edges_between_empty_when_frontier_has_no_call_edges() {
let (graph, _node_a, node_b) = make_graph_with_call_edge();
let snapshot = graph.snapshot();
let ws = workspace_root();
let start_set: HashSet<NodeId> = HashSet::new();
let edges = collect_call_edges_between(
&snapshot,
node_b,
&start_set,
RelationType::Callers,
0,
&ws,
);
assert!(edges.is_empty());
}
#[test]
fn collect_call_edges_between_skips_edges_outside_start_set() {
let (graph, node_a, _node_b) = make_graph_with_call_edge();
let snapshot = graph.snapshot();
let ws = workspace_root();
let start_set: HashSet<NodeId> = HashSet::new();
let edges = collect_call_edges_between(
&snapshot,
node_a,
&start_set,
RelationType::Callers,
0,
&ws,
);
assert!(edges.is_empty());
}
#[test]
fn collect_call_relation_via_db_does_not_leak_unrelated_same_named_chains() {
use std::sync::Arc;
let mut graph = CodeGraph::new();
let file_id = graph.files_mut().register(Path::new("lib.rs")).unwrap();
let mk_node = |g: &mut CodeGraph, qname: &str, simple: &str| -> NodeId {
let qn = g.strings_mut().intern(qname).unwrap();
let nm = g.strings_mut().intern(simple).unwrap();
g.nodes_mut()
.alloc(NodeEntry::new(NodeKind::Function, nm, file_id).with_qualified_name(qn))
.unwrap()
};
let alpha_helper = mk_node(&mut graph, "alpha::helper", "helper");
let beta_helper = mk_node(&mut graph, "beta::helper", "helper");
let alpha_caller = mk_node(&mut graph, "alpha::caller_a", "caller_a");
let beta_caller = mk_node(&mut graph, "beta::caller_b", "caller_b");
let alpha_root = mk_node(&mut graph, "alpha::root_a", "root_a");
let beta_root = mk_node(&mut graph, "beta::root_b", "root_b");
let calls = EdgeKind::Calls {
argument_count: 0,
is_async: false,
};
graph
.edges_mut()
.add_edge(alpha_caller, alpha_helper, calls.clone(), file_id);
graph
.edges_mut()
.add_edge(beta_caller, beta_helper, calls.clone(), file_id);
graph
.edges_mut()
.add_edge(alpha_root, alpha_caller, calls.clone(), file_id);
graph
.edges_mut()
.add_edge(beta_root, beta_caller, calls, file_id);
let snapshot = Arc::new(graph.snapshot());
let start_nodes = vec![alpha_helper];
let edges = collect_call_relation_via_db(
&snapshot,
&workspace_root(),
&start_nodes,
"alpha::helper",
RelationType::Callers,
2,
100,
);
for edge in &edges {
let from_qn = edge
.from
.as_ref()
.map(|f| f.qualified_name.as_str())
.unwrap_or("");
let to_qn = edge
.to
.as_ref()
.map(|f| f.qualified_name.as_str())
.unwrap_or("");
assert!(
!from_qn.contains("beta") && !to_qn.contains("beta"),
"depth-2 BFS leaked a beta chain: from={from_qn:?} \
to={to_qn:?} (full edges: {edges:#?})"
);
}
let depth1 = edges
.iter()
.any(|e| e.depth == 1 && e.from.as_ref().is_some_and(|f| f.name == "caller_a"));
assert!(
depth1,
"expected alpha::caller_a -> alpha::helper at depth 1, got {edges:#?}"
);
let depth2 = edges
.iter()
.any(|e| e.depth >= 2 && e.from.as_ref().is_some_and(|f| f.name == "root_a"));
assert!(
depth2,
"expected alpha::root_a -> alpha::caller_a at depth >= 2, got {edges:#?}"
);
assert!(graph_has_edge(&snapshot, beta_root, beta_caller));
assert!(graph_has_edge(&snapshot, beta_caller, beta_helper));
}
fn graph_has_edge(
snapshot: &sqry_core::graph::unified::concurrent::GraphSnapshot,
src: NodeId,
tgt: NodeId,
) -> bool {
snapshot
.edges()
.edges_from(src)
.iter()
.any(|edge| edge.target == tgt)
}
#[test]
fn collect_imports_returns_import_edges() {
let mut graph = CodeGraph::new();
let file_id = graph
.files_mut()
.register(Path::new("src/main.rs"))
.unwrap();
let nm_a = graph.strings_mut().intern("module_a").unwrap();
let nm_b = graph.strings_mut().intern("module_b").unwrap();
let entry_a = NodeEntry::new(NodeKind::Module, nm_a, file_id);
let entry_b = NodeEntry::new(NodeKind::Module, nm_b, file_id);
let node_a = graph.nodes_mut().alloc(entry_a).unwrap();
let node_b = graph.nodes_mut().alloc(entry_b).unwrap();
graph
.indices_mut()
.add(node_a, NodeKind::Module, nm_a, None, file_id);
graph
.indices_mut()
.add(node_b, NodeKind::Module, nm_b, None, file_id);
let import_kind = EdgeKind::Imports {
alias: None,
is_wildcard: false,
};
graph
.edges_mut()
.add_edge(node_a, node_b, import_kind, file_id);
let snapshot = graph.snapshot();
let ws = workspace_root();
let imports = collect_imports(&snapshot, node_a, 0, &ws);
assert_eq!(imports.len(), 1);
assert_eq!(imports[0].relation_type, "imports");
let meta = imports[0].metadata.as_ref().unwrap();
assert_eq!(meta["is_wildcard"], serde_json::Value::Bool(false));
}
#[test]
fn collect_imports_with_wildcard() {
let mut graph = CodeGraph::new();
let file_id = graph.files_mut().register(Path::new("src/a.rs")).unwrap();
let nm_a = graph.strings_mut().intern("a").unwrap();
let nm_b = graph.strings_mut().intern("b").unwrap();
let node_a = graph
.nodes_mut()
.alloc(NodeEntry::new(NodeKind::Module, nm_a, file_id))
.unwrap();
let node_b = graph
.nodes_mut()
.alloc(NodeEntry::new(NodeKind::Module, nm_b, file_id))
.unwrap();
graph
.indices_mut()
.add(node_a, NodeKind::Module, nm_a, None, file_id);
graph
.indices_mut()
.add(node_b, NodeKind::Module, nm_b, None, file_id);
let import_kind = EdgeKind::Imports {
alias: None,
is_wildcard: true,
};
graph
.edges_mut()
.add_edge(node_a, node_b, import_kind, file_id);
let snapshot = graph.snapshot();
let imports = collect_imports(&snapshot, node_a, 0, &workspace_root());
assert_eq!(imports.len(), 1);
let meta = imports[0].metadata.as_ref().unwrap();
assert_eq!(meta["is_wildcard"], serde_json::Value::Bool(true));
}
fn make_graph_with_return_type_edge() -> (CodeGraph, NodeId, NodeId, NodeId) {
use sqry_core::graph::unified::edge::kind::TypeOfContext;
let mut graph = CodeGraph::new();
let file_id = graph.files_mut().register(Path::new("src/lib.rs")).unwrap();
let caller_name = graph.strings_mut().intern("caller_fn").unwrap();
let other_name = graph.strings_mut().intern("other_fn").unwrap();
let ret_name = graph.strings_mut().intern("ret_type").unwrap();
let caller = graph
.nodes_mut()
.alloc(NodeEntry::new(NodeKind::Function, caller_name, file_id))
.unwrap();
let other = graph
.nodes_mut()
.alloc(NodeEntry::new(NodeKind::Function, other_name, file_id))
.unwrap();
let ret_type_node = graph
.nodes_mut()
.alloc(NodeEntry::new(NodeKind::Type, ret_name, file_id))
.unwrap();
graph
.indices_mut()
.add(caller, NodeKind::Function, caller_name, None, file_id);
graph
.indices_mut()
.add(other, NodeKind::Function, other_name, None, file_id);
graph
.indices_mut()
.add(ret_type_node, NodeKind::Type, ret_name, None, file_id);
graph.edges_mut().add_edge(
caller,
ret_type_node,
EdgeKind::TypeOf {
context: Some(TypeOfContext::Return),
index: None,
name: None,
},
file_id,
);
(graph, caller, other, ret_type_node)
}
#[test]
fn collect_returns_emits_edge_when_typeof_return_present() {
let (graph, caller, _other, ret_type_node) = make_graph_with_return_type_edge();
let snapshot = graph.snapshot();
let ws = workspace_root();
let returns = collect_returns(&snapshot, caller, 0, &ws);
assert_eq!(
returns.len(),
1,
"expected exactly one TypeOf{{Return}} edge entry"
);
let edge = &returns[0];
assert_eq!(edge.relation_type, "returns");
let from = edge
.from
.as_ref()
.expect("from must be populated by collect_returns");
let to = edge
.to
.as_ref()
.expect("to must be populated post-fix (no more placeholder stub)");
assert_eq!(from.name, "caller_fn");
assert_eq!(to.name, "ret_type");
assert!(
edge.metadata.is_none(),
"metadata must be None after migrating to real edge walk; got {:?}",
edge.metadata
);
let resolved_target_name = snapshot
.get_node(ret_type_node)
.and_then(|e| snapshot.strings().resolve(e.name).map(|s| s.to_string()));
assert_eq!(resolved_target_name.as_deref(), Some("ret_type"));
}
#[test]
fn collect_returns_empty_for_node_without_return_edge() {
let (graph, _caller, other, _ret_type_node) = make_graph_with_return_type_edge();
let snapshot = graph.snapshot();
let ws = workspace_root();
let returns = collect_returns(&snapshot, other, 0, &ws);
assert!(
returns.is_empty(),
"node with no TypeOf{{Return}} edges must yield an empty Vec, not a stub; got {returns:?}"
);
}
#[test]
fn collect_returns_empty_when_node_not_found() {
let graph = CodeGraph::new();
let snapshot = graph.snapshot();
let ws = workspace_root();
let fake_node = NodeId::new(9999, 0);
let returns = collect_returns(&snapshot, fake_node, 0, &ws);
assert!(returns.is_empty());
}
#[test]
fn find_nodes_by_name_finds_registered_symbol() {
let (graph, node_a, _node_b) = make_graph_with_call_edge();
let snapshot = graph.snapshot();
let nodes = find_nodes_by_name(&snapshot, "caller_fn");
assert!(!nodes.is_empty());
assert!(nodes.contains(&node_a));
}
#[test]
fn find_nodes_by_name_returns_empty_for_nonexistent() {
let graph = CodeGraph::new();
let snapshot = graph.snapshot();
let nodes = find_nodes_by_name(&snapshot, "does_not_exist_xyz");
assert!(nodes.is_empty());
}
#[test]
fn collect_exports_returns_export_edges() {
use sqry_core::graph::unified::ExportKind;
let mut graph = CodeGraph::new();
let file_id = graph.files_mut().register(Path::new("src/lib.rs")).unwrap();
let nm_a = graph.strings_mut().intern("exported_fn").unwrap();
let nm_b = graph.strings_mut().intern("target_fn").unwrap();
let node_a = graph
.nodes_mut()
.alloc(NodeEntry::new(NodeKind::Function, nm_a, file_id))
.unwrap();
let node_b = graph
.nodes_mut()
.alloc(NodeEntry::new(NodeKind::Function, nm_b, file_id))
.unwrap();
graph
.indices_mut()
.add(node_a, NodeKind::Function, nm_a, None, file_id);
graph
.indices_mut()
.add(node_b, NodeKind::Function, nm_b, None, file_id);
let export_kind = EdgeKind::Exports {
kind: ExportKind::Direct,
alias: None,
};
graph
.edges_mut()
.add_edge(node_a, node_b, export_kind, file_id);
let snapshot = graph.snapshot();
let ws = workspace_root();
let exports = collect_exports(&snapshot, node_a, 0, &ws);
assert_eq!(exports.len(), 1);
assert_eq!(exports[0].relation_type, "exports");
let meta = exports[0].metadata.as_ref().unwrap();
assert!(meta.get("kind").is_some());
}
#[test]
fn collect_call_hierarchy_items_outgoing_finds_callees() {
let (graph, node_a, _node_b) = make_graph_with_call_edge();
let snapshot = graph.snapshot();
let ws = workspace_root();
let items = collect_call_hierarchy_items(
&snapshot,
node_a,
CallHierarchyDirection::Outgoing,
2,
10,
&ws,
);
assert_eq!(items.len(), 1);
assert_eq!(items[0].symbol.name, "callee_fn");
}
#[test]
fn collect_call_hierarchy_items_incoming_finds_callers() {
let (graph, _node_a, node_b) = make_graph_with_call_edge();
let snapshot = graph.snapshot();
let ws = workspace_root();
let items = collect_call_hierarchy_items(
&snapshot,
node_b,
CallHierarchyDirection::Incoming,
2,
10,
&ws,
);
assert_eq!(items.len(), 1);
assert_eq!(items[0].symbol.name, "caller_fn");
}
#[test]
fn collect_call_hierarchy_items_respects_max_results() {
let mut graph = CodeGraph::new();
let file_id = graph.files_mut().register(Path::new("src/lib.rs")).unwrap();
let nm_root = graph.strings_mut().intern("root").unwrap();
let node_root = graph
.nodes_mut()
.alloc(NodeEntry::new(NodeKind::Function, nm_root, file_id))
.unwrap();
graph
.indices_mut()
.add(node_root, NodeKind::Function, nm_root, None, file_id);
let call_kind = EdgeKind::Calls {
argument_count: 0,
is_async: false,
};
for i in 0..5u32 {
let nm = graph.strings_mut().intern(&format!("callee_{i}")).unwrap();
let node = graph
.nodes_mut()
.alloc(NodeEntry::new(NodeKind::Function, nm, file_id))
.unwrap();
graph
.indices_mut()
.add(node, NodeKind::Function, nm, None, file_id);
graph
.edges_mut()
.add_edge(node_root, node, call_kind.clone(), file_id);
}
let snapshot = graph.snapshot();
let ws = workspace_root();
let items = collect_call_hierarchy_items(
&snapshot,
node_root,
CallHierarchyDirection::Outgoing,
1,
3,
&ws,
);
assert!(items.len() <= 3);
}
#[test]
fn collect_call_hierarchy_items_empty_when_no_edges() {
let (graph, _node_a, node_b) = make_graph_with_call_edge();
let snapshot = graph.snapshot();
let ws = workspace_root();
let items = collect_call_hierarchy_items(
&snapshot,
node_b,
CallHierarchyDirection::Outgoing,
2,
10,
&ws,
);
assert!(items.is_empty());
}
#[test]
fn collect_call_hierarchy_items_depth_one_no_recursion() {
let (graph, node_a, _node_b) = make_graph_with_call_edge();
let snapshot = graph.snapshot();
let ws = workspace_root();
let items = collect_call_hierarchy_items(
&snapshot,
node_a,
CallHierarchyDirection::Outgoing,
1,
10,
&ws,
);
assert_eq!(items.len(), 1);
assert!(items[0].children.is_empty()); }
}