use std::collections::HashSet;
use std::path::Path;
use rustdoc_types::{ItemEnum, Visibility};
use crate::model::CrateModel;
use crate::rustdoc_json;
pub struct CrossCrateResolution {
pub model: CrateModel,
pub inner_module_path: Option<String>,
}
pub struct SubCrate {
pub display_name: String,
pub model: CrateModel,
}
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 discover_all_reexported_crates(
primary_model: &CrateModel,
toolchain: &str,
manifest_path: Option<&str>,
target_dir: &Path,
verbose: bool,
) -> Vec<SubCrate> {
let crate_name = primary_model.crate_name();
let Some(root) = primary_model.root_module() else {
return Vec::new();
};
let children = primary_model.module_children(root);
let mut results = Vec::new();
let mut seen_names = HashSet::new();
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 !seen_names.insert(name.to_string()) {
continue;
}
if let Some(sub) = resolve_single_reexport(
name,
&use_item.source,
toolchain,
manifest_path,
target_dir,
verbose,
) {
results.push(sub);
}
}
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;
}
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 {
eprintln!("warning: failed to generate JSON for '{source_crate}', skipping");
continue;
};
let Ok(krate) = rustdoc_json::parse_rustdoc_json_cached(&json_path) else {
eprintln!("warning: failed to parse JSON for '{source_crate}', skipping");
continue;
};
let source_model = CrateModel::from_crate(krate);
let Some(sroot) = source_model.root_module() else {
continue;
};
for (_sid, schild) in source_model.module_children(sroot) {
let ItemEnum::Use(su) = &schild.inner else {
continue;
};
if su.is_glob {
continue;
}
if !matches!(schild.visibility, Visibility::Public) {
continue;
}
let sname = schild.name.as_deref().unwrap_or(&su.name);
if !seen_names.insert(sname.to_string()) {
continue;
}
if let Some(sub) = resolve_single_reexport(
sname,
&su.source,
toolchain,
manifest_path,
target_dir,
verbose,
) {
results.push(sub);
}
}
}
results
}
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
}
fn extract_crate_name(source: &str) -> String {
source.split("::").next().unwrap_or(source).to_string()
}
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
}
fn resolve_single_reexport(
display_name: &str,
source: &str,
toolchain: &str,
manifest_path: Option<&str>,
target_dir: &Path,
verbose: bool,
) -> Option<SubCrate> {
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<&str> = current_source.split_once("::").map(|(_, p)| p);
if let Some(sub) = sub_path {
if model.find_module(sub).is_some() {
return Some(SubCrate {
display_name: display_name.to_string(),
model,
});
}
let first_sub = sub.split("::").next().unwrap_or(sub);
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(SubCrate {
display_name: display_name.to_string(),
model,
});
}
} else {
return Some(SubCrate {
display_name: display_name.to_string(),
model,
});
}
}
None
}