#![allow(clippy::vec_box)]
use std::collections::HashSet;
use std::path::Path;
use rustdoc_types::{Id, Item, ItemEnum, MacroKind, Visibility};
use crate::model::{CrateModel, ReachableInfo, compute_reachable_set};
use crate::rustdoc_json::{self, LockfilePackages};
pub struct AccessibleEntry {
pub accessible_path: String,
pub crate_idx: usize,
pub item_id: Id,
pub item_kind: AccessibleItemKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AccessibleItemKind {
Module,
Function,
Struct,
Enum,
Trait,
Union,
TypeAlias,
Constant,
Static,
Macro,
ProcMacro,
ProcAttrMacro,
ProcDeriveMacro,
}
impl AccessibleItemKind {
fn from_item(item: &Item) -> Option<Self> {
match &item.inner {
ItemEnum::Module(_) => Some(Self::Module),
ItemEnum::Function(_) => Some(Self::Function),
ItemEnum::Struct(_) => Some(Self::Struct),
ItemEnum::Enum(_) => Some(Self::Enum),
ItemEnum::Trait(_) => Some(Self::Trait),
ItemEnum::Union(_) => Some(Self::Union),
ItemEnum::TypeAlias(_) => Some(Self::TypeAlias),
ItemEnum::Constant { .. } => Some(Self::Constant),
ItemEnum::Static(_) => Some(Self::Static),
ItemEnum::Macro(_) => Some(Self::Macro),
ItemEnum::ProcMacro(pm) => Some(match pm.kind {
MacroKind::Bang => Self::ProcMacro,
MacroKind::Attr => Self::ProcAttrMacro,
MacroKind::Derive => Self::ProcDeriveMacro,
}),
_ => None,
}
}
}
pub struct CrossCrateIndex {
#[allow(clippy::vec_box)]
pub source_models: Vec<Box<(CrateModel, ReachableInfo)>>,
pub items: Vec<AccessibleEntry>,
}
pub struct CrossCrateResolution {
pub model: CrateModel,
pub inner_module_path: Option<String>,
}
pub fn resolve_cross_crate_module(
primary_model: &CrateModel,
module_path: &str,
toolchain: &str,
manifest_path: Option<&str>,
target_dir: &Path,
verbose: bool,
) -> Option<CrossCrateResolution> {
let (first_segment, rest) = match module_path.split_once("::") {
Some((first, rest)) => (first, Some(rest.to_string())),
None => (module_path, None),
};
let crate_name = primary_model.crate_name();
let root = primary_model.root_module()?;
let children = primary_model.module_children(root);
for (_id, child) in &children {
let ItemEnum::Use(use_item) = &child.inner else {
continue;
};
if use_item.is_glob {
continue;
}
if !matches!(child.visibility, Visibility::Public) {
continue;
}
if is_intra_crate_source(&use_item.source, crate_name) {
continue;
}
let name = child.name.as_deref().unwrap_or(&use_item.name);
if name != first_segment {
continue;
}
if let Some(resolution) = follow_use_chain(
&use_item.source,
first_segment,
rest.clone(),
toolchain,
manifest_path,
target_dir,
verbose,
) {
return Some(resolution);
}
}
for (_id, child) in &children {
let ItemEnum::Use(use_item) = &child.inner else {
continue;
};
if !use_item.is_glob {
continue;
}
if !matches!(child.visibility, Visibility::Public) {
continue;
}
if is_intra_crate_source(&use_item.source, crate_name) {
continue;
}
let source_crate = extract_crate_name(&use_item.source);
let Ok(json_path) = rustdoc_json::generate_rustdoc_json(
&source_crate,
toolchain,
manifest_path,
true,
target_dir,
verbose,
true, ) else {
continue;
};
let Ok(krate) = rustdoc_json::parse_rustdoc_json_cached(&json_path) else {
continue;
};
let source_model = CrateModel::from_crate(krate);
if source_model.find_module(first_segment).is_some() {
return Some(CrossCrateResolution {
model: source_model,
inner_module_path: if let Some(r) = &rest {
Some(format!("{first_segment}::{r}"))
} else {
Some(first_segment.to_string())
},
});
}
if let Some(root) = source_model.root_module() {
for (_sid, schild) in source_model.module_children(root) {
let ItemEnum::Use(su) = &schild.inner else {
continue;
};
if su.is_glob {
continue;
}
let sname = schild.name.as_deref().unwrap_or(&su.name);
if sname != first_segment {
continue;
}
if let Some(resolution) = follow_use_chain(
&su.source,
first_segment,
rest.clone(),
toolchain,
manifest_path,
target_dir,
verbose,
) {
return Some(resolution);
}
}
}
}
None
}
pub fn root_has_cross_crate_reexports(model: &CrateModel) -> bool {
let crate_name = model.crate_name();
let Some(root) = model.root_module() else {
return false;
};
let children = model.module_children(root);
for (_id, child) in &children {
let ItemEnum::Use(use_item) = &child.inner else {
continue;
};
if !matches!(child.visibility, Visibility::Public) {
continue;
}
if is_intra_crate_source(&use_item.source, crate_name) {
continue;
}
if use_item.is_glob {
return true;
}
let name = child.name.as_deref().unwrap_or(&use_item.name);
if use_item.id.is_none() || model.find_module(name).is_none() {
return true;
}
}
false
}
pub fn collect_external_crate_names(model: &CrateModel) -> Vec<String> {
let crate_name = model.crate_name();
let Some(root) = model.root_module() else {
return Vec::new();
};
let children = model.module_children(root);
let mut seen = HashSet::new();
for (_id, child) in &children {
let ItemEnum::Use(use_item) = &child.inner else {
continue;
};
if !matches!(child.visibility, Visibility::Public) {
continue;
}
if is_intra_crate_source(&use_item.source, crate_name) {
continue;
}
let source_crate = extract_crate_name(&use_item.source);
if model.find_module(&source_crate).is_some() {
continue;
}
seen.insert(source_crate);
}
seen.into_iter().collect()
}
#[allow(clippy::too_many_arguments)]
pub fn build_cross_crate_index(
primary_model: &CrateModel,
toolchain: &str,
manifest_path: Option<&str>,
target_dir: &Path,
verbose: bool,
workspace_members: &HashSet<String>,
available_packages: &LockfilePackages,
) -> CrossCrateIndex {
let mut source_models: Vec<Box<(CrateModel, ReachableInfo)>> = Vec::new();
let mut items: Vec<AccessibleEntry> = Vec::new();
let mut visited_crates: HashSet<String> = HashSet::new();
visited_crates.insert(primary_model.crate_name().to_string());
let ctx = WalkParams {
toolchain,
manifest_path,
target_dir,
verbose,
workspace_members,
available_packages,
};
let Some(root) = primary_model.root_module() else {
return CrossCrateIndex {
source_models,
items,
};
};
let primary_reachable = compute_reachable_set(primary_model);
walk_accessible(
primary_model,
root,
"",
None, &primary_reachable,
0,
&mut visited_crates,
&mut source_models,
&mut items,
&ctx,
);
dedup_accessible_entries(&mut items);
items.sort_by(|a, b| a.accessible_path.cmp(&b.accessible_path));
CrossCrateIndex {
source_models,
items,
}
}
struct WalkParams<'a> {
toolchain: &'a str,
manifest_path: Option<&'a str>,
target_dir: &'a Path,
verbose: bool,
workspace_members: &'a HashSet<String>,
available_packages: &'a LockfilePackages,
}
#[allow(clippy::too_many_arguments)]
fn walk_accessible(
model: &CrateModel,
module_item: &Item,
prefix: &str,
crate_idx: Option<usize>, reachable: &ReachableInfo,
depth: usize,
visited_crates: &mut HashSet<String>,
source_models: &mut Vec<Box<(CrateModel, ReachableInfo)>>,
items: &mut Vec<AccessibleEntry>,
ctx: &WalkParams,
) {
if depth > 8 {
return;
}
let crate_name = model.crate_name().to_string();
let children = model.module_children(module_item);
for (child_id, child) in &children {
if !reachable.reachable.contains(child_id) {
continue;
}
match &child.inner {
ItemEnum::Module(_) => {
let name = match &child.name {
Some(n) => n.as_str(),
None => continue,
};
if reachable.glob_private_modules.contains(child_id) {
walk_accessible(
model,
child,
prefix,
crate_idx,
reachable,
depth + 1,
visited_crates,
source_models,
items,
ctx,
);
continue;
}
if !matches!(child.visibility, Visibility::Public) {
continue;
}
let child_prefix = join_path(prefix, name);
if let Some(ci) = crate_idx {
items.push(AccessibleEntry {
accessible_path: child_prefix.clone(),
crate_idx: ci,
item_id: Id(child_id.0),
item_kind: AccessibleItemKind::Module,
});
}
walk_accessible(
model,
child,
&child_prefix,
crate_idx,
reachable,
depth + 1,
visited_crates,
source_models,
items,
ctx,
);
}
ItemEnum::Use(use_item) => {
if !matches!(child.visibility, Visibility::Public) {
continue;
}
if use_item.is_glob {
handle_glob_reexport(
model,
use_item,
&crate_name,
prefix,
crate_idx,
reachable,
depth,
visited_crates,
source_models,
items,
ctx,
);
} else {
handle_named_reexport(
model,
child,
use_item,
&crate_name,
prefix,
crate_idx,
reachable,
depth,
visited_crates,
source_models,
items,
ctx,
);
}
}
_ => {
if let Some(ci) = crate_idx {
let name = match &child.name {
Some(n) => n.as_str(),
None => continue,
};
if let Some(kind) = AccessibleItemKind::from_item(child) {
items.push(AccessibleEntry {
accessible_path: join_path(prefix, name),
crate_idx: ci,
item_id: Id(child_id.0),
item_kind: kind,
});
}
}
}
}
}
}
#[allow(clippy::too_many_arguments)]
fn handle_glob_reexport(
model: &CrateModel,
use_item: &rustdoc_types::Use,
crate_name: &str,
prefix: &str,
crate_idx: Option<usize>,
reachable: &ReachableInfo,
depth: usize,
visited_crates: &mut HashSet<String>,
source_models: &mut Vec<Box<(CrateModel, ReachableInfo)>>,
items: &mut Vec<AccessibleEntry>,
ctx: &WalkParams,
) {
let source_path = use_item
.source
.strip_prefix("self::")
.or_else(|| use_item.source.strip_prefix(&format!("{crate_name}::")))
.unwrap_or(&use_item.source);
if let Some(source_mod) = model.find_module(source_path) {
let is_private = !matches!(source_mod.visibility, Visibility::Public);
let walk_prefix = if is_private {
prefix.to_string()
} else {
join_path(
prefix,
source_path.rsplit("::").next().unwrap_or(source_path),
)
};
walk_accessible(
model,
source_mod,
&walk_prefix,
crate_idx,
reachable,
depth + 1,
visited_crates,
source_models,
items,
ctx,
);
} else if is_intra_crate_source(&use_item.source, crate_name) {
} else {
let source_crate = extract_crate_name(&use_item.source);
let sub_path = use_item.source.split_once("::").map(|(_, p)| p.to_string());
if let Some(ci) =
load_or_find_source_crate(&source_crate, source_models, visited_crates, ctx)
{
let start_id = if let Some(ref sp) = sub_path {
source_models[ci].0.find_module(sp).and_then(|_| {
let full = format!("{}::{sp}", source_models[ci].0.crate_name());
source_models[ci].0.module_index.get(&full).cloned()
})
} else {
Some(source_models[ci].0.krate.root)
};
if let Some(start_id) = start_id {
let box_ptr: *const (CrateModel, ReachableInfo) = &*source_models[ci];
let (sub_model, sub_reachable) = unsafe { &*box_ptr };
if let Some(start_mod) = sub_model.krate.index.get(&start_id) {
walk_accessible(
sub_model,
start_mod,
prefix,
Some(ci),
sub_reachable,
depth + 1,
visited_crates,
source_models,
items,
ctx,
);
}
}
}
}
}
#[allow(clippy::too_many_arguments)]
fn handle_named_reexport(
model: &CrateModel,
child: &Item,
use_item: &rustdoc_types::Use,
crate_name: &str,
prefix: &str,
crate_idx: Option<usize>,
reachable: &ReachableInfo,
depth: usize,
visited_crates: &mut HashSet<String>,
source_models: &mut Vec<Box<(CrateModel, ReachableInfo)>>,
items: &mut Vec<AccessibleEntry>,
ctx: &WalkParams,
) {
let alias = child.name.as_deref().unwrap_or(&use_item.name);
if let Some(target_id) = &use_item.id
&& let Some(target) = model.krate.index.get(target_id)
{
let target_path = join_path(prefix, alias);
if matches!(target.inner, ItemEnum::Module(_)) {
if let Some(ci) = crate_idx {
items.push(AccessibleEntry {
accessible_path: target_path.clone(),
crate_idx: ci,
item_id: *target_id,
item_kind: AccessibleItemKind::Module,
});
}
walk_accessible(
model,
target,
&target_path,
crate_idx,
reachable,
depth + 1,
visited_crates,
source_models,
items,
ctx,
);
} else if let Some(ci) = crate_idx
&& let Some(kind) = AccessibleItemKind::from_item(target)
{
items.push(AccessibleEntry {
accessible_path: target_path,
crate_idx: ci,
item_id: *target_id,
item_kind: kind,
});
}
} else if is_intra_crate_source(&use_item.source, crate_name) {
} else {
let source_crate = extract_crate_name(&use_item.source);
let sub_path = use_item.source.split_once("::").map(|(_, p)| p.to_string());
if let Some(ci) =
load_or_find_source_crate(&source_crate, source_models, visited_crates, ctx)
{
let alias_path = join_path(prefix, alias);
if let Some(ref sp) = sub_path {
let has_module = source_models[ci].0.find_module(sp).is_some();
if has_module {
let mod_id = {
let full = format!("{}::{sp}", source_models[ci].0.crate_name());
source_models[ci].0.module_index.get(&full).cloned()
};
items.push(AccessibleEntry {
accessible_path: alias_path.clone(),
crate_idx: ci,
item_id: source_models[ci].0.krate.root,
item_kind: AccessibleItemKind::Module,
});
if let Some(mod_id) = mod_id {
let box_ptr: *const (CrateModel, ReachableInfo) = &*source_models[ci];
let (sub_model, sub_reachable) = unsafe { &*box_ptr };
if let Some(mod_item) = sub_model.krate.index.get(&mod_id) {
walk_accessible(
sub_model,
mod_item,
&alias_path,
Some(ci),
sub_reachable,
depth + 1,
visited_crates,
source_models,
items,
ctx,
);
}
}
} else {
let item_name = sp.rsplit("::").next().unwrap_or(sp);
find_and_register_item(&source_models[ci].0, item_name, &alias_path, ci, items);
}
} else {
let root_id = source_models[ci].0.krate.root;
items.push(AccessibleEntry {
accessible_path: alias_path.clone(),
crate_idx: ci,
item_id: root_id,
item_kind: AccessibleItemKind::Module,
});
let box_ptr: *const (CrateModel, ReachableInfo) = &*source_models[ci];
let (sub_model, sub_reachable) = unsafe { &*box_ptr };
if let Some(root) = sub_model.krate.index.get(&root_id) {
walk_accessible(
sub_model,
root,
&alias_path,
Some(ci),
sub_reachable,
depth + 1,
visited_crates,
source_models,
items,
ctx,
);
}
}
}
}
}
fn load_or_find_source_crate(
source_crate_name: &str,
source_models: &mut Vec<Box<(CrateModel, ReachableInfo)>>,
visited_crates: &mut HashSet<String>,
ctx: &WalkParams,
) -> Option<usize> {
for (i, entry) in source_models.iter().enumerate() {
let model = &entry.0;
if model.crate_name() == source_crate_name {
return Some(i);
}
}
if !ctx.available_packages.is_empty() {
let hyphenated = source_crate_name.replace('_', "-");
if !ctx.available_packages.contains(source_crate_name)
&& !ctx.available_packages.contains(&hyphenated)
{
return None;
}
}
let resolved_name = ctx
.available_packages
.resolve_spec(source_crate_name)
.unwrap_or_else(|| source_crate_name.to_string());
let use_cache = !ctx.workspace_members.contains(source_crate_name)
&& !ctx
.workspace_members
.contains(&source_crate_name.replace('_', "-"));
let json_path = rustdoc_json::generate_rustdoc_json(
&resolved_name,
ctx.toolchain,
ctx.manifest_path,
true, ctx.target_dir,
ctx.verbose,
use_cache,
)
.ok()
.or_else(|| {
if !resolved_name.contains('-') {
let hyphenated = resolved_name.replace('_', "-");
if hyphenated != resolved_name {
return rustdoc_json::generate_rustdoc_json(
&hyphenated,
ctx.toolchain,
ctx.manifest_path,
true,
ctx.target_dir,
ctx.verbose,
use_cache,
)
.ok();
}
}
None
})?;
let krate = rustdoc_json::parse_rustdoc_json_cached(&json_path).ok()?;
let model = CrateModel::from_crate(krate);
visited_crates.insert(source_crate_name.to_string());
let reachable = compute_reachable_set(&model);
let ci = source_models.len();
source_models.push(Box::new((model, reachable)));
Some(ci)
}
fn find_and_register_item(
model: &CrateModel,
item_name: &str,
accessible_path: &str,
crate_idx: usize,
items: &mut Vec<AccessibleEntry>,
) {
let Some(root) = model.root_module() else {
return;
};
for (child_id, child) in model.module_children(root) {
let name = child.name.as_deref().or(match &child.inner {
ItemEnum::Use(u) => Some(u.name.as_str()),
_ => None,
});
if name == Some(item_name)
&& let Some(kind) = AccessibleItemKind::from_item(child)
{
items.push(AccessibleEntry {
accessible_path: accessible_path.to_string(),
crate_idx,
item_id: Id(child_id.0),
item_kind: kind,
});
return;
}
}
}
fn join_path(prefix: &str, name: &str) -> String {
if prefix.is_empty() {
name.to_string()
} else {
format!("{prefix}::{name}")
}
}
fn dedup_accessible_entries(items: &mut Vec<AccessibleEntry>) {
use std::collections::HashMap;
let mut best: HashMap<(usize, u32), usize> = HashMap::new();
for (i, entry) in items.iter().enumerate() {
let key = (entry.crate_idx, entry.item_id.0);
let dominated = if let Some(&existing_idx) = best.get(&key) {
let existing = &items[existing_idx];
prefer_path(&entry.accessible_path, &existing.accessible_path)
} else {
true };
if dominated {
best.insert(key, i);
}
}
let keep: HashSet<usize> = best.into_values().collect();
let mut idx = 0;
items.retain(|_| {
let k = keep.contains(&idx);
idx += 1;
k
});
}
fn prefer_path(candidate: &str, existing: &str) -> bool {
let candidate_prelude = candidate.split("::").any(|seg| seg == "prelude");
let existing_prelude = existing.split("::").any(|seg| seg == "prelude");
match (candidate_prelude, existing_prelude) {
(false, true) => true, (true, false) => false, _ => candidate.len() < existing.len(), }
}
pub(crate) fn extract_crate_name(source: &str) -> String {
source.split("::").next().unwrap_or(source).to_string()
}
pub(crate) fn is_intra_crate_source(source: &str, crate_name: &str) -> bool {
source.starts_with("self::") || extract_crate_name(source) == crate_name
}
fn follow_use_chain(
source: &str,
_display_name: &str,
rest: Option<String>,
toolchain: &str,
manifest_path: Option<&str>,
target_dir: &Path,
verbose: bool,
) -> Option<CrossCrateResolution> {
let mut visited = HashSet::new();
let mut current_source = source.to_string();
for _ in 0..5 {
let crate_name = extract_crate_name(¤t_source);
if !visited.insert(crate_name.clone()) {
break; }
let json_path = rustdoc_json::generate_rustdoc_json(
&crate_name,
toolchain,
manifest_path,
true,
target_dir,
verbose,
true, )
.ok()?;
let krate = rustdoc_json::parse_rustdoc_json_cached(&json_path).ok()?;
let model = CrateModel::from_crate(krate);
let sub_path: Option<String> = current_source.split_once("::").map(|(_, p)| p.to_string());
if let Some(sub) = sub_path {
if model.find_module(&sub).is_some() {
return Some(CrossCrateResolution {
model,
inner_module_path: if let Some(r) = &rest {
Some(format!("{sub}::{r}"))
} else {
Some(sub)
},
});
}
let first_sub: String = sub.split("::").next().unwrap_or(&sub).to_string();
let mut found_next = false;
if let Some(root) = model.root_module() {
for (_id, child) in model.module_children(root) {
let ItemEnum::Use(u) = &child.inner else {
continue;
};
if u.is_glob {
continue;
}
let n = child.name.as_deref().unwrap_or(&u.name);
if n == first_sub {
current_source = u.source.clone();
found_next = true;
break;
}
}
}
if !found_next {
return Some(CrossCrateResolution {
model,
inner_module_path: rest,
});
}
} else {
return Some(CrossCrateResolution {
model,
inner_module_path: rest,
});
}
}
None
}