use sqry_core::graph::unified::concurrent::{CodeGraph, GraphSnapshot};
use sqry_core::graph::unified::edge::{BidirectionalEdgeStore, EdgeKind};
use sqry_core::graph::unified::node::NodeId;
use sqry_core::graph::unified::storage::{AuxiliaryIndices, FileRegistry, NodeArena};
use crate::execution::symbol_utils::relative_path_forward_slash;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum LocationResolutionSource {
OwnSpan,
CanonicalSibling,
IncomingEdgeSpan,
ExternSymbol,
Fallback,
}
#[derive(Debug, Clone)]
pub(crate) struct NodeLocation {
#[allow(dead_code)] pub file_path: String,
pub line: u32,
pub column: u32,
pub end_line: u32,
pub end_column: u32,
#[allow(dead_code)] pub language: Option<String>,
#[allow(dead_code)] pub resolution_source: LocationResolutionSource,
}
pub(crate) fn node_location_for_reporting(
graph: &CodeGraph,
node_id: NodeId,
workspace_root: &std::path::Path,
) -> Option<NodeLocation> {
resolve_location_inner(
graph.nodes(),
graph.files(),
graph.indices(),
graph.edges(),
node_id,
workspace_root,
)
}
pub(crate) fn node_location_for_reporting_snapshot(
snapshot: &GraphSnapshot,
node_id: NodeId,
workspace_root: &std::path::Path,
) -> Option<NodeLocation> {
resolve_location_inner(
snapshot.nodes(),
snapshot.files(),
snapshot.indices(),
snapshot.edges(),
node_id,
workspace_root,
)
}
fn resolve_location_inner(
nodes: &NodeArena,
files: &FileRegistry,
indices: &AuxiliaryIndices,
edges: &BidirectionalEdgeStore,
node_id: NodeId,
workspace_root: &std::path::Path,
) -> Option<NodeLocation> {
let entry = nodes.get(node_id)?;
let file_path = files
.resolve(entry.file)
.map(|p| relative_path_forward_slash(p.as_ref(), workspace_root))
.unwrap_or_default();
let language = files.language_for_file(entry.file).map(|l| l.to_string());
if entry.start_line > 0 {
return Some(NodeLocation {
file_path,
line: entry.start_line,
column: entry.start_column,
end_line: entry.end_line,
end_column: entry.end_column,
language,
resolution_source: LocationResolutionSource::OwnSpan,
});
}
if let Some(qn_id) = entry.qualified_name {
let siblings = indices.by_qualified_name(qn_id);
for &sibling_id in siblings {
if sibling_id == node_id {
continue;
}
if let Some(sibling) = nodes.get(sibling_id)
&& sibling.start_line > 0
{
let sibling_file_path = files
.resolve(sibling.file)
.map(|p| relative_path_forward_slash(p.as_ref(), workspace_root))
.unwrap_or_default();
let sibling_language = files.language_for_file(sibling.file).map(|l| l.to_string());
let source = if files.is_external(sibling.file) {
LocationResolutionSource::ExternSymbol
} else {
LocationResolutionSource::CanonicalSibling
};
return Some(NodeLocation {
file_path: sibling_file_path,
line: sibling.start_line,
column: sibling.start_column,
end_line: sibling.end_line,
end_column: sibling.end_column,
language: sibling_language,
resolution_source: source,
});
}
}
}
if entry.qualified_name.is_none() {
let name_siblings = indices.by_name(entry.name);
for &sibling_id in name_siblings {
if sibling_id == node_id {
continue;
}
if let Some(sibling) = nodes.get(sibling_id)
&& sibling.start_line > 0
&& sibling.kind == entry.kind
{
let sibling_file_path = files
.resolve(sibling.file)
.map(|p| relative_path_forward_slash(p.as_ref(), workspace_root))
.unwrap_or_default();
let sibling_language = files.language_for_file(sibling.file).map(|l| l.to_string());
let source = if files.is_external(sibling.file) {
LocationResolutionSource::ExternSymbol
} else {
LocationResolutionSource::CanonicalSibling
};
return Some(NodeLocation {
file_path: sibling_file_path,
line: sibling.start_line,
column: sibling.start_column,
end_line: sibling.end_line,
end_column: sibling.end_column,
language: sibling_language,
resolution_source: source,
});
}
}
}
let incoming = edges.edges_to(node_id);
for edge_ref in &incoming {
if !matches!(edge_ref.kind, EdgeKind::Calls { .. } | EdgeKind::References) {
continue;
}
for span in &edge_ref.spans {
let line = u32::try_from(span.start.line.saturating_add(1)).unwrap_or(u32::MAX);
let column = u32::try_from(span.start.column).unwrap_or(u32::MAX);
if line > 0 {
let edge_file_path = files
.resolve(edge_ref.file)
.map(|p| relative_path_forward_slash(p.as_ref(), workspace_root))
.unwrap_or_else(|| file_path.clone());
return Some(NodeLocation {
file_path: edge_file_path,
line,
column,
end_line: u32::try_from(span.end.line.saturating_add(1)).unwrap_or(u32::MAX),
end_column: u32::try_from(span.end.column).unwrap_or(u32::MAX),
language: language.clone(),
resolution_source: LocationResolutionSource::IncomingEdgeSpan,
});
}
}
}
Some(NodeLocation {
file_path,
line: entry.start_line,
column: entry.start_column,
end_line: entry.end_line,
end_column: entry.end_column,
language,
resolution_source: LocationResolutionSource::Fallback,
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
use sqry_core::graph::unified::concurrent::CodeGraph;
use sqry_core::graph::unified::node::NodeKind;
use sqry_core::graph::unified::storage::NodeEntry;
fn make_graph_with_nodes(nodes: Vec<(NodeKind, &str, u32, u32)>) -> (CodeGraph, Vec<NodeId>) {
let mut graph = CodeGraph::new();
let file_id = graph
.files_mut()
.register(std::path::Path::new("test.rs"))
.unwrap();
let mut ids = Vec::new();
for (kind, name, start_line, start_col) in &nodes {
let name_id = graph.strings_mut().intern(name).unwrap();
let qn_id = graph.strings_mut().intern(name).unwrap();
let entry = NodeEntry::new(*kind, name_id, file_id)
.with_location(*start_line, *start_col, *start_line + 5, 0)
.with_qualified_name(qn_id);
let node_id = graph.nodes_mut().alloc(entry).unwrap();
ids.push(node_id);
}
graph.rebuild_indices();
(graph, ids)
}
#[test]
fn test_own_span_resolution() {
let (graph, ids) = make_graph_with_nodes(vec![(NodeKind::Function, "main", 10, 0)]);
let ws = PathBuf::from("/workspace");
let loc = node_location_for_reporting(&graph, ids[0], &ws).unwrap();
assert_eq!(loc.line, 10);
assert_eq!(loc.resolution_source, LocationResolutionSource::OwnSpan);
}
#[test]
fn test_canonical_sibling_resolution() {
let (graph, ids) = make_graph_with_nodes(vec![
(NodeKind::Function, "kfree", 0, 0),
(NodeKind::Function, "kfree", 42, 0),
]);
let ws = PathBuf::from("/workspace");
let loc = node_location_for_reporting(&graph, ids[0], &ws).unwrap();
assert_eq!(loc.line, 42);
assert_eq!(
loc.resolution_source,
LocationResolutionSource::CanonicalSibling
);
}
#[test]
fn test_fallback_when_all_siblings_are_stubs() {
let (graph, ids) = make_graph_with_nodes(vec![(NodeKind::Function, "mystery", 0, 0)]);
let ws = PathBuf::from("/workspace");
let loc = node_location_for_reporting(&graph, ids[0], &ws).unwrap();
assert_eq!(loc.line, 0);
assert_eq!(loc.resolution_source, LocationResolutionSource::Fallback);
}
#[test]
fn test_no_infinite_recursion_two_stubs() {
let (graph, ids) = make_graph_with_nodes(vec![
(NodeKind::Function, "phantom", 0, 0),
(NodeKind::Function, "phantom", 0, 0),
]);
let ws = PathBuf::from("/workspace");
let loc = node_location_for_reporting(&graph, ids[0], &ws).unwrap();
assert_eq!(loc.line, 0);
assert_eq!(loc.resolution_source, LocationResolutionSource::Fallback);
}
#[test]
fn test_invalid_node_returns_none() {
let (graph, _ids) = make_graph_with_nodes(vec![]);
let ws = PathBuf::from("/workspace");
let result = node_location_for_reporting(&graph, NodeId::INVALID, &ws);
assert!(result.is_none());
}
}