pub mod cross_file;
pub mod module_resolver;
pub mod references;
use crate::error::{Result, SpliceError};
use crate::graph::CodeGraph;
use serde::Serialize;
use sqlitegraph::{NodeId, SnapshotId};
use std::path::{Path, PathBuf};
pub fn normalize_lookup_path(path: &Path) -> PathBuf {
if let Ok(canonical) = std::fs::canonicalize(path) {
canonical
} else {
std::env::current_dir()
.unwrap_or_else(|_| PathBuf::from("."))
.join(path)
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct ResolvedSpan {
#[serde(skip_serializing)]
pub node_id: NodeId,
pub match_id: String,
pub name: String,
pub kind: String,
pub language: Option<String>,
pub file_path: String,
pub byte_start: usize,
pub byte_end: usize,
pub line_start: usize,
pub line_end: usize,
pub col_start: usize,
pub col_end: usize,
}
pub fn resolve_symbol(
graph: &CodeGraph,
file: Option<&Path>,
kind: Option<&str>,
name: &str,
) -> Result<ResolvedSpan> {
use uuid::Uuid;
let match_id = Uuid::new_v4().to_string();
let _cache_key = if let Some(file_path) = file {
let file_str = file_path
.to_str()
.ok_or_else(|| SpliceError::Other(format!("Invalid UTF-8 in path: {:?}", file_path)))?;
format!("{}::{}", file_str, name)
} else {
name.to_string()
};
if let Some(file_path) = file {
let normalized = normalize_lookup_path(file_path);
return resolve_symbol_in_file(graph, &normalized, kind, name, &match_id);
}
let all_matches = graph.find_symbols_by_name(name);
if all_matches.is_empty() {
let all_symbols = graph.all_symbol_names();
let suggestions = crate::suggestions::suggest_similar_symbols(name, &all_symbols, 3);
let hint = if suggestions.is_empty() {
format!(
"Symbol '{}' not found. Run `splice ingest` to index the codebase.",
name
)
} else {
format!("Did you mean: {}?", suggestions.join(", "))
};
return Err(SpliceError::SymbolNotFound {
message: format!("Symbol '{}' not found", name),
symbol: name.to_string(),
file: None,
hint,
});
}
if all_matches.len() > 1 {
let files: Vec<String> = all_matches
.into_iter()
.filter_map(|(_id, path)| path)
.collect();
return Err(SpliceError::AmbiguousSymbol {
name: name.to_string(),
files,
});
}
let (node_id, file_path) = all_matches.into_iter().next().unwrap();
let file_path_str =
file_path.ok_or_else(|| SpliceError::Other("Symbol node missing file_path".to_string()))?;
let backend = graph.inner()?;
let node = backend.get_node(SnapshotId(0), node_id.as_i64())?;
let byte_start = node
.data
.get("byte_start")
.and_then(|v| v.as_u64())
.ok_or_else(|| SpliceError::Other("Missing byte_start property".to_string()))?
as usize;
let byte_end = node
.data
.get("byte_end")
.and_then(|v| v.as_u64())
.ok_or_else(|| SpliceError::Other("Missing byte_end property".to_string()))?
as usize;
let kind_str = node
.data
.get("kind")
.and_then(|v| v.as_str())
.ok_or_else(|| SpliceError::Other("Missing kind property".to_string()))?
.to_string();
let language = node
.data
.get("language")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let line_start = node
.data
.get("start_line")
.and_then(|v| v.as_u64())
.unwrap_or(0) as usize;
let line_end = node
.data
.get("end_line")
.and_then(|v| v.as_u64())
.unwrap_or(0) as usize;
let col_start = node
.data
.get("start_col")
.and_then(|v| v.as_u64())
.unwrap_or(0) as usize;
let col_end = node
.data
.get("end_col")
.and_then(|v| v.as_u64())
.unwrap_or(0) as usize;
Ok(ResolvedSpan {
node_id,
match_id,
name: name.to_string(),
kind: kind_str,
language,
file_path: file_path_str,
byte_start,
byte_end,
line_start,
line_end,
col_start,
col_end,
})
}
fn resolve_symbol_in_file(
graph: &CodeGraph,
file_path: &Path,
kind: Option<&str>,
name: &str,
match_id: &str,
) -> Result<ResolvedSpan> {
let file_str = file_path
.to_str()
.ok_or_else(|| SpliceError::Other(format!("Invalid UTF-8 in path: {:?}", file_path)))?;
let node_id = match graph.find_symbol_in_file(file_str, name) {
Some(id) => id,
None => {
let all_symbols = graph.all_symbol_names();
let suggestions = crate::suggestions::suggest_similar_symbols(name, &all_symbols, 3);
let hint = if suggestions.is_empty() {
format!(
"Symbol '{}' not found in {}. Run `splice ingest` to index the codebase.",
name, file_str
)
} else {
format!("Did you mean: {}?", suggestions.join(", "))
};
return Err(SpliceError::SymbolNotFound {
message: format!("Symbol '{}' not found in {}", name, file_str),
symbol: name.to_string(),
file: Some(file_path.to_path_buf()),
hint,
});
}
};
let backend = graph.inner()?;
let node = backend.get_node(SnapshotId(0), node_id.as_i64())?;
let byte_start = node
.data
.get("byte_start")
.and_then(|v| v.as_u64())
.ok_or_else(|| SpliceError::Other("Missing byte_start property".to_string()))?
as usize;
let byte_end = node
.data
.get("byte_end")
.and_then(|v| v.as_u64())
.ok_or_else(|| SpliceError::Other("Missing byte_end property".to_string()))?
as usize;
let kind_str = node
.data
.get("kind")
.and_then(|v| v.as_str())
.ok_or_else(|| SpliceError::Other("Missing kind property".to_string()))?
.to_string();
let language = node
.data
.get("language")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
if let Some(k) = kind {
if kind_str != k {
return Err(SpliceError::SymbolNotFound {
message: format!(
"Symbol '{}' of kind '{}' not found in {}",
name, k, file_str
),
symbol: name.to_string(),
file: Some(file_path.to_path_buf()),
hint: format!(
"Symbol '{}' exists but is a '{}', not '{}'. Try adjusting the --kind flag.",
name, kind_str, k
),
});
}
}
let node_file_path = node
.file_path
.as_deref()
.ok_or_else(|| SpliceError::Other("Missing file_path in node".to_string()))?
.to_string();
let line_start = node
.data
.get("line_start")
.and_then(|v| v.as_u64())
.unwrap_or(0) as usize;
let line_end = node
.data
.get("line_end")
.and_then(|v| v.as_u64())
.unwrap_or(0) as usize;
let col_start = node
.data
.get("col_start")
.and_then(|v| v.as_u64())
.unwrap_or(0) as usize;
let col_end = node
.data
.get("col_end")
.and_then(|v| v.as_u64())
.unwrap_or(0) as usize;
Ok(ResolvedSpan {
node_id,
match_id: match_id.to_string(),
name: name.to_string(),
kind: kind_str,
language,
file_path: node_file_path,
byte_start,
byte_end,
line_start,
line_end,
col_start,
col_end,
})
}
#[deprecated(
since = "2.2.0",
note = "Use resolve_symbol with Option<&str> kind instead. This function will be removed in v3.0."
)]
pub fn resolve_symbol_with_rust_kind(
graph: &CodeGraph,
file: Option<&Path>,
kind: Option<crate::ingest::rust::RustSymbolKind>,
name: &str,
) -> Result<ResolvedSpan> {
let kind_str = kind.map(|k| k.as_str().to_string());
resolve_symbol(graph, file, kind_str.as_deref(), name)
}
pub fn find_symbol_or_suggest(
graph: &CodeGraph,
name: &str,
file: Option<&Path>,
) -> Result<NodeId> {
if let Some(file_path) = file {
let normalized = normalize_lookup_path(file_path);
if let Some(file_str) = normalized.to_str() {
if let Some(node_id) = graph.find_symbol_in_file(file_str, name) {
return Ok(node_id);
}
}
}
let all_matches = graph.find_symbols_by_name(name);
if !all_matches.is_empty() {
if let Some((node_id, _)) = all_matches.first() {
return Ok(*node_id);
}
}
let all_symbols = graph.all_symbol_names();
let suggestions = crate::suggestions::suggest_similar_symbols(name, &all_symbols, 3);
let hint = if suggestions.is_empty() {
format!(
"Symbol '{}' not found. Run `splice ingest` to index the codebase.",
name
)
} else {
format!("Did you mean: {}?", suggestions.join(", "))
};
Err(SpliceError::SymbolNotFound {
message: format!("Symbol '{}' not found", name),
symbol: name.to_string(),
file: file.map(|p| p.to_path_buf()),
hint,
})
}