use crate::types::{DocIndex, ItemKind, QueryResult};
pub(crate) fn lookup(index: &DocIndex, query: &str, kind_filter: Option<ItemKind>) -> QueryResult {
let segments: Vec<&str> = if query.is_empty() {
Vec::new()
} else {
query.split("::").collect()
};
let query_path = segments.join("::");
let query_lower = query_path.to_lowercase();
if let Some(result) = try_exact_path_match(index, &query_path, kind_filter) {
return result;
}
if let Some(result) =
try_case_insensitive_path_match(index, &query_path, &query_lower, kind_filter)
{
return result;
}
if let Some(result) = try_suffix_match(index, &query_path, &query_lower, kind_filter) {
return result;
}
if let Some(result) = try_name_match(index, &segments, &query_path, kind_filter) {
return result;
}
let suggestions = compute_suggestions(index, &query_path);
QueryResult::NotFound {
query: query_path,
suggestions,
}
}
fn try_exact_path_match(
index: &DocIndex,
query_path: &str,
kind_filter: Option<ItemKind>,
) -> Option<QueryResult> {
let indices = index.lookup_by_path(query_path);
if indices.is_empty() {
return None;
}
let filtered = apply_kind_filter(index, indices, kind_filter);
Some(classify_results(index, &filtered, query_path))
}
fn try_case_insensitive_path_match(
index: &DocIndex,
query_path: &str,
query_lower: &str,
kind_filter: Option<ItemKind>,
) -> Option<QueryResult> {
let suffix_indices = index.lookup_by_suffix(query_lower);
if suffix_indices.is_empty() {
return None;
}
let query_segment_count = query_path.split("::").count();
let ci_path_matches: Vec<usize> = suffix_indices
.iter()
.copied()
.filter(|&i| {
let item = &index.items[i];
item.path.to_lowercase() == query_lower
&& item.path.split("::").count() == query_segment_count
})
.collect();
if ci_path_matches.is_empty() {
return None;
}
let filtered = apply_kind_filter(index, &ci_path_matches, kind_filter);
let case_filtered = apply_case_sensitivity(index, &filtered, query_path);
if case_filtered.is_empty() {
return None; }
Some(classify_results(index, &case_filtered, query_path))
}
fn try_suffix_match(
index: &DocIndex,
query_path: &str,
query_lower: &str,
kind_filter: Option<ItemKind>,
) -> Option<QueryResult> {
let suffix_indices = index.lookup_by_suffix(query_lower);
if suffix_indices.is_empty() {
return None;
}
let filtered = apply_kind_filter(index, suffix_indices, kind_filter);
let case_filtered = apply_case_sensitivity(index, &filtered, query_path);
let query_segments: Vec<&str> = query_lower.split("::").collect();
let exact_suffix = filter_exact_suffix_matches(index, &case_filtered, &query_segments);
if !exact_suffix.is_empty() {
let non_duplicate = filter_non_duplicate_matches(index, &exact_suffix, &query_segments);
if !non_duplicate.is_empty() {
return Some(classify_results(index, &non_duplicate, query_path));
}
return Some(classify_results(index, &exact_suffix, query_path));
}
if !case_filtered.is_empty() {
return Some(classify_results(index, &case_filtered, query_path));
}
None
}
fn filter_exact_suffix_matches(
index: &DocIndex,
indices: &[usize],
query_segments: &[&str],
) -> Vec<usize> {
indices
.iter()
.copied()
.filter(|&idx| {
let item_segments: Vec<&str> = index.items[idx].path.split("::").collect();
if item_segments.len() < query_segments.len() {
return false;
}
let offset = item_segments.len() - query_segments.len();
item_segments[offset..]
.iter()
.zip(query_segments.iter())
.all(|(item_seg, query_seg)| item_seg.to_lowercase() == *query_seg)
})
.collect()
}
fn filter_non_duplicate_matches(
index: &DocIndex,
indices: &[usize],
query_segments: &[&str],
) -> Vec<usize> {
if query_segments.len() != 1 {
return indices.to_vec(); }
let query_seg = query_segments[0];
indices
.iter()
.copied()
.filter(|&idx| {
let item_segments_lower: Vec<String> = index.items[idx]
.path
.to_lowercase()
.split("::")
.map(String::from)
.collect();
let offset = item_segments_lower.len() - 1;
offset == 0
|| !item_segments_lower[..offset]
.iter()
.any(|seg| seg == query_seg)
})
.collect()
}
fn try_name_match(
index: &DocIndex,
segments: &[&str],
query_path: &str,
kind_filter: Option<ItemKind>,
) -> Option<QueryResult> {
if segments.len() != 1 {
return None;
}
let name_lower = segments[0].to_lowercase();
let name_indices = index.lookup_by_name(&name_lower);
if name_indices.is_empty() {
return None;
}
let filtered = apply_kind_filter(index, name_indices, kind_filter);
let case_filtered = apply_case_sensitivity(index, &filtered, segments[0]);
if case_filtered.is_empty() {
return None;
}
Some(classify_results(index, &case_filtered, query_path))
}
fn apply_kind_filter(
index: &DocIndex,
indices: &[usize],
kind_filter: Option<ItemKind>,
) -> Vec<usize> {
match kind_filter {
None => indices.to_vec(),
Some(filter) => indices
.iter()
.copied()
.filter(|&i| index.items[i].kind.matches_filter(filter))
.collect(),
}
}
fn apply_case_sensitivity(index: &DocIndex, indices: &[usize], query: &str) -> Vec<usize> {
if !query.chars().any(char::is_uppercase) {
return indices.to_vec();
}
if query.contains("::") {
let query_segments: Vec<&str> = query.split("::").collect();
indices
.iter()
.copied()
.filter(|&idx| {
let item_segments: Vec<&str> = index.items[idx].path.split("::").collect();
if item_segments.len() < query_segments.len() {
return false;
}
let offset = item_segments.len() - query_segments.len();
item_segments[offset..] == query_segments[..]
})
.collect()
} else {
indices
.iter()
.copied()
.filter(|&idx| index.items[idx].name == query)
.collect()
}
}
fn classify_results(index: &DocIndex, indices: &[usize], query: &str) -> QueryResult {
if indices.is_empty() {
return QueryResult::NotFound {
query: query.to_string(),
suggestions: compute_suggestions(index, query),
};
}
let resolved = resolve_reexport_stubs(index, indices);
let identity_deduped = deduplicate_by_identity(index, &resolved);
let deduped = deduplicate_by_path_kind(index, &identity_deduped);
if deduped.len() == 1 {
return QueryResult::Found { index: deduped[0] };
}
if let Some(selected) = try_auto_select(index, &deduped) {
return QueryResult::Found { index: selected };
}
let sorted = sort_by_priority(index, &deduped);
QueryResult::Ambiguous {
indices: sorted,
query: query.to_string(),
}
}
fn resolve_reexport_stubs(index: &DocIndex, indices: &[usize]) -> Vec<usize> {
let mut result = Vec::with_capacity(indices.len());
let index_set: std::collections::HashSet<usize> = indices.iter().copied().collect();
for &idx in indices {
let item = &index.items[idx];
if is_reexport_stub(item) {
let has_canonical = indices.iter().any(|&other_idx| {
other_idx != idx && {
let other = &index.items[other_idx];
other.name == item.name && !is_reexport_stub(other)
}
});
if has_canonical {
continue; }
let name_lower = item.name.to_lowercase();
let name_indices = index.lookup_by_name(&name_lower);
if !name_indices.is_empty() {
let canonical = name_indices.iter().find(|&&ni| {
!index_set.contains(&ni) && {
let candidate = &index.items[ni];
candidate.name == item.name && !is_reexport_stub(candidate)
}
});
if let Some(&canonical_idx) = canonical {
result.push(canonical_idx);
continue;
}
}
}
result.push(idx);
}
result
}
pub(crate) fn is_reexport_stub(item: &crate::types::IndexItem) -> bool {
item.signature.starts_with("pub use ") && item.children.is_empty()
}
fn deduplicate_by_identity(index: &DocIndex, indices: &[usize]) -> Vec<usize> {
if indices.len() <= 1 {
return indices.to_vec();
}
let reexport_sources: std::collections::HashSet<String> = indices
.iter()
.filter_map(|&idx| {
index.items[idx]
.reexport_source
.as_ref()
.map(|s| s.to_lowercase())
})
.collect();
let mut groups: std::collections::HashMap<(String, ItemKind), Vec<usize>> =
std::collections::HashMap::new();
let mut ungrouped: Vec<usize> = Vec::new();
for &idx in indices {
let item = &index.items[idx];
if let Some(ref source) = item.reexport_source {
let key = (source.to_lowercase(), item.kind);
groups.entry(key).or_default().push(idx);
} else if reexport_sources.contains(&item.path.to_lowercase()) {
let key = (item.path.to_lowercase(), item.kind);
groups.entry(key).or_default().push(idx);
} else {
ungrouped.push(idx);
}
}
let mut kept: std::collections::HashSet<usize> = std::collections::HashSet::new();
for members in groups.values() {
let best = members
.iter()
.copied()
.min_by_key(|&idx| index.items[idx].path.matches("::").count())
.expect("invariant: group is non-empty");
kept.insert(best);
}
for &idx in &ungrouped {
kept.insert(idx);
}
indices
.iter()
.copied()
.filter(|idx| kept.contains(idx))
.collect()
}
fn deduplicate_by_path_kind(index: &DocIndex, indices: &[usize]) -> Vec<usize> {
let mut seen = std::collections::HashSet::new();
indices
.iter()
.copied()
.filter(|&idx| {
let item = &index.items[idx];
seen.insert((item.path.clone(), item.kind))
})
.collect()
}
fn try_auto_select(index: &DocIndex, indices: &[usize]) -> Option<usize> {
let mut crate_root_primary = Vec::new();
let mut crate_root_other = Vec::new();
let mut nested = Vec::new();
for &idx in indices {
let item = &index.items[idx];
let depth = item.path.split("::").count();
if depth == 2 && item.kind.is_primary() {
crate_root_primary.push(idx);
} else if depth == 2 {
crate_root_other.push(idx);
} else {
nested.push(idx);
}
}
let has_3_level_primary = nested.iter().any(|&idx| {
let item = &index.items[idx];
item.path.split("::").count() == 3 && item.kind.is_primary()
});
if crate_root_primary.len() == 1 && crate_root_other.is_empty() && !has_3_level_primary {
Some(crate_root_primary[0])
} else {
None
}
}
fn sort_by_priority(index: &DocIndex, indices: &[usize]) -> Vec<usize> {
let mut crate_root_primary: Vec<usize> = Vec::new();
let mut crate_root_other: Vec<usize> = Vec::new();
let mut nested: Vec<usize> = Vec::new();
for &idx in indices {
let item = &index.items[idx];
let depth = item.path.split("::").count();
if depth == 2 && item.kind.is_primary() {
crate_root_primary.push(idx);
} else if depth == 2 {
crate_root_other.push(idx);
} else {
nested.push(idx);
}
}
crate_root_primary.sort_by(|&a, &b| index.items[a].path.cmp(&index.items[b].path));
crate_root_other.sort_by(|&a, &b| index.items[a].path.cmp(&index.items[b].path));
nested.sort_by(|&a, &b| {
let depth_a = index.items[a].path.split("::").count();
let depth_b = index.items[b].path.split("::").count();
depth_a
.cmp(&depth_b)
.then_with(|| index.items[a].path.cmp(&index.items[b].path))
});
let mut result = Vec::with_capacity(indices.len());
result.extend(crate_root_primary);
result.extend(crate_root_other);
result.extend(nested);
result
}
fn collect_suggestions(
scored: impl IntoIterator<Item = (String, usize)>,
max: usize,
) -> Vec<String> {
let mut candidates: Vec<(String, usize)> = scored.into_iter().collect();
candidates.sort_by(|a, b| a.1.cmp(&b.1).then_with(|| a.0.cmp(&b.0)));
let mut seen = std::collections::HashSet::new();
candidates.retain(|(path, _)| seen.insert(path.clone()));
candidates.truncate(max);
candidates.into_iter().map(|(path, _)| path).collect()
}
fn compute_suggestions(index: &DocIndex, query: &str) -> Vec<String> {
let query_lower = query.to_lowercase();
let last_segment = query.split("::").last().unwrap_or(query).to_lowercase();
let scored = index.items.iter().filter_map(|item| {
let path_lower = item.path.to_lowercase();
let name_lower = item.name.to_lowercase();
if !could_match_within_distance(&query_lower, &path_lower, 3)
&& !could_match_within_distance(&query_lower, &name_lower, 3)
&& !could_match_within_distance(&last_segment, &name_lower, 3)
{
return None;
}
let path_dist = levenshtein_distance(&query_lower, &path_lower);
let name_dist = levenshtein_distance(&query_lower, &name_lower);
let seg_dist = levenshtein_distance(&last_segment, &name_lower);
let distance = path_dist.min(name_dist).min(seg_dist);
if distance <= 3 {
Some((item.path.clone(), distance))
} else {
None
}
});
collect_suggestions(scored, 5)
}
fn could_match_within_distance(s1: &str, s2: &str, max_dist: usize) -> bool {
let len1 = s1.len();
let len2 = s2.len();
let len_diff = len1.abs_diff(len2);
if len_diff > max_dist {
return false;
}
if len1 > 2 && len2 > 2 && s1.as_bytes()[0] != s2.as_bytes()[0] && len_diff >= max_dist {
return false;
}
true
}
pub(crate) fn lookup_method(
index: &DocIndex,
parent_segments: &[&str],
method_name: &str,
kind_filter: Option<ItemKind>,
) -> QueryResult {
let parent_query = parent_segments.join("::");
let parent_result = lookup(index, &parent_query, None);
match parent_result {
QueryResult::Found { index: parent_idx } => {
let parent_item = &index.items[parent_idx];
let method_lower = method_name.to_lowercase();
let matching_children: Vec<usize> = parent_item
.children
.iter()
.filter(|child| {
child.name.to_lowercase() == method_lower
&& kind_filter.is_none_or(|kf| child.kind.matches_filter(kf))
})
.map(|child| child.index)
.collect();
let case_filtered = apply_case_sensitivity(index, &matching_children, method_name);
if !case_filtered.is_empty() {
let full_path = format!("{}::{method_name}", parent_item.path);
return classify_results(index, &case_filtered, &full_path);
}
let full_path = format!("{}::{method_name}", parent_item.path);
let suggestions = compute_method_suggestions(index, parent_idx, method_name);
QueryResult::NotFound {
query: full_path,
suggestions,
}
}
other => other,
}
}
fn compute_method_suggestions(
index: &DocIndex,
parent_idx: usize,
method_name: &str,
) -> Vec<String> {
let method_lower = method_name.to_lowercase();
let parent_item = &index.items[parent_idx];
let scored = parent_item.children.iter().filter_map(|child| {
let child_lower = child.name.to_lowercase();
if !could_match_within_distance(&method_lower, &child_lower, 3) {
return None;
}
let distance = levenshtein_distance(&method_lower, &child_lower);
if distance <= 3 {
let path = format!("{}::{}", parent_item.path, child.name);
Some((path, distance))
} else {
None
}
});
collect_suggestions(scored, 5)
}
pub(crate) fn looks_like_item_name(query: &str) -> bool {
const COMMON_METHODS: &[&str] = &[
"clone", "default", "into", "from", "borrow", "deref", "format", "parse", "write", "read",
"open", "close", "send", "recv",
];
if query.is_empty() {
return false;
}
if query.contains('-') {
return false;
}
if query.chars().any(char::is_uppercase) {
return true;
}
if query.contains('_') {
let segments: Vec<&str> = query.split('_').collect();
let all_simple = segments
.iter()
.all(|s| s.chars().all(|c| c.is_lowercase() || c.is_ascii_digit()));
let avg_len = if segments.is_empty() {
0
} else {
segments.iter().map(|s| s.len()).sum::<usize>() / segments.len()
};
if segments.len() <= 3 && all_simple && avg_len <= 6 {
return false; }
return true; }
if query.len() <= 4 {
return true; }
if COMMON_METHODS.contains(&query) {
return true;
}
false
}
fn levenshtein_distance(s1: &str, s2: &str) -> usize {
let a: Vec<char> = s1.chars().collect();
let b: Vec<char> = s2.chars().collect();
let m = a.len();
let n = b.len();
if m == 0 {
return n;
}
if n == 0 {
return m;
}
let mut prev: Vec<usize> = (0..=n).collect();
let mut curr = vec![0; n + 1];
for i in 1..=m {
curr[0] = i;
for j in 1..=n {
let cost = usize::from(a[i - 1] != b[j - 1]);
curr[j] = (prev[j] + 1).min(curr[j - 1] + 1).min(prev[j - 1] + cost);
}
std::mem::swap(&mut prev, &mut curr);
}
prev[n]
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::make_item;
use crate::types::ChildRef;
fn build_test_index() -> DocIndex {
let mut index = DocIndex::new("tokio".to_string(), "1.0.0".to_string());
index.add_item(make_item("tokio", "tokio", ItemKind::Module));
index.add_item(make_item("sync", "tokio::sync", ItemKind::Module));
index.add_item(make_item("Mutex", "tokio::sync::Mutex", ItemKind::Struct));
index.add_item(make_item("RwLock", "tokio::sync::RwLock", ItemKind::Struct));
index.add_item(make_item("spawn", "tokio::spawn", ItemKind::Function));
index.add_item(make_item("Runtime", "tokio::Runtime", ItemKind::Struct));
index.add_item(make_item(
"Builder",
"tokio::runtime::Builder",
ItemKind::Struct,
));
index.add_item(make_item(
"MAX_THREADS",
"tokio::runtime::MAX_THREADS",
ItemKind::Constant,
));
index
}
#[test]
fn lookup_returns_found_when_exact_path_matches() {
let index = build_test_index();
let result = lookup(&index, "tokio::sync::Mutex", None);
match result {
QueryResult::Found { index: idx } => {
assert_eq!(idx, 2);
}
other => panic!("expected Found, got {other:?}"),
}
}
#[test]
fn lookup_returns_found_for_exact_crate_root() {
let index = build_test_index();
let result = lookup(&index, "tokio", None);
match result {
QueryResult::Found { index: idx } => {
assert_eq!(idx, 0);
}
other => panic!("expected Found, got {other:?}"),
}
}
#[test]
fn lookup_returns_found_when_case_insensitive_path_matches() {
let index = build_test_index();
let result = lookup(&index, "mutex", None);
match result {
QueryResult::Found { index: idx } => {
assert_eq!(index.items[idx].name, "Mutex");
}
other => panic!("expected Found, got {other:?}"),
}
}
#[test]
fn lookup_case_insensitive_full_path() {
let index = build_test_index();
let result = lookup(&index, "tokio::sync::mutex", None);
match result {
QueryResult::Found { index: idx } => {
assert_eq!(index.items[idx].path, "tokio::sync::Mutex");
}
other => panic!("expected Found, got {other:?}"),
}
}
#[test]
fn lookup_returns_found_when_suffix_matches() {
let index = build_test_index();
let result = lookup(&index, "sync::Mutex", None);
match result {
QueryResult::Found { index: idx } => {
assert_eq!(index.items[idx].path, "tokio::sync::Mutex");
}
other => panic!("expected Found, got {other:?}"),
}
}
#[test]
fn lookup_suffix_match_case_insensitive() {
let index = build_test_index();
let result = lookup(&index, "sync::mutex", None);
match result {
QueryResult::Found { index: idx } => {
assert_eq!(index.items[idx].path, "tokio::sync::Mutex");
}
other => panic!("expected Found, got {other:?}"),
}
}
#[test]
fn lookup_prefers_non_duplicate_suffix_match() {
let mut index = DocIndex::new("mycrate".to_string(), "1.0.0".to_string());
index.add_item(make_item("sync", "mycrate::sync::sync", ItemKind::Function));
index.add_item(make_item("sync", "mycrate::sync", ItemKind::Module));
let result = lookup(&index, "sync", None);
match result {
QueryResult::Found { index: idx } => {
assert_eq!(index.items[idx].path, "mycrate::sync");
}
other => panic!("expected Found, got {other:?}"),
}
}
#[test]
fn lookup_returns_found_via_name_match() {
let mut index = DocIndex::new("mycrate".to_string(), "1.0.0".to_string());
index.add_item(make_item("Foo", "mycrate::inner::Foo", ItemKind::Struct));
let result = lookup(&index, "Foo", None);
match result {
QueryResult::Found { index: idx } => {
assert_eq!(index.items[idx].name, "Foo");
}
other => panic!("expected Found, got {other:?}"),
}
}
#[test]
fn lookup_kind_filter_narrows_results() {
let mut index = DocIndex::new("mycrate".to_string(), "1.0.0".to_string());
index.add_item(make_item("Foo", "mycrate::Foo", ItemKind::Struct));
index.add_item(make_item("Foo", "mycrate::Foo", ItemKind::Function));
let result = lookup(&index, "mycrate::Foo", Some(ItemKind::Struct));
match result {
QueryResult::Found { index: idx } => {
assert_eq!(index.items[idx].kind, ItemKind::Struct);
}
other => panic!("expected Found, got {other:?}"),
}
let result = lookup(&index, "mycrate::Foo", Some(ItemKind::Function));
match result {
QueryResult::Found { index: idx } => {
assert_eq!(index.items[idx].kind, ItemKind::Function);
}
other => panic!("expected Found, got {other:?}"),
}
}
#[test]
fn lookup_kind_filter_relaxation_when_no_results() {
let mut index = DocIndex::new("mycrate".to_string(), "1.0.0".to_string());
index.add_item(make_item("Foo", "mycrate::Foo", ItemKind::Module));
let result = lookup(&index, "mycrate::Foo", Some(ItemKind::Function));
assert!(matches!(result, QueryResult::NotFound { .. }));
let result = lookup(&index, "mycrate::Foo", None);
assert!(matches!(result, QueryResult::Found { .. }));
}
#[test]
fn lookup_returns_ambiguous_when_multiple_matches() {
let mut index = DocIndex::new("mycrate".to_string(), "1.0.0".to_string());
index.add_item(make_item(
"Builder",
"mycrate::http::Builder",
ItemKind::Struct,
));
index.add_item(make_item(
"Builder",
"mycrate::runtime::Builder",
ItemKind::Struct,
));
let result = lookup(&index, "Builder", None);
match result {
QueryResult::Ambiguous { indices, .. } => {
assert_eq!(indices.len(), 2);
}
other => panic!("expected Ambiguous, got {other:?}"),
}
}
#[test]
fn lookup_returns_not_found_when_nothing_matches() {
let index = build_test_index();
let result = lookup(&index, "NonexistentItem", None);
match result {
QueryResult::NotFound { query, .. } => {
assert_eq!(query, "NonexistentItem");
}
other => panic!("expected NotFound, got {other:?}"),
}
}
#[test]
fn lookup_case_sensitive_uppercase_query_matches_exactly() {
let mut index = DocIndex::new("mycrate".to_string(), "1.0.0".to_string());
index.add_item(make_item("Mutex", "mycrate::Mutex", ItemKind::Struct));
index.add_item(make_item("mutex", "mycrate::mutex", ItemKind::Module));
let result = lookup(&index, "Mutex", None);
match result {
QueryResult::Found { index: idx } => {
assert_eq!(index.items[idx].name, "Mutex");
assert_eq!(index.items[idx].kind, ItemKind::Struct);
}
other => panic!("expected Found, got {other:?}"),
}
}
#[test]
fn lookup_case_insensitive_lowercase_query_matches_any_case() {
let mut index = DocIndex::new("mycrate".to_string(), "1.0.0".to_string());
index.add_item(make_item("Mutex", "mycrate::Mutex", ItemKind::Struct));
let result = lookup(&index, "mutex", None);
match result {
QueryResult::Found { index: idx } => {
assert_eq!(index.items[idx].name, "Mutex");
}
other => panic!("expected Found, got {other:?}"),
}
}
#[test]
fn lookup_case_sensitive_uppercase_does_not_match_different_case() {
let mut index = DocIndex::new("mycrate".to_string(), "1.0.0".to_string());
index.add_item(make_item("mutex", "mycrate::mutex", ItemKind::Module));
let result = lookup(&index, "Mutex", None);
assert!(matches!(result, QueryResult::NotFound { .. }));
}
#[test]
fn lookup_case_sensitive_multi_segment() {
let mut index = DocIndex::new("mycrate".to_string(), "1.0.0".to_string());
index.add_item(make_item("Mutex", "mycrate::sync::Mutex", ItemKind::Struct));
index.add_item(make_item("mutex", "mycrate::sync::mutex", ItemKind::Module));
let result = lookup(&index, "sync::Mutex", None);
match result {
QueryResult::Found { index: idx } => {
assert_eq!(index.items[idx].path, "mycrate::sync::Mutex");
}
other => panic!("expected Found, got {other:?}"),
}
}
#[test]
fn lookup_auto_selects_crate_root_primary() {
let mut index = DocIndex::new("serde".to_string(), "1.0.0".to_string());
index.add_item(make_item(
"Deserialize",
"serde::Deserialize",
ItemKind::Trait,
));
index.add_item(make_item(
"Deserialize",
"serde::de::Deserialize",
ItemKind::Macro,
));
let result = lookup(&index, "Deserialize", None);
match result {
QueryResult::Found { index: idx } => {
assert_eq!(index.items[idx].path, "serde::Deserialize");
}
other => panic!("expected Found via auto-selection, got {other:?}"),
}
}
#[test]
fn lookup_deduplicates_by_path_and_kind() {
let mut index = DocIndex::new("mycrate".to_string(), "1.0.0".to_string());
index.add_item(make_item("Foo", "mycrate::Foo", ItemKind::Struct));
index.add_item(make_item("Foo", "mycrate::Foo", ItemKind::Struct));
let result = lookup(&index, "mycrate::Foo", None);
match result {
QueryResult::Found { .. } => {} other => panic!("expected Found after dedup, got {other:?}"),
}
}
#[test]
fn lookup_preserves_different_kinds_same_path() {
let mut index = DocIndex::new("mycrate".to_string(), "1.0.0".to_string());
index.add_item(make_item("Parser", "mycrate::Parser", ItemKind::Trait));
index.add_item(make_item("Parser", "mycrate::Parser", ItemKind::ProcMacro));
let result = lookup(&index, "mycrate::Parser", None);
match result {
QueryResult::Ambiguous { indices, .. } => {
assert_eq!(indices.len(), 2);
}
other => panic!("expected Ambiguous (different kinds), got {other:?}"),
}
}
#[test]
fn lookup_not_found_provides_suggestions_for_typos() {
let index = build_test_index();
let result = lookup(&index, "Mutx", None);
match result {
QueryResult::NotFound { suggestions, .. } => {
assert!(
suggestions.iter().any(|s| s.contains("Mutex")),
"expected suggestion containing 'Mutex', got {suggestions:?}"
);
}
other => panic!("expected NotFound with suggestions, got {other:?}"),
}
}
#[test]
fn levenshtein_identical_strings() {
assert_eq!(levenshtein_distance("hello", "hello"), 0);
}
#[test]
fn levenshtein_empty_strings() {
assert_eq!(levenshtein_distance("", ""), 0);
assert_eq!(levenshtein_distance("abc", ""), 3);
assert_eq!(levenshtein_distance("", "abc"), 3);
}
#[test]
fn levenshtein_single_edit() {
assert_eq!(levenshtein_distance("kitten", "sitten"), 1); assert_eq!(levenshtein_distance("abc", "abcd"), 1); assert_eq!(levenshtein_distance("abcd", "abc"), 1); }
#[test]
fn levenshtein_multiple_edits() {
assert_eq!(levenshtein_distance("kitten", "sitting"), 3);
}
#[test]
fn could_match_rejects_large_length_difference() {
assert!(!could_match_within_distance("a", "abcde", 3));
}
#[test]
fn could_match_accepts_similar_length() {
assert!(could_match_within_distance("abc", "abd", 3));
}
#[test]
fn lookup_empty_query_returns_not_found() {
let index = build_test_index();
let result = lookup(&index, "", None);
assert!(matches!(result, QueryResult::NotFound { .. }));
}
#[test]
fn lookup_exact_path_takes_priority_over_suffix() {
let mut index = DocIndex::new("grox".to_string(), "0.1.0".to_string());
index.add_item(make_item("cli", "grox::cli", ItemKind::Module));
index.add_item(make_item("Cli", "grox::cli::Cli", ItemKind::Struct));
let result = lookup(&index, "grox::cli", None);
match result {
QueryResult::Found { index: idx } => {
assert_eq!(index.items[idx].path, "grox::cli");
assert_eq!(index.items[idx].kind, ItemKind::Module);
}
other => panic!("expected Found for exact path, got {other:?}"),
}
}
#[test]
fn auto_select_single_primary_at_root_wins() {
let mut index = DocIndex::new("mycrate".to_string(), "1.0.0".to_string());
index.add_item(make_item("Widget", "mycrate::Widget", ItemKind::Struct));
index.add_item(make_item(
"Widget",
"mycrate::inner::Widget",
ItemKind::Function,
));
let result = lookup(&index, "Widget", None);
match result {
QueryResult::Found { index: idx } => {
assert_eq!(index.items[idx].path, "mycrate::Widget");
assert_eq!(index.items[idx].kind, ItemKind::Struct);
}
other => panic!("expected Found via auto-selection, got {other:?}"),
}
}
#[test]
fn auto_select_blocked_by_3_level_primary() {
let mut index = DocIndex::new("mycrate".to_string(), "1.0.0".to_string());
index.add_item(make_item("Widget", "mycrate::Widget", ItemKind::Struct));
index.add_item(make_item("Widget", "mycrate::ui::Widget", ItemKind::Struct));
let result = lookup(&index, "Widget", None);
assert!(
matches!(result, QueryResult::Ambiguous { .. }),
"expected Ambiguous when 3-level primary exists, got {result:?}"
);
}
#[test]
fn dedup_removes_path_kind_duplicates() {
let mut index = DocIndex::new("mycrate".to_string(), "1.0.0".to_string());
index.add_item(make_item("Foo", "mycrate::Foo", ItemKind::Struct));
index.add_item(make_item("Foo", "mycrate::Foo", ItemKind::Struct));
index.add_item(make_item("Foo", "mycrate::Foo", ItemKind::Struct));
let result = lookup(&index, "mycrate::Foo", None);
match result {
QueryResult::Found { .. } => {} other => panic!("expected Found after dedup, got {other:?}"),
}
}
#[test]
fn reexport_stub_resolved_to_canonical() {
let mut index = DocIndex::new("mycrate".to_string(), "1.0.0".to_string());
let mut canonical = make_item("Widget", "mycrate::inner::Widget", ItemKind::Struct);
canonical.children.push(ChildRef {
index: 999, kind: ItemKind::Function,
name: "new".to_string(),
});
index.add_item(canonical);
let mut stub = make_item("Widget", "mycrate::Widget", ItemKind::Struct);
stub.signature = "pub use inner::Widget".to_string();
index.add_item(stub);
let result = lookup(&index, "Widget", None);
match result {
QueryResult::Found { index: idx } => {
assert_eq!(index.items[idx].path, "mycrate::inner::Widget");
}
other => panic!("expected Found (canonical), got {other:?}"),
}
}
#[test]
fn suggestions_returns_close_matches_for_typo() {
let index = build_test_index();
let suggestions = compute_suggestions(&index, "Mutx");
assert!(
suggestions.iter().any(|s| s.contains("Mutex")),
"expected Mutex in suggestions, got {suggestions:?}"
);
}
#[test]
fn suggestions_dedup_and_cap_at_5() {
let mut index = DocIndex::new("mycrate".to_string(), "1.0.0".to_string());
for i in 0..20 {
let name = format!("Foob{i}");
let path = format!("mycrate::{name}");
index.add_item(make_item(&name, &path, ItemKind::Struct));
}
let suggestions = compute_suggestions(&index, "Foob");
assert!(
suggestions.len() <= 5,
"suggestions should be capped at 5, got {}",
suggestions.len()
);
let unique: std::collections::HashSet<&String> = suggestions.iter().collect();
assert_eq!(
unique.len(),
suggestions.len(),
"suggestions should be deduplicated"
);
}
#[test]
fn method_lookup_finds_method_on_parent() {
let mut index = DocIndex::new("tokio".to_string(), "1.0.0".to_string());
let mut mutex = make_item("Mutex", "tokio::sync::Mutex", ItemKind::Struct);
mutex.children.push(ChildRef {
index: 1,
kind: ItemKind::Function,
name: "lock".to_string(),
});
index.add_item(mutex);
index.add_item(make_item(
"lock",
"tokio::sync::Mutex::lock",
ItemKind::Function,
));
let result = lookup_method(&index, &["sync", "Mutex"], "lock", None);
match result {
QueryResult::Found { index: idx } => {
assert_eq!(index.items[idx].name, "lock");
}
other => panic!("expected Found for method lookup, got {other:?}"),
}
}
#[test]
fn method_lookup_returns_not_found_with_suggestions_when_method_missing() {
let mut index = DocIndex::new("tokio".to_string(), "1.0.0".to_string());
let mut mutex = make_item("Mutex", "tokio::sync::Mutex", ItemKind::Struct);
mutex.children.push(ChildRef {
index: 1,
kind: ItemKind::Function,
name: "lock".to_string(),
});
index.add_item(mutex);
index.add_item(make_item(
"lock",
"tokio::sync::Mutex::lock",
ItemKind::Function,
));
let result = lookup_method(&index, &["sync", "Mutex"], "lokc", None);
match result {
QueryResult::NotFound { suggestions, .. } => {
assert!(
suggestions.iter().any(|s| s.contains("lock")),
"expected lock in suggestions, got {suggestions:?}"
);
}
other => panic!("expected NotFound with suggestions, got {other:?}"),
}
}
#[test]
fn method_lookup_bubbles_ambiguous_parent() {
let mut index = DocIndex::new("mycrate".to_string(), "1.0.0".to_string());
index.add_item(make_item(
"Builder",
"mycrate::http::Builder",
ItemKind::Struct,
));
index.add_item(make_item(
"Builder",
"mycrate::runtime::Builder",
ItemKind::Struct,
));
let result = lookup_method(&index, &["Builder"], "build", None);
assert!(
matches!(result, QueryResult::Ambiguous { .. }),
"expected Ambiguous for ambiguous parent, got {result:?}"
);
}
#[test]
fn looks_like_item_name_uppercase_returns_true() {
assert!(looks_like_item_name("Mutex"));
assert!(looks_like_item_name("HashMap"));
assert!(looks_like_item_name("Vec"));
assert!(looks_like_item_name("MAX_SIZE"));
}
#[test]
fn looks_like_item_name_lowercase_long_returns_false() {
assert!(!looks_like_item_name("serde"));
assert!(!looks_like_item_name("tokio"));
assert!(!looks_like_item_name("regex"));
assert!(!looks_like_item_name("reqwest"));
}
#[test]
fn looks_like_item_name_short_returns_true() {
assert!(looks_like_item_name("new"));
assert!(looks_like_item_name("len"));
assert!(looks_like_item_name("pop"));
assert!(looks_like_item_name("push"));
}
#[test]
fn looks_like_item_name_common_method_returns_true() {
assert!(looks_like_item_name("clone"));
assert!(looks_like_item_name("default"));
assert!(looks_like_item_name("parse"));
assert!(looks_like_item_name("format"));
}
#[test]
fn looks_like_item_name_hyphen_returns_false() {
assert!(!looks_like_item_name("my-crate"));
assert!(!looks_like_item_name("serde-json"));
}
#[test]
fn looks_like_item_name_empty_returns_false() {
assert!(!looks_like_item_name(""));
}
#[test]
fn looks_like_item_name_underscore_crate_returns_false() {
assert!(!looks_like_item_name("serde_json"));
assert!(!looks_like_item_name("tokio_util"));
}
#[test]
fn looks_like_item_name_complex_snake_case_returns_true() {
assert!(looks_like_item_name("my_longer_function_name"));
}
#[test]
fn dedup_by_identity_keeps_shallowest_reexport() {
let mut index = DocIndex::new("mycrate".to_string(), "1.0.0".to_string());
let mut canonical = make_item("Helper", "mycrate::inner::Helper", ItemKind::Struct);
canonical.children.push(ChildRef {
index: 999,
kind: ItemKind::Function,
name: "new".to_string(),
});
index.add_item(canonical);
let mut reexport = make_item("Helper", "mycrate::Helper", ItemKind::Struct);
reexport.reexport_source = Some("mycrate::inner::Helper".to_string());
index.add_item(reexport);
let result = lookup(&index, "Helper", None);
match result {
QueryResult::Found { index: idx } => {
assert_eq!(index.items[idx].path, "mycrate::Helper");
}
other => panic!("expected Found at shallow path, got {other:?}"),
}
}
#[test]
fn dedup_by_identity_handles_two_reexports_same_source() {
let mut index = DocIndex::new("mycrate".to_string(), "1.0.0".to_string());
let canonical = make_item("Foo", "mycrate::a::b::Foo", ItemKind::Struct);
index.add_item(canonical);
let mut re1 = make_item("Foo", "mycrate::a::Foo", ItemKind::Struct);
re1.reexport_source = Some("mycrate::a::b::Foo".to_string());
index.add_item(re1);
let mut re2 = make_item("Foo", "mycrate::Foo", ItemKind::Struct);
re2.reexport_source = Some("mycrate::a::b::Foo".to_string());
index.add_item(re2);
let result = lookup(&index, "Foo", None);
match result {
QueryResult::Found { index: idx } => {
assert_eq!(
index.items[idx].path, "mycrate::Foo",
"should keep shallowest"
);
}
other => panic!("expected Found, got {other:?}"),
}
}
#[test]
fn dedup_by_identity_passes_through_unrelated() {
let mut index = DocIndex::new("mycrate".to_string(), "1.0.0".to_string());
index.add_item(make_item("Error", "mycrate::de::Error", ItemKind::Trait));
index.add_item(make_item("Error", "mycrate::ser::Error", ItemKind::Trait));
let result = lookup(&index, "Error", None);
assert!(
matches!(result, QueryResult::Ambiguous { .. }),
"different items should remain Ambiguous, got {result:?}"
);
}
}