use std::collections::{HashMap, HashSet};
mod shape;
pub(crate) use shape::*;
pub(crate) type UseMap = HashMap<String, String>;
pub(crate) type ReexportMap = HashMap<String, String>;
pub(crate) type AliasMap = HashMap<String, String>;
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum BareFallback {
Ignore,
CurrentModule,
}
pub(crate) fn strip_raw(ident: &str) -> String {
ident.strip_prefix("r#").unwrap_or(ident).to_string()
}
pub(crate) fn canonical_path_str(path: &str) -> String {
path.split("::")
.map(strip_raw)
.collect::<Vec<_>>()
.join("::")
}
pub(crate) fn collect_uses(items: &[syn::Item]) -> UseMap {
let mut map = UseMap::new();
for item in items {
if let syn::Item::Use(use_item) = item {
collect_use_tree(&use_item.tree, String::new(), &mut map);
}
}
map
}
fn collect_use_tree(tree: &syn::UseTree, prefix: String, map: &mut UseMap) {
let join = |prefix: &str, ident: &str| {
if prefix.is_empty() {
ident.to_string()
} else {
format!("{prefix}::{ident}")
}
};
match tree {
syn::UseTree::Path(path) => {
let ident = strip_raw(&path.ident.to_string());
collect_use_tree(&path.tree, join(&prefix, &ident), map);
}
syn::UseTree::Name(name) => {
let ident = strip_raw(&name.ident.to_string());
if ident == "self" {
if let Some(last) = prefix.rsplit("::").next().filter(|s| !s.is_empty()) {
map.insert(last.to_string(), prefix.clone());
}
} else {
map.insert(ident.clone(), join(&prefix, &ident));
}
}
syn::UseTree::Rename(rename) => {
let ident = strip_raw(&rename.ident.to_string());
let alias = strip_raw(&rename.rename.to_string());
if alias == "_" {
} else if ident == "self" {
if !prefix.is_empty() {
map.insert(alias, prefix.clone());
}
} else {
map.insert(alias, join(&prefix, &ident));
}
}
syn::UseTree::Glob(_) => {}
syn::UseTree::Group(group) => {
for item in &group.items {
collect_use_tree(item, prefix.clone(), map);
}
}
}
}
fn resolve_crate_relative(segs: &[String], module: &str) -> Option<String> {
let head = segs.first()?;
match head.as_str() {
"crate" => Some(segs.join("::")),
"self" | "super" => {
let mut parts: Vec<&str> = module.split("::").collect();
let mut i = 0;
while i < segs.len() {
match segs[i].as_str() {
"self" => i += 1,
"super" => {
if parts.len() <= 1 {
return None;
}
parts.pop();
i += 1;
}
_ => break,
}
}
let rest = &segs[i..];
if rest.is_empty() {
Some(parts.join("::"))
} else {
Some(format!("{}::{}", parts.join("::"), rest.join("::")))
}
}
_ => None,
}
}
pub(crate) fn resolve_path(
path: &syn::Path,
uses: &UseMap,
module: &str,
bare: BareFallback,
) -> Option<String> {
let segs: Vec<String> = path
.segments
.iter()
.map(|s| strip_raw(&s.ident.to_string()))
.collect();
let head = segs.first()?;
if let Some(canonical) = resolve_crate_relative(&segs, module) {
return Some(canonical);
}
match uses.get(head) {
Some(full) => {
let rest = &segs[1..];
let combined = if rest.is_empty() {
full.clone()
} else {
format!("{full}::{}", rest.join("::"))
};
let combined_segs: Vec<String> = combined.split("::").map(strip_raw).collect();
Some(resolve_crate_relative(&combined_segs, module).unwrap_or(combined))
}
None => match bare {
BareFallback::Ignore => None,
BareFallback::CurrentModule => {
if module.is_empty() {
Some(format!("crate::{}", segs.join("::")))
} else {
Some(format!("{module}::{}", segs.join("::")))
}
}
},
}
}
pub(crate) fn extern_verbatim_segs(segs: &[String], externs: &HashSet<String>) -> Option<String> {
let head = segs.first()?;
externs.contains(head).then(|| segs.join("::"))
}
pub(crate) type ExternRenameMap = HashMap<String, String>;
pub(crate) fn extern_verbatim_renamed(
path: &syn::Path,
externs: &HashSet<String>,
renames: &ExternRenameMap,
) -> Option<String> {
let mut segs: Vec<String> = path
.segments
.iter()
.map(|s| strip_raw(&s.ident.to_string()))
.collect();
if let Some(real) = segs.first().and_then(|h| renames.get(h)).cloned() {
segs[0] = real;
return Some(segs.join("::"));
}
extern_verbatim_segs(&segs, externs)
}
pub(crate) fn apply_crate_root_rename(canonical: String, renames: &ExternRenameMap) -> String {
let segs: Vec<&str> = canonical.split("::").collect();
if segs.len() >= 2 && segs[0] == "crate" {
if let Some(real) = renames.get(segs[1]) {
let mut out = vec![real.as_str()];
out.extend_from_slice(&segs[2..]);
return out.join("::");
}
}
canonical
}
pub(crate) fn apply_bare_alias_rename(canonical: String, renames: &ExternRenameMap) -> String {
let mut segs: Vec<&str> = canonical.split("::").collect();
if let Some(real) = segs.first().and_then(|h| renames.get(*h)) {
segs[0] = real.as_str();
return segs.join("::");
}
canonical
}
pub(crate) fn renames_shadowed(
renames: &ExternRenameMap,
child_mods: &HashSet<String>,
) -> ExternRenameMap {
renames
.iter()
.filter(|(alias, _)| !child_mods.contains(*alias))
.map(|(a, b)| (a.clone(), b.clone()))
.collect()
}
pub(crate) fn collect_reexports(
items: &[syn::Item],
module: &str,
externs: &HashSet<String>,
child_mods: &HashSet<String>,
renames: &ExternRenameMap,
out: &mut ReexportMap,
) {
let externs_bare: HashSet<String> = externs.difference(child_mods).cloned().collect();
let renames_bare = renames_shadowed(renames, child_mods);
for item in items {
if let syn::Item::Use(use_item) = item {
if matches!(use_item.vis, syn::Visibility::Inherited) {
continue;
}
let mut local = UseMap::new();
collect_use_tree(&use_item.tree, String::new(), &mut local);
let (head_externs, head_renames) = if use_item.leading_colon.is_some() {
(externs, renames)
} else {
(&externs_bare, &renames_bare)
};
for (name, written) in local {
let alias = format!("{module}::{name}");
if let Some(target) =
canonicalize_use_target(&written, module, head_externs, head_renames)
{
if target != alias && !is_strict_path_prefix(&alias, &target) {
out.insert(alias, target);
}
}
}
}
}
}
fn canonicalize_use_target(
written: &str,
module: &str,
externs: &HashSet<String>,
renames: &ExternRenameMap,
) -> Option<String> {
let segs: Vec<String> = written.split("::").map(strip_raw).collect();
if let Some(real) = segs.first().and_then(|h| renames.get(h)).cloned() {
let mut renamed = segs.clone();
renamed[0] = real;
return Some(renamed.join("::"));
}
resolve_crate_relative(&segs, module).or_else(|| extern_verbatim_segs(&segs, externs))
}
fn is_strict_path_prefix(prefix: &str, path: &str) -> bool {
path.len() > prefix.len() && path.starts_with(prefix) && path[prefix.len()..].starts_with("::")
}
fn rewrite_longest_prefix(
path: &str,
map: &std::collections::HashMap<String, String>,
) -> Option<String> {
let segments: Vec<&str> = path.split("::").collect();
for end in (1..=segments.len()).rev() {
let prefix = segments[..end].join("::");
if let Some(target) = map.get(&prefix) {
if end == segments.len() {
return Some(target.clone());
}
return Some(format!("{target}::{}", segments[end..].join("::")));
}
}
None
}
pub(crate) fn canonicalize_through_reexports(path: &str, reexports: &ReexportMap) -> String {
let mut current = path.to_string();
let mut seen = std::collections::HashSet::new();
let cap = reexports.len() + 1;
while seen.insert(current.clone()) {
if seen.len() > cap {
break;
}
match rewrite_longest_prefix(¤t, reexports) {
Some(next) => current = next,
None => break,
}
}
current
}
pub(crate) fn canonicalize_through_aliases(
path: &str,
aliases: &AliasMap,
reexports: &ReexportMap,
) -> String {
let mut current = path.to_string();
let mut seen = std::collections::HashSet::new();
let cap = aliases.len() + reexports.len() + 1;
while seen.insert(current.clone()) {
if seen.len() > cap {
break;
}
if let Some(next) = rewrite_longest_prefix(¤t, aliases) {
current = next;
continue;
}
if let Some(next) = rewrite_longest_prefix(¤t, reexports) {
current = next;
continue;
}
break;
}
current
}
pub(crate) fn alias_nominal_target(ty: &syn::Type) -> Option<&syn::Path> {
if let syn::Type::Path(tp) = ty {
if tp.qself.is_none()
&& tp
.path
.segments
.iter()
.all(|s| matches!(s.arguments, syn::PathArguments::None))
{
return Some(&tp.path);
}
}
None
}
pub(crate) fn bare_local_alias(
path: &syn::Path,
module: &str,
aliases: &AliasMap,
) -> Option<String> {
bare_single_segment_ident(path)
.map(|n| format!("{module}::{n}"))
.filter(|key| aliases.contains_key(key))
}
pub(crate) fn bare_single_segment_ident(path: &syn::Path) -> Option<String> {
if path.leading_colon.is_some() || path.segments.len() != 1 {
return None;
}
let seg = &path.segments[0];
if !matches!(seg.arguments, syn::PathArguments::None) {
return None;
}
Some(strip_raw(&seg.ident.to_string()))
}