use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use compact_str::CompactString;
use rustdoc_types::{Crate, Id, ItemEnum, ItemKind, Visibility};
use tracing::instrument;
use super::{CrateCollection, RUST_PATH_SEP};
use crate::linker::{AnchorUtils, LinkRegistry};
type Str = CompactString;
type RegistryKey = (Str, Id);
#[derive(PartialEq, Eq)]
struct BorrowedKey<'a>(&'a str, Id);
impl Hash for BorrowedKey<'_> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.hash(state);
self.1.hash(state);
}
}
fn keys_match(stored: &RegistryKey, borrowed: &BorrowedKey<'_>) -> bool {
stored.0 == borrowed.0 && stored.1 == borrowed.1
}
#[derive(Debug, Default)]
pub struct UnifiedLinkRegistry {
item_paths: hashbrown::HashMap<RegistryKey, Str>,
item_names: hashbrown::HashMap<RegistryKey, Str>,
name_index: HashMap<Str, Vec<(Str, Id, ItemKind)>>,
re_export_sources: hashbrown::HashMap<RegistryKey, Str>,
primary_crate: Option<Str>,
}
impl UnifiedLinkRegistry {
#[must_use]
#[instrument(skip(crates), fields(crate_count = crates.names().len()))]
pub fn build(crates: &CrateCollection, primary_crate: Option<&str>) -> Self {
#[cfg(feature = "trace")]
tracing::debug!(?primary_crate, "Building unified link registry");
let mut registry = Self {
primary_crate: primary_crate.map(Str::from),
..Default::default()
};
for (crate_name, krate) in crates.iter() {
#[cfg(feature = "trace")]
tracing::trace!(crate_name, "Registering crate items");
registry.register_crate(crate_name, krate);
}
#[cfg(feature = "trace")]
tracing::debug!(
item_count = registry.item_paths.len(),
name_count = registry.name_index.len(),
"Registry build complete"
);
registry
}
fn register_crate(&mut self, crate_name: &str, krate: &Crate) {
let Some(root) = krate.index.get(&krate.root) else {
return;
};
self.register_item(
crate_name,
krate.root,
crate_name,
"index.md",
ItemKind::Module,
);
self.register_from_paths(crate_name, krate);
if let ItemEnum::Module(module) = &root.inner {
for item_id in &module.items {
if let Some(item) = krate.index.get(item_id) {
self.register_item_recursive(krate, crate_name, *item_id, item, "");
}
}
}
}
fn register_from_paths(&mut self, crate_name: &str, krate: &Crate) {
for (id, path_info) in &krate.paths {
if path_info.crate_id != 0 {
continue;
}
let Some(name) = path_info.path.last() else {
continue;
};
if path_info.kind == rustdoc_types::ItemKind::Module {
continue;
}
self.register_item(crate_name, *id, name, "index.md", path_info.kind);
}
}
#[expect(clippy::match_same_arms)]
const fn item_enum_to_kind(inner: &ItemEnum) -> ItemKind {
match inner {
ItemEnum::Module(_) => ItemKind::Module,
ItemEnum::Struct(_) => ItemKind::Struct,
ItemEnum::Enum(_) => ItemKind::Enum,
ItemEnum::Trait(_) => ItemKind::Trait,
ItemEnum::Function(_) => ItemKind::Function,
ItemEnum::Constant { .. } => ItemKind::Constant,
ItemEnum::TypeAlias(_) => ItemKind::TypeAlias,
ItemEnum::Macro(_) => ItemKind::Macro,
ItemEnum::Use(_) => ItemKind::Use,
_ => ItemKind::Use, }
}
fn register_item_recursive(
&mut self,
krate: &Crate,
crate_name: &str,
item_id: Id,
item: &rustdoc_types::Item,
parent_path: &str,
) {
let name = item.name.as_deref().unwrap_or("unnamed");
match &item.inner {
ItemEnum::Module(module) => {
let module_path = if parent_path.is_empty() {
name.to_string()
} else {
format!("{parent_path}/{name}")
};
let file_path = format!("{module_path}/index.md");
self.register_item(crate_name, item_id, name, &file_path, ItemKind::Module);
for child_id in &module.items {
if let Some(child) = krate.index.get(child_id) {
self.register_item_recursive(
krate,
crate_name,
*child_id,
child,
&module_path,
);
}
}
},
ItemEnum::Struct(_)
| ItemEnum::Enum(_)
| ItemEnum::Trait(_)
| ItemEnum::Function(_)
| ItemEnum::Constant { .. }
| ItemEnum::TypeAlias(_)
| ItemEnum::Macro(_) => {
let file_path = if parent_path.is_empty() {
"index.md".to_string()
} else {
format!("{parent_path}/index.md")
};
let kind = Self::item_enum_to_kind(&item.inner);
self.register_item(crate_name, item_id, name, &file_path, kind);
},
ItemEnum::Use(use_item) => {
let file_path = if parent_path.is_empty() {
"index.md".to_string()
} else {
format!("{parent_path}/index.md")
};
if use_item.is_glob {
if let Some(target_id) = &use_item.id
&& let Some(target_module) = krate.index.get(target_id)
&& let ItemEnum::Module(module) = &target_module.inner
{
for child_id in &module.items {
if let Some(child) = krate.index.get(child_id) {
if !matches!(child.visibility, Visibility::Public) {
continue;
}
let child_name = child.name.as_deref().unwrap_or("unnamed");
let child_kind = Self::item_enum_to_kind(&child.inner);
self.register_item(
crate_name, *child_id, child_name, &file_path, child_kind,
);
}
}
}
} else {
let export_name = &use_item.name;
let kind = use_item
.id
.and_then(|id| krate.index.get(&id))
.map_or(ItemKind::Use, |target| {
Self::item_enum_to_kind(&target.inner)
});
self.register_item(crate_name, item_id, export_name, &file_path, kind);
if let Some(target_id) = use_item.id
&& !self.contains(crate_name, target_id)
{
self.register_item(crate_name, target_id, export_name, &file_path, kind);
}
if !use_item.source.is_empty() {
let key = (Str::from(crate_name), item_id);
self.re_export_sources
.insert(key, Str::from(use_item.source.as_str()));
}
}
},
_ => {},
}
}
fn register_item(&mut self, crate_name: &str, id: Id, name: &str, path: &str, kind: ItemKind) {
let key = (Str::from(crate_name), id);
self.item_paths.insert(key.clone(), Str::from(path));
self.item_names.insert(key, Str::from(name));
self.name_index
.entry(Str::from(name))
.or_default()
.push((Str::from(crate_name), id, kind));
}
#[must_use]
#[instrument(skip(self), level = "trace")]
pub fn get_path(&self, crate_name: &str, id: Id) -> Option<&Str> {
use std::hash::BuildHasher;
let borrowed = BorrowedKey(crate_name, id);
let hash = self.item_paths.hasher().hash_one(&borrowed);
let result = self
.item_paths
.raw_entry()
.from_hash(hash, |k| keys_match(k, &borrowed))
.map(|(_, v)| v);
#[cfg(feature = "trace")]
tracing::trace!(found = result.is_some(), "Path lookup");
result
}
#[must_use]
pub fn get_name(&self, crate_name: &str, id: Id) -> Option<&Str> {
use std::hash::BuildHasher;
let borrowed = BorrowedKey(crate_name, id);
let hash = self.item_names.hasher().hash_one(&borrowed);
self.item_names
.raw_entry()
.from_hash(hash, |k| keys_match(k, &borrowed))
.map(|(_, v)| v)
}
#[must_use]
pub fn get_re_export_source(&self, crate_name: &str, id: Id) -> Option<&Str> {
use std::hash::BuildHasher;
let borrowed = BorrowedKey(crate_name, id);
let hash = self.re_export_sources.hasher().hash_one(&borrowed);
self.re_export_sources
.raw_entry()
.from_hash(hash, |k| keys_match(k, &borrowed))
.map(|(_, v)| v)
}
#[must_use]
pub fn resolve_reexport(&self, crate_name: &str, id: Id) -> Option<(Str, Id)> {
let source = self.get_re_export_source(crate_name, id)?;
self.resolve_path(source)
}
#[must_use]
#[instrument(skip(self), level = "trace")]
pub fn resolve_name(&self, name: &str, current_crate: &str) -> Option<(Str, Id)> {
let candidates = self.name_index.get(name)?;
if candidates.is_empty() {
#[cfg(feature = "trace")]
tracing::trace!("No candidates found");
return None;
}
let current_crate_candidates: Vec<_> = candidates
.iter()
.filter(|(crate_name, _, _)| crate_name == current_crate)
.collect();
if !current_crate_candidates.is_empty() {
if let Some((crate_name, id, _)) = current_crate_candidates
.iter()
.find(|(_, _, kind)| *kind == ItemKind::Module)
{
#[cfg(feature = "trace")]
tracing::trace!(resolved_crate = %crate_name, "Resolved to current crate (module)");
return Some(((*crate_name).clone(), *id));
}
let (crate_name, id, _) = current_crate_candidates[0];
#[cfg(feature = "trace")]
tracing::trace!(resolved_crate = %crate_name, "Resolved to current crate");
return Some((crate_name.clone(), *id));
}
if let Some(primary) = &self.primary_crate {
let primary_candidates: Vec<_> = candidates
.iter()
.filter(|(crate_name, _, _)| crate_name == primary)
.collect();
if !primary_candidates.is_empty() {
if let Some((crate_name, id, _)) = primary_candidates
.iter()
.find(|(_, _, kind)| *kind == ItemKind::Module)
{
#[cfg(feature = "trace")]
tracing::trace!(resolved_crate = %crate_name, "Resolved to primary crate (module)");
return Some(((*crate_name).clone(), *id));
}
let (crate_name, id, _) = primary_candidates[0];
#[cfg(feature = "trace")]
tracing::trace!(resolved_crate = %crate_name, "Resolved to primary crate");
return Some((crate_name.clone(), *id));
}
}
if let Some((crate_name, id, _)) = candidates
.iter()
.find(|(_, _, kind)| *kind == ItemKind::Module)
{
#[cfg(feature = "trace")]
tracing::trace!(resolved_crate = %crate_name, "Resolved to module");
return Some((crate_name.clone(), *id));
}
let result = candidates.first().map(|(c, id, _)| (c.clone(), *id));
#[cfg(feature = "trace")]
tracing::trace!(
resolved_crate = ?result.as_ref().map(|(c, _)| c),
"Resolved to first match"
);
result
}
#[must_use]
pub fn resolve_path(&self, path: &str) -> Option<(Str, Id)> {
let segments: Vec<&str> = path.split(RUST_PATH_SEP).collect();
if segments.is_empty() {
return None;
}
let target_crate = segments[0];
let item_name = segments.last()?;
let candidates = self.name_index.get(*item_name)?;
for (crate_name, id, _kind) in candidates {
if crate_name == target_crate {
return Some((crate_name.clone(), *id));
}
}
None
}
#[must_use]
pub fn create_link(
&self,
from_crate: &str,
from_path: &str,
to_crate: &str,
to_id: Id,
) -> Option<String> {
let target_path = self.get_path(to_crate, to_id)?;
let name = self.get_name(to_crate, to_id)?;
let from_full = format!("{from_crate}/{from_path}");
let to_full = format!("{to_crate}/{target_path}");
let relative = Self::compute_cross_crate_path(&from_full, &to_full);
if from_full == to_full {
let anchor = AnchorUtils::slugify_anchor(name);
return Some(format!("[`{name}`](#{anchor})"));
}
Some(format!("[`{name}`]({relative})"))
}
#[must_use]
pub fn compute_cross_crate_path(from: &str, to: &str) -> String {
LinkRegistry::compute_relative_path(from, to)
}
#[must_use]
pub fn get_anchor(&self, crate_name: &str, id: Id) -> Option<String> {
let name = self.get_name(crate_name, id)?;
Some(format!("#{}", AnchorUtils::slugify_anchor(name)))
}
#[must_use]
pub fn contains(&self, crate_name: &str, id: Id) -> bool {
use std::hash::BuildHasher;
let borrowed = BorrowedKey(crate_name, id);
let hash = self.item_paths.hasher().hash_one(&borrowed);
self.item_paths
.raw_entry()
.from_hash(hash, |k| keys_match(k, &borrowed))
.is_some()
}
#[must_use]
pub fn len(&self) -> usize {
self.item_paths.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.item_paths.is_empty()
}
}
#[cfg(test)]
mod tests {
use hashbrown::DefaultHashBuilder;
use super::*;
#[test]
fn test_cross_crate_path_same_crate() {
assert_eq!(
UnifiedLinkRegistry::compute_cross_crate_path(
"tracing/index.md",
"tracing/span/index.md"
),
"span/index.md"
);
}
#[test]
fn test_cross_crate_path_different_crates() {
assert_eq!(
UnifiedLinkRegistry::compute_cross_crate_path(
"tracing/span/index.md",
"tracing_core/subscriber/index.md"
),
"../../tracing_core/subscriber/index.md"
);
}
#[test]
fn test_cross_crate_path_to_root() {
assert_eq!(
UnifiedLinkRegistry::compute_cross_crate_path(
"tracing/span/enter/index.md",
"tracing/index.md"
),
"../../index.md"
);
}
#[test]
fn test_borrowed_key_hash_compatibility() {
use std::hash::BuildHasher;
let hasher = DefaultHashBuilder::default();
let id = Id(42);
let owned: RegistryKey = (Str::from("test_crate"), id);
let borrowed = BorrowedKey("test_crate", id);
let owned_hash = hasher.hash_one(&owned);
let borrowed_hash = hasher.hash_one(&borrowed);
assert_eq!(
owned_hash, borrowed_hash,
"BorrowedKey hash must equal RegistryKey hash"
);
}
#[test]
fn test_raw_entry_lookup() {
let mut registry = UnifiedLinkRegistry::default();
let id = Id(123);
registry.register_item(
"my_crate",
id,
"MyType",
"module/index.md",
ItemKind::Struct,
);
assert!(registry.contains("my_crate", id));
assert_eq!(
registry.get_path("my_crate", id),
Some(&Str::from("module/index.md"))
);
assert_eq!(
registry.get_name("my_crate", id),
Some(&Str::from("MyType"))
);
assert!(!registry.contains("other_crate", id));
assert!(registry.get_path("other_crate", id).is_none());
}
}