use crate::doc_ref::ParentRef;
use crate::iterators::{LazyChild, LazyChildren};
use crate::string_utils::case_aware_jaro_winkler;
use crate::{DocRef, Navigator, RustdocData};
use fieldwork::Fieldwork;
use rustdoc_types::{Id, Item, ItemEnum, ItemKind, Use};
use semver::VersionReq;
#[derive(Fieldwork)]
#[fieldwork(get)]
pub struct Suggestion<'a> {
path: String,
item: Option<DocRef<'a, Item>>,
score: f64,
}
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
pub(crate) struct ItemKey {
crate_docs: usize,
item: usize,
}
impl<'a, T> From<DocRef<'a, T>> for ItemKey {
fn from(d: DocRef<'a, T>) -> Self {
Self {
crate_docs: d.crate_docs() as *const RustdocData as usize,
item: d.item() as *const T as usize,
}
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
struct Frame {
item: ItemKey,
segment: Option<String>,
}
pub struct Resolver<'a> {
navigator: &'a Navigator,
stack: Vec<Frame>,
}
impl<'a> Resolver<'a> {
pub fn new(navigator: &'a Navigator) -> Self {
Self {
navigator,
stack: Vec::new(),
}
}
pub fn navigator(&self) -> &'a Navigator {
self.navigator
}
pub fn current_path(&self) -> impl Iterator<Item = &str> + '_ {
self.stack.iter().filter_map(|f| f.segment.as_deref())
}
fn with_pushed<R>(
&mut self,
item: ItemKey,
segment: Option<&str>,
f: impl FnOnce(&mut Self) -> R,
) -> Option<R> {
let frame = Frame {
item,
segment: segment.map(str::to_owned),
};
if self.stack.contains(&frame) {
log::trace!("cycle: {frame:?}");
return None;
}
self.stack.push(frame);
let r = f(self);
self.stack.pop();
Some(r)
}
}
impl<'a> Resolver<'a> {
pub fn resolve_path(
&mut self,
mut path: &str,
suggestions: &mut Vec<Suggestion<'a>>,
) -> Option<DocRef<'a, Item>> {
if let Some(p) = path.strip_prefix("::") {
path = p;
}
let (crate_specifier, path_start_index) = if let Some(first_scope) = path.find("::") {
(&path[..first_scope], Some(first_scope + 2))
} else {
(path, None)
};
let (crate_name, version_req) = if let Some(at) = crate_specifier.find("@") {
(
&crate_specifier[..at],
VersionReq::parse(&crate_specifier[at + 1..]).unwrap_or(VersionReq::STAR),
)
} else {
(crate_specifier, VersionReq::STAR)
};
let Some(crate_data) = self.navigator.load_crate(crate_name, &version_req) else {
suggestions.extend(self.navigator.list_available_crates().map(|crate_info| {
Suggestion {
path: crate_info.name().to_string(),
item: None,
score: case_aware_jaro_winkler(crate_info.name(), crate_name),
}
}));
return None;
};
let item = crate_data.get(self.navigator, &crate_data.root)?;
let Some(path_start_index) = path_start_index else {
return Some(item);
};
if let Some(item) = self.find_children_recursive(item, path, path_start_index, suggestions)
{
return Some(item);
}
let suffix = &path[path_start_index..];
if let Some(item) = crate_data
.path_to_id
.get(suffix)
.and_then(|id| crate_data.index.get(id))
.map(|item| DocRef::new(self.navigator, crate_data, item))
{
return Some(item);
}
if let Some(sep) = suffix.rfind("::") {
let parent_suffix = &suffix[..sep];
let child_start = path_start_index + sep + 2;
if let Some(parent_id) = crate_data.path_to_id.get(parent_suffix)
&& let Some(parent_item) = crate_data.index.get(parent_id)
{
let parent_ref = DocRef::new(self.navigator, crate_data, parent_item);
return self.find_children_recursive(parent_ref, path, child_start, suggestions);
}
}
None
}
fn find_children_recursive(
&mut self,
item: DocRef<'a, Item>,
path: &str,
index: usize,
suggestions: &mut Vec<Suggestion<'a>>,
) -> Option<DocRef<'a, Item>> {
let remaining = &path[path.len().min(index)..];
if remaining.is_empty() {
return Some(item);
}
let segment_end = remaining
.find("::")
.map(|x| index + x)
.unwrap_or(path.len());
let segment = &path[index..segment_end];
let next_segment_start = path.len().min(segment_end + 2);
let (kind_filter, segment_name) = parse_discriminated_segment(segment);
log::trace!(
"🔎 searching for {segment_name} (kind={kind_filter:?}) in {} ({:?}) (remaining {})",
&path[..index],
item.kind(),
&path[next_segment_start..]
);
if let Some(found) = self.find_named(item, segment_name, |resolver, candidate| {
if !kind_filter.is_none_or(|k| candidate.kind() == k) {
return None;
}
resolver.find_children_recursive(candidate, path, next_segment_start, suggestions)
}) {
return Some(found);
}
suggestions.extend(self.generate_suggestions(item, path, index));
None
}
fn generate_suggestions(
&mut self,
item: DocRef<'a, Item>,
path: &str,
index: usize,
) -> Vec<Suggestion<'a>> {
let prefix = path[..index].to_owned();
let path_owned = path.to_owned();
let mut candidates = self.children(item);
if let ItemEnum::Trait(trait_) = item.inner() {
candidates.extend(trait_.items.iter().filter_map(|id| item.get(id)));
}
candidates
.into_iter()
.filter_map(move |item| {
item.name().and_then(|name| {
let full_path = format!("{prefix}{name}");
if path_owned.starts_with(&full_path) {
None
} else {
let score = case_aware_jaro_winkler(&path_owned, &full_path);
Some(Suggestion {
path: full_path,
score,
item: Some(item),
})
}
})
})
.collect()
}
}
impl<'a> Resolver<'a> {
pub fn find_child(
&mut self,
parent: DocRef<'a, Item>,
target: &str,
) -> Option<DocRef<'a, Item>> {
self.find_named(parent, target, |_, candidate| Some(candidate))
}
pub fn find_by_path<'s>(
&mut self,
parent: DocRef<'a, Item>,
segments: impl IntoIterator<Item = &'s str>,
) -> Option<DocRef<'a, Item>> {
let mut current = parent;
for segment in segments {
current = self.find_child(current, segment)?;
}
Some(current)
}
fn find_named<F>(
&mut self,
parent: DocRef<'a, Item>,
target: &str,
mut accept: F,
) -> Option<DocRef<'a, Item>>
where
F: FnMut(&mut Self, DocRef<'a, Item>) -> Option<DocRef<'a, Item>>,
{
self.find_named_dyn(parent, target, &mut accept)
}
#[allow(clippy::type_complexity, reason = "it's fine")]
fn find_named_dyn(
&mut self,
parent: DocRef<'a, Item>,
target: &str,
accept: &mut dyn FnMut(&mut Self, DocRef<'a, Item>) -> Option<DocRef<'a, Item>>,
) -> Option<DocRef<'a, Item>> {
self.with_pushed(parent.into(), Some(target), |resolver| {
if matches!(
parent.inner(),
ItemEnum::Struct(_) | ItemEnum::Enum(_) | ItemEnum::Union(_) | ItemEnum::Trait(_)
) {
for method in parent.methods() {
if method.name() == Some(target)
&& let Some(found) = accept(resolver, method)
{
return Some(found);
}
}
}
if let ItemEnum::Trait(trait_) = parent.inner() {
for assoc in trait_.items.iter().filter_map(|id| parent.get(id)) {
if assoc.name() == Some(target)
&& let Some(found) = accept(resolver, assoc.with_parent(parent))
{
return Some(found);
}
}
}
enum Phase1<'a> {
Direct(DocRef<'a, Item>),
NonGlob(DocRef<'a, Use>),
}
let mut phase1: Vec<Phase1<'a>> = Vec::new();
let mut globs = Vec::<DocRef<'a, Use>>::new();
for child in LazyChildren::new(parent) {
match child {
LazyChild::Item(item) => {
if item.name() == Some(target) {
phase1.push(Phase1::Direct(item));
}
}
LazyChild::NonGlob { use_item, .. } => {
if use_item.item().name == target {
phase1.push(Phase1::NonGlob(use_item));
}
}
LazyChild::Glob { use_item, .. } => {
globs.push(use_item);
}
}
}
for cand in phase1 {
let resolved = match cand {
Phase1::Direct(item) => item,
Phase1::NonGlob(use_item) => {
let imported_name: &'a str = &use_item.item().name;
let Some(target_item) = resolver.follow_use(use_item, parent) else {
continue;
};
target_item.with_name(imported_name)
}
};
if let Some(found) = accept(resolver, resolved) {
return Some(found);
}
}
for glob_use in globs {
if let Some(source_module) = resolver.follow_use(glob_use, parent)
&& let Some(found) = resolver.find_named_dyn(source_module, target, accept)
{
return Some(found);
}
}
None
})
.flatten()
}
}
impl<'a> Resolver<'a> {
pub fn follow_use(
&mut self,
use_item: DocRef<'a, Use>,
parent_module: DocRef<'a, Item>,
) -> Option<DocRef<'a, Item>> {
let use_inner = use_item.item();
if let Some(id) = use_inner.id
&& let Some(target) = use_item.crate_docs().get(self.navigator, &id)
{
return Some(target);
}
self.with_pushed(use_item.into(), None, |resolver| {
let module_path = parent_module.containing_module_path();
let rewritten = rewrite_relative_prefix(&module_path, &use_inner.source)?;
resolver.resolve_path(&rewritten, &mut vec![])
})
.flatten()
}
pub fn resolve_lazy_child(&mut self, child: LazyChild<'a>) -> Option<DocRef<'a, Item>> {
match child {
LazyChild::Item(item) => Some(item),
LazyChild::NonGlob { use_item, parent } => {
let name: &'a str = &use_item.item().name;
self.follow_use(use_item, parent).map(|i| i.with_name(name))
}
LazyChild::Glob { use_item, parent } => self.follow_use(use_item, parent),
}
}
}
impl<'a> Resolver<'a> {
pub fn children(&mut self, item: DocRef<'a, Item>) -> Vec<DocRef<'a, Item>> {
let mut out = Vec::new();
self.collect_children(item, false, ParentRef::from(item), &mut out);
out
}
pub fn children_including_uses(&mut self, item: DocRef<'a, Item>) -> Vec<DocRef<'a, Item>> {
let mut out = Vec::new();
self.collect_children(item, true, ParentRef::from(item), &mut out);
out
}
pub fn ids(&mut self, parent: DocRef<'a, Item>, ids: &'a [Id]) -> Vec<DocRef<'a, Item>> {
let mut out = Vec::new();
self.collect_ids(parent, ids, false, Some(ParentRef::from(parent)), &mut out);
out
}
pub fn ids_including_uses(
&mut self,
parent: DocRef<'a, Item>,
ids: &'a [Id],
) -> Vec<DocRef<'a, Item>> {
let mut out = Vec::new();
self.collect_ids(parent, ids, true, Some(ParentRef::from(parent)), &mut out);
out
}
fn collect_children(
&mut self,
item: DocRef<'a, Item>,
include_uses: bool,
parent_ref: ParentRef<'a>,
out: &mut Vec<DocRef<'a, Item>>,
) {
match item.inner() {
ItemEnum::Module(module) => {
self.collect_ids(item, &module.items, include_uses, Some(parent_ref), out);
}
ItemEnum::Enum(enum_item) => {
self.collect_ids(
item,
&enum_item.variants,
include_uses,
Some(parent_ref),
out,
);
out.extend(item.methods());
}
ItemEnum::Struct(_) | ItemEnum::Union(_) => {
out.extend(item.methods());
}
ItemEnum::Use(use_item) => {
let use_ref = item.build_ref(use_item);
self.collect_use_children(use_ref, item, include_uses, out);
}
_ => {}
}
}
fn collect_ids(
&mut self,
parent: DocRef<'a, Item>,
ids: &'a [Id],
include_uses: bool,
parent_ref: Option<ParentRef<'a>>,
out: &mut Vec<DocRef<'a, Item>>,
) {
for id in ids {
let Some(item) = parent.get(id) else { continue };
if let ItemEnum::Use(use_item) = item.inner() {
if include_uses {
out.push(item);
continue;
}
let use_ref = item.build_ref(use_item);
self.collect_use_children(use_ref, parent, include_uses, out);
} else {
let item = match parent_ref {
Some(p) => item.with_parent(p),
None => item,
};
out.push(item);
}
}
}
fn collect_use_children(
&mut self,
use_ref: DocRef<'a, Use>,
parent: DocRef<'a, Item>,
include_uses: bool,
out: &mut Vec<DocRef<'a, Item>>,
) {
let Some(source_item) = self.follow_use(use_ref, parent) else {
return;
};
let use_item = use_ref.item();
if use_item.is_glob {
match source_item.inner() {
ItemEnum::Module(module) => {
self.collect_ids(source_item, &module.items, include_uses, None, out);
}
ItemEnum::Enum(enum_item) => {
self.collect_ids(source_item, &enum_item.variants, include_uses, None, out);
}
_ => {}
}
} else {
out.push(source_item.with_name(&use_item.name));
}
}
}
impl<'a> Resolver<'a> {
pub fn get_path(&mut self, origin: DocRef<'a, Item>, id: Id) -> Option<DocRef<'a, Item>> {
let item_summary = origin.crate_docs().paths.get(&id)?;
let crate_ = origin
.crate_docs()
.traverse_to_crate_by_id(self.navigator, item_summary.crate_id)?;
let root = crate_.root_item(self.navigator);
self.find_by_path(root, item_summary.path.iter().skip(1).map(String::as_str))
}
pub fn get_item_from_id_path(
&mut self,
crate_name: &str,
ids: &[u32],
) -> Option<(DocRef<'a, Item>, Vec<&'a str>)> {
let mut path = vec![];
let crate_docs = self.navigator.load_crate(crate_name, &VersionReq::STAR)?;
let mut item = crate_docs.get(self.navigator, &crate_docs.root)?;
path.push(item.crate_docs().name());
for id in ids {
item = item.get(&Id(*id))?;
if let ItemEnum::Use(use_item) = item.inner() {
let use_ref = item.build_ref(use_item);
let parent = item;
item = self
.follow_use(use_ref, parent)
.or_else(|| self.resolve_path(&use_item.source, &mut vec![]))?;
if !use_item.is_glob {
item.set_name(&use_item.name);
}
} else if let Some(name) = item.name() {
path.push(name);
}
}
Some((item, path))
}
}
fn parse_discriminated_segment(segment: &str) -> (Option<ItemKind>, &str) {
let Some(at) = segment.find('@') else {
return (None, segment);
};
let (disc, name) = (&segment[..at], &segment[at + 1..]);
match disc {
"mod" | "module" => (Some(ItemKind::Module), name),
"struct" => (Some(ItemKind::Struct), name),
"enum" => (Some(ItemKind::Enum), name),
"union" => (Some(ItemKind::Union), name),
"trait" => (Some(ItemKind::Trait), name),
"traitalias" => (Some(ItemKind::TraitAlias), name),
"fn" | "function" | "method" => (Some(ItemKind::Function), name),
"tyalias" | "typealias" => (Some(ItemKind::TypeAlias), name),
"type" => (Some(ItemKind::AssocType), name),
"const" | "constant" => (Some(ItemKind::Constant), name),
"static" => (Some(ItemKind::Static), name),
"macro" => (Some(ItemKind::Macro), name),
"attr" => (Some(ItemKind::ProcAttribute), name),
"derive" => (Some(ItemKind::ProcDerive), name),
"prim" | "primitive" => (Some(ItemKind::Primitive), name),
"field" => (Some(ItemKind::StructField), name),
"variant" => (Some(ItemKind::Variant), name),
"value" => (None, name),
_ => (None, segment),
}
}
fn rewrite_relative_prefix(module_path: &[&str], source: &str) -> Option<String> {
let mut supers = 0;
let mut rest = source;
while let Some(tail) = rest.strip_prefix("super::") {
supers += 1;
rest = tail;
}
if supers > 0 || rest == "super" {
let supers = if rest == "super" { supers + 1 } else { supers };
let tail = if rest == "super" { "" } else { rest };
let remaining = module_path.len().checked_sub(supers)?;
if remaining == 0 {
return None;
}
let prefix = module_path[..remaining].join("::");
return Some(if tail.is_empty() {
prefix
} else {
format!("{prefix}::{tail}")
});
}
if let Some(tail) = source.strip_prefix("self::") {
return Some(format!("{}::{}", module_path.join("::"), tail));
}
if source == "self" {
return Some(module_path.join("::"));
}
if let Some(tail) = source.strip_prefix("crate::") {
let crate_name = module_path.first().copied()?;
return Some(format!("{crate_name}::{tail}"));
}
if source == "crate" {
return module_path.first().copied().map(String::from);
}
Some(source.to_owned())
}