use crate::cli::{CrateSpec, FeatureFlags, QueryPath};
use crate::types::{DocIndex, ItemKind, QueryResult};
pub(crate) fn parse_reexport_source(item: &crate::types::IndexItem) -> Option<String> {
if let Some(ref source) = item.reexport_source {
return Some(source.clone());
}
let rest = item.signature.strip_prefix("pub use ")?;
let source = if let Some(pos) = rest.find(" as ") {
&rest[..pos]
} else {
rest.trim_end_matches(';').trim()
};
if source.is_empty() {
return None;
}
Some(source.to_string())
}
pub(crate) fn try_follow_reexport(
stub: &crate::types::IndexItem,
ctx: Option<&crate::resolve::ProjectContext>,
features: &FeatureFlags,
feature_suffix: &str,
private: bool,
) -> Option<(DocIndex, usize)> {
let source_path = parse_reexport_source(stub)?;
let (crate_name, item_path) = source_path.split_once("::")?;
let query_path = QueryPath {
crate_spec: CrateSpec::Named(crate_name.to_string()),
item_segments: Vec::new(),
};
let (source, _) = crate::resolve_crate_source(ctx, query_path).ok()?;
let (source_index, _source) =
crate::load_or_build_index(source, features, feature_suffix, private, false, ctx).ok()?;
let source_query = QueryPath {
crate_spec: CrateSpec::CurrentCrate,
item_segments: item_path.split("::").map(String::from).collect(),
};
let result = crate::resolve_item(&source_query, &source_index, None);
match result {
QueryResult::Found { index: idx } => Some((source_index, idx)),
QueryResult::Ambiguous { .. } | QueryResult::NotFound { .. } => {
follow_by_name(&source_index, item_path).map(|idx| (source_index, idx))
}
}
}
pub(crate) fn try_resolve_via_prefix_reexport(
query: &QueryPath,
index: &DocIndex,
ctx: Option<&crate::resolve::ProjectContext>,
features: &FeatureFlags,
feature_suffix: &str,
private: bool,
) -> Option<(DocIndex, usize)> {
if query.item_segments.len() < 2 {
return None;
}
let crate_name = &index.crate_name;
for prefix_len in (1..query.item_segments.len()).rev() {
let prefix_segments = &query.item_segments[..prefix_len];
let remaining = &query.item_segments[prefix_len..];
let with_crate = format!(
"{}::{}",
crate_name,
prefix_segments
.iter()
.map(String::as_str)
.collect::<Vec<_>>()
.join("::")
);
let without_crate = prefix_segments
.iter()
.map(String::as_str)
.collect::<Vec<_>>()
.join("::");
let candidates = index.lookup_by_path(&with_crate);
let candidates = if candidates.is_empty() {
index.lookup_by_path(&without_crate)
} else {
candidates
};
let Some(&stub_idx) = candidates
.iter()
.find(|&&i| index.items[i].reexport_source.is_some())
else {
continue;
};
let stub = &index.items[stub_idx];
if stub.reexport_source.is_none() {
continue;
}
let source_path = stub.reexport_source.as_ref()?;
let (source_crate, source_item_path) = source_path.split_once("::")?;
let crate_query = QueryPath {
crate_spec: CrateSpec::Named(source_crate.to_string()),
item_segments: Vec::new(),
};
let (source, _) = crate::resolve_crate_source(ctx, crate_query).ok()?;
let (source_index, _) =
crate::load_or_build_index(source, features, feature_suffix, private, false, ctx)
.ok()?;
let mut full_segments: Vec<String> =
source_item_path.split("::").map(String::from).collect();
full_segments.extend(remaining.iter().cloned());
let inner_query = QueryPath {
crate_spec: CrateSpec::CurrentCrate,
item_segments: full_segments,
};
let result = crate::resolve_item(&inner_query, &source_index, None);
if let QueryResult::Found { index: idx } = result {
return Some((source_index, idx));
}
if let Some(idx) = follow_by_name(&source_index, &remaining.join("::")) {
return Some((source_index, idx));
}
}
None
}
pub(crate) fn try_resolve_via_glob_reexport(
query: &QueryPath,
index: &DocIndex,
ctx: Option<&crate::resolve::ProjectContext>,
features: &FeatureFlags,
feature_suffix: &str,
private: bool,
) -> Option<(DocIndex, usize)> {
if query.item_segments.is_empty() || index.glob_uses.is_empty() {
return None;
}
let crate_name = &index.crate_name;
let mut parent_segments: Vec<String> = vec![crate_name.clone()];
parent_segments.extend(
query.item_segments[..query.item_segments.len() - 1]
.iter()
.cloned(),
);
let item_name = query.item_segments.last()?;
while !parent_segments.is_empty() {
let parent_path = parent_segments.join("::");
let candidates: Vec<&crate::types::GlobUse> = index
.glob_uses
.iter()
.filter(|g| {
g.parent_path == parent_path
|| (parent_segments.len() == 1 && g.parent_path.is_empty())
})
.collect();
for glob in candidates {
let source = glob.source_path.trim_start_matches("::");
let (source_crate, source_item_path) = source
.split_once("::")
.map_or((source, ""), |(c, p)| (c, p));
if source_crate.is_empty() {
continue;
}
let crate_query = QueryPath {
crate_spec: CrateSpec::Named(source_crate.to_string()),
item_segments: Vec::new(),
};
let Ok((src, _)) = crate::resolve_crate_source(ctx, crate_query) else {
continue;
};
let Ok((source_index, _)) =
crate::load_or_build_index(src, features, feature_suffix, private, false, ctx)
else {
continue;
};
let mut inner_segments: Vec<String> = if source_item_path.is_empty() {
Vec::new()
} else {
source_item_path.split("::").map(String::from).collect()
};
let suffix_start = parent_segments.len() - 1; inner_segments.extend(query.item_segments[suffix_start..].iter().cloned());
let inner_query = QueryPath {
crate_spec: CrateSpec::CurrentCrate,
item_segments: inner_segments.clone(),
};
let result = crate::resolve_item(&inner_query, &source_index, None);
if let QueryResult::Found { index: idx } = result {
return Some((source_index, idx));
}
if let Some(idx) = follow_by_name(&source_index, item_name) {
return Some((source_index, idx));
}
}
parent_segments.pop();
}
None
}
fn follow_by_name(source_index: &DocIndex, item_path: &str) -> Option<usize> {
let item_name = item_path.rsplit("::").next()?;
let candidates = source_index.lookup_by_name(&item_name.to_lowercase());
if candidates.is_empty() {
return None;
}
let exact_non_stub = candidates.iter().copied().find(|&idx| {
let item = &source_index.items[idx];
item.name == item_name && !crate::query::is_reexport_stub(item)
});
if exact_non_stub.is_some() {
return exact_non_stub;
}
candidates.iter().copied().find(|&idx| {
let item = &source_index.items[idx];
item.name == item_name
})
}
pub(crate) fn try_resolve_reexport_on_not_found(
query: &QueryPath,
index: &DocIndex,
kind_filter: Option<ItemKind>,
) -> Option<QueryResult> {
let item_name = query.item_segments.last()?;
let name_lower = item_name.to_lowercase();
let name_indices = index.lookup_by_name(&name_lower);
if name_indices.is_empty() {
return None;
}
let case_strict = item_name.chars().any(char::is_uppercase);
let name_matches = |item: &crate::types::IndexItem| -> bool {
if case_strict {
item.name == *item_name
} else {
item.name.eq_ignore_ascii_case(item_name)
}
};
let mut reexport_matches: Vec<usize> = name_indices
.iter()
.copied()
.filter(|&idx| {
let item = &index.items[idx];
item.reexport_source.is_some()
&& name_matches(item)
&& kind_filter.is_none_or(|k| item.kind.matches_filter(k))
})
.collect();
if reexport_matches.is_empty() {
reexport_matches = name_indices
.iter()
.copied()
.filter(|&idx| {
let item = &index.items[idx];
name_matches(item) && kind_filter.is_none_or(|k| item.kind.matches_filter(k))
})
.collect();
}
match reexport_matches.len() {
0 => None,
1 => Some(QueryResult::Found {
index: reexport_matches[0],
}),
_ => Some(QueryResult::Ambiguous {
indices: reexport_matches,
query: query.item_segments.join("::"),
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::{make_item, make_reexport_stub};
use crate::types::{GlobUse, ItemKind};
fn query_path(crate_name: &str, segments: &[&str]) -> QueryPath {
QueryPath {
crate_spec: CrateSpec::Named(crate_name.to_string()),
item_segments: segments.iter().map(|s| (*s).to_string()).collect(),
}
}
#[test]
fn case_strict_match_does_not_treat_uppercase_vec_as_lowercase_vec() {
let mut index = DocIndex::new("std".to_string(), "1.0.0".to_string());
index.add_item(make_reexport_stub(
"vec",
"std::vec",
ItemKind::Module,
"alloc::vec",
));
index.add_item(make_reexport_stub(
"vec",
"std::vec",
ItemKind::Macro,
"alloc::vec",
));
let q = query_path("std", &["vec", "Vec"]);
let result = try_resolve_reexport_on_not_found(&q, &index, None);
assert!(
result.is_none(),
"case-strict 'Vec' must not match items named 'vec', got {result:?}"
);
}
#[test]
fn case_insensitive_match_when_query_is_all_lowercase() {
let mut index = DocIndex::new("std".to_string(), "1.0.0".to_string());
index.add_item(make_reexport_stub(
"vec",
"std::vec",
ItemKind::Module,
"alloc::vec",
));
index.add_item(make_reexport_stub(
"vec",
"std::vec",
ItemKind::Macro,
"alloc::vec",
));
let q = query_path("std", &["vec"]);
let result = try_resolve_reexport_on_not_found(&q, &index, None);
assert!(
matches!(result, Some(QueryResult::Ambiguous { ref indices, .. }) if indices.len() == 2),
"lowercase 'vec' should match both mod and macro stubs, got {result:?}"
);
}
#[test]
fn exact_case_match_returns_found_singleton() {
let mut index = DocIndex::new("mycrate".to_string(), "0.1.0".to_string());
index.add_item(make_reexport_stub(
"Foo",
"mycrate::Foo",
ItemKind::Struct,
"inner::Foo",
));
let q = query_path("mycrate", &["Foo"]);
let result = try_resolve_reexport_on_not_found(&q, &index, None);
assert!(
matches!(result, Some(QueryResult::Found { .. })),
"exact-case 'Foo' should resolve to Found, got {result:?}"
);
}
#[test]
fn no_matches_returns_none() {
let mut index = DocIndex::new("mycrate".to_string(), "0.1.0".to_string());
index.add_item(make_item(
"real_thing",
"mycrate::real_thing",
ItemKind::Struct,
));
let q = query_path("mycrate", &["nonexistent"]);
let result = try_resolve_reexport_on_not_found(&q, &index, None);
assert!(result.is_none());
}
#[test]
fn follow_by_name_prefers_non_stub_over_stub() {
let mut index = DocIndex::new("inner".to_string(), "0.1.0".to_string());
index.add_item(make_reexport_stub(
"Foo",
"inner::a::Foo",
ItemKind::Struct,
"deeper::Foo",
));
index.add_item(make_item("Foo", "inner::b::Foo", ItemKind::Struct));
let idx = follow_by_name(&index, "Foo").expect("name match should be found");
assert_eq!(idx, 1, "should prefer the non-stub at index 1, got {idx}");
}
#[test]
fn follow_by_name_returns_stub_when_only_stub_exists() {
let mut index = DocIndex::new("inner".to_string(), "0.1.0".to_string());
index.add_item(make_reexport_stub(
"Foo",
"inner::a::Foo",
ItemKind::Struct,
"deeper::Foo",
));
let idx = follow_by_name(&index, "Foo").expect("stub fallback should match");
assert_eq!(idx, 0);
}
#[test]
fn follow_by_name_unknown_returns_none() {
let mut index = DocIndex::new("inner".to_string(), "0.1.0".to_string());
index.add_item(make_item("Bar", "inner::Bar", ItemKind::Struct));
assert!(follow_by_name(&index, "Foo").is_none());
}
#[test]
fn follow_by_name_handles_path_input_taking_last_segment() {
let mut index = DocIndex::new("inner".to_string(), "0.1.0".to_string());
index.add_item(make_item("Foo", "inner::deep::Foo", ItemKind::Struct));
let idx = follow_by_name(&index, "anything::Foo").expect("trailing 'Foo' should match");
assert_eq!(idx, 0);
}
#[test]
fn prefix_descent_returns_none_for_single_segment_query() {
let features = FeatureFlags {
all_features: false,
no_default_features: false,
features: Vec::new(),
};
let mut index = DocIndex::new("std".to_string(), "1.0.0".to_string());
index.add_item(make_reexport_stub(
"vec",
"std::vec",
ItemKind::Module,
"alloc::vec",
));
let q = query_path("std", &["vec"]);
let result = try_resolve_via_prefix_reexport(&q, &index, None, &features, "", false);
assert!(
result.is_none(),
"1-segment query has no prefix to descend through"
);
}
#[test]
fn prefix_descent_returns_none_when_no_prefix_is_a_stub() {
let features = FeatureFlags {
all_features: false,
no_default_features: false,
features: Vec::new(),
};
let index = DocIndex::new("mycrate".to_string(), "0.1.0".to_string());
let q = query_path("mycrate", &["a", "b", "c"]);
let result = try_resolve_via_prefix_reexport(&q, &index, None, &features, "", false);
assert!(result.is_none());
}
#[test]
fn glob_descent_returns_none_when_no_glob_uses() {
let features = FeatureFlags {
all_features: false,
no_default_features: false,
features: Vec::new(),
};
let index = DocIndex::new("clap".to_string(), "4.5.0".to_string());
let q = query_path("clap", &["Arg"]);
let result = try_resolve_via_glob_reexport(&q, &index, None, &features, "", false);
assert!(result.is_none());
}
#[test]
fn glob_descent_returns_none_for_empty_segments() {
let features = FeatureFlags {
all_features: false,
no_default_features: false,
features: Vec::new(),
};
let mut index = DocIndex::new("clap".to_string(), "4.5.0".to_string());
index.glob_uses.push(GlobUse {
parent_path: "clap".to_string(),
source_path: "clap_builder".to_string(),
});
let q = query_path("clap", &[]);
let result = try_resolve_via_glob_reexport(&q, &index, None, &features, "", false);
assert!(
result.is_none(),
"no item segments — nothing to look up via glob"
);
}
}