use crate::db::models::CodeElement;
pub const MAX_MATCHES: usize = 20;
#[derive(Debug, Clone)]
pub struct Match {
pub score: u32,
pub tie_break: u32,
pub element: CodeElement,
}
fn type_rank(element_type: &str) -> u32 {
match element_type {
"function" | "method" | "constructor" => 0,
"class" | "struct" | "interface" | "enum" | "trait" => 1,
"route" | "module" | "property" | "field" => 2,
"file" => 3,
"directory" | "folder" => 4,
_ => 5,
}
}
fn basename(path: &str) -> &str {
path.rsplit('/').next().unwrap_or(path)
}
fn basename_stem(path: &str) -> &str {
let base = basename(path);
base.rsplit_once('.').map(|(stem, _)| stem).unwrap_or(base)
}
fn is_prefix(haystack: &str, needle: &str) -> bool {
haystack.len() >= needle.len() && haystack[..needle.len()].eq_ignore_ascii_case(needle)
}
fn score_candidate(element: &CodeElement, alias: &str) -> Option<u32> {
if element.name == alias {
return Some(0);
}
if element.name.eq_ignore_ascii_case(alias) {
return Some(1);
}
if element.file_path == alias {
return Some(2);
}
if basename(&element.file_path) == alias {
return Some(3);
}
if basename_stem(&element.file_path) == alias {
return Some(4);
}
if alias.len() >= 3 && is_prefix(&element.name, alias) {
return Some(5);
}
None
}
fn score_qualified(element: &CodeElement, path: &str, symbol: &str) -> Option<u32> {
if element.file_path == path {
if element.name == symbol {
return Some(0); }
if element.name.eq_ignore_ascii_case(symbol) {
return Some(1); }
if symbol.len() >= 3 && is_prefix(&element.name, symbol) {
return Some(2); }
return None;
}
if !path.contains('/') && basename(&element.file_path) == path && element.name == symbol {
return Some(3); }
None
}
struct Scored {
score: u32,
order: u32,
element: CodeElement,
}
pub fn resolve(elements: &[CodeElement], alias: &str, max_matches: usize) -> Vec<Match> {
let alias = alias.trim();
if alias.is_empty() || elements.is_empty() {
return Vec::new();
}
let max_matches = max_matches.max(1);
let qualified: Option<(&str, &str)> = alias
.split_once("::")
.or_else(|| alias.split_once('/'))
.map(|(p, s)| (p.trim(), s.trim()))
.filter(|(p, s)| !p.is_empty() && !s.is_empty());
let mut scored: Vec<Scored> = Vec::new();
for (order, element) in elements.iter().enumerate() {
let score = match qualified {
Some((path, symbol)) => score_qualified(element, path, symbol),
None => score_candidate(element, alias),
};
if let Some(score) = score {
scored.push(Scored {
score,
order: order as u32,
element: element.clone(),
});
}
}
scored.sort_by_key(|s| (s.score, type_rank(&s.element.element_type), s.order));
scored
.into_iter()
.take(max_matches)
.map(|s| Match {
score: s.score,
tie_break: s.order,
element: s.element,
})
.collect()
}
pub fn resolve_exact(elements: &[CodeElement], alias: &str) -> Option<CodeElement> {
resolve(elements, alias, MAX_MATCHES)
.into_iter()
.find(|m| m.score == 0)
.map(|m| m.element)
}
#[cfg(test)]
mod tests {
use super::*;
fn element(name: &str, file_path: &str, element_type: &str) -> CodeElement {
let qualified_name = if element_type == "file" {
file_path.to_string() } else {
format!("{}::{}", file_path, name)
};
CodeElement {
qualified_name,
element_type: element_type.to_string(),
name: name.to_string(),
file_path: file_path.to_string(),
line_start: 1,
line_end: 10,
language: "rust".to_string(),
..Default::default()
}
}
fn scores(matches: &[Match]) -> Vec<u32> {
matches.iter().map(|m| m.score).collect()
}
fn names(matches: &[Match]) -> Vec<&str> {
matches.iter().map(|m| m.element.name.as_str()).collect()
}
#[test]
fn alias_handler_resolves_to_handler_module_symbols() {
let elements = vec![
element("Handler", "src/mcp/handler.rs", "struct"),
element("handle_tool_call", "src/mcp/handler.rs", "function"),
element("handler", "src/other/handler.rs", "module"),
element("Handler", "src/other/handler.rs", "struct"),
];
let matches = resolve(&elements, "handler", 10);
assert_eq!(
matches[0].element.qualified_name,
"src/other/handler.rs::handler"
);
assert_eq!(matches[0].score, 0);
assert_eq!(matches[1].element.name, "Handler");
assert_eq!(matches[1].score, 1);
assert_eq!(matches[2].element.name, "Handler");
assert_eq!(matches[2].score, 1);
assert_eq!(matches[3].element.name, "handle_tool_call");
assert_eq!(matches[3].score, 4);
assert!(matches[3].element.file_path.contains("src/mcp/"));
}
#[test]
fn alias_handler_rs_symbol_resolves_by_path_and_name() {
let elements = vec![
element("handle_tool_call", "src/mcp/handler.rs", "function"),
element("handle_tool_call", "src/other/handler.rs", "function"),
];
let matches = resolve(&elements, "src/mcp/handler.rs::handle_tool_call", 10);
assert_eq!(matches.len(), 1);
assert_eq!(
matches[0].element.qualified_name,
"src/mcp/handler.rs::handle_tool_call"
);
assert_eq!(matches[0].score, 0);
}
#[test]
fn ambiguous_alias_returns_ranked_list_not_silent_pick() {
let elements = vec![
element("Handler", "src/a/handler.rs", "struct"),
element("Handler", "src/b/handler.rs", "struct"),
element("handler", "src/c/handler.rs", "module"),
element("handle_tool_call", "src/d/handler.rs", "function"),
];
let matches = resolve(&elements, "Handler", 10);
assert_eq!(matches.len(), 3);
assert_eq!(
matches[0].element.qualified_name,
"src/a/handler.rs::Handler"
);
assert_eq!(
matches[1].element.qualified_name,
"src/b/handler.rs::Handler"
);
assert_eq!(matches[0].score, matches[1].score);
assert!(matches[0].tie_break < matches[1].tie_break);
assert_eq!(matches[2].element.name, "handler");
assert_eq!(matches[2].score, 1);
}
#[test]
fn unknown_alias_returns_empty() {
let elements = vec![
element("handle_tool_call", "src/mcp/handler.rs", "function"),
element("get_dependencies", "src/graph/query.rs", "function"),
];
assert!(resolve(&elements, "nope_does_not_exist", 10).is_empty());
assert!(resolve(&elements, "", 10).is_empty());
}
#[test]
fn resolve_is_deterministic() {
let elements = vec![
element("Handler", "src/a/handler.rs", "struct"),
element("handler", "src/b/handler.rs", "module"),
element("handle_tool_call", "src/c/handler.rs", "function"),
];
let a = resolve(&elements, "handler", 10);
let b = resolve(&elements, "handler", 10);
assert_eq!(a.len(), b.len());
for (x, y) in a.iter().zip(b.iter()) {
assert_eq!(x.score, y.score);
assert_eq!(x.element.qualified_name, y.element.qualified_name);
assert_eq!(x.tie_break, y.tie_break);
}
}
#[test]
fn case_insensitive_fallback_before_prefix() {
let elements = vec![
element("Handler", "src/a/handler.rs", "struct"),
element("HandleToolCall", "src/b/handler.rs", "function"),
];
let matches = resolve(&elements, "handler", 10);
assert_eq!(matches.len(), 2);
assert_eq!(matches[0].element.name, "Handler"); assert_eq!(matches[1].element.name, "HandleToolCall"); assert_eq!(scores(&matches), vec![1, 4]);
}
#[test]
fn basename_alias_resolves_to_file_element() {
let elements = vec![
element("handler.rs", "src/mcp/handler.rs", "file"),
element("handle_tool_call", "src/mcp/handler.rs", "function"),
];
let matches = resolve(&elements, "handler.rs", 10);
assert_eq!(matches.len(), 2);
assert_eq!(matches[0].element.element_type, "file");
assert_eq!(matches[0].element.qualified_name, "src/mcp/handler.rs");
assert_eq!(matches[0].score, 0);
assert_eq!(matches[1].element.name, "handle_tool_call");
assert_eq!(matches[1].score, 3);
}
#[test]
fn slash_qualified_alias_matches_basename() {
let elements = vec![
element("handle_tool_call", "src/mcp/handler.rs", "function"),
element("handle_tool_call", "src/other/handler.rs", "function"),
];
let matches = resolve(&elements, "handler.rs/handle_tool_call", 10);
assert_eq!(matches.len(), 2);
assert_eq!(
matches[0].element.qualified_name,
"src/mcp/handler.rs::handle_tool_call"
);
assert_eq!(matches[0].score, 3);
assert_eq!(
matches[1].element.qualified_name,
"src/other/handler.rs::handle_tool_call"
);
}
#[test]
fn type_rank_breaks_collisions_deterministically() {
let elements = vec![
element("Handler", "src/a/handler.rs", "file"),
element("Handler", "src/b/handler.rs", "struct"),
];
let matches = resolve(&elements, "Handler", 10);
assert_eq!(matches.len(), 2);
assert_eq!(matches[0].element.element_type, "struct");
assert_eq!(matches[1].element.element_type, "file");
}
#[test]
fn empty_input_returns_empty() {
assert!(resolve(&[], "anything", 10).is_empty());
}
#[test]
fn resolve_exact_returns_none_for_fuzzy_only() {
let elements = vec![
element("handle_tool_call", "src/mcp/handler.rs", "function"),
element("Handler", "src/mcp/handler.rs", "struct"),
];
assert!(resolve_exact(&elements, "handl").is_none());
let exact = resolve_exact(&elements, "Handler").unwrap();
assert_eq!(exact.qualified_name, "src/mcp/handler.rs::Handler");
}
}