use crate::core::ir::ApiSurface;
use ahash::{AHashMap, AHashSet};
use heck::ToPascalCase;
pub(crate) fn disambiguate_type_names(surface: &mut ApiSurface) {
let renames = compute_renames(surface);
if renames.is_empty() {
return;
}
apply_renames(surface, &renames);
}
fn compute_renames(surface: &ApiSurface) -> AHashMap<String, String> {
let mut entries: Vec<(String, String, Kind, bool)> = Vec::new();
for t in &surface.types {
entries.push((t.name.clone(), t.rust_path.clone(), Kind::Type, t.binding_excluded));
}
for e in &surface.enums {
entries.push((e.name.clone(), e.rust_path.clone(), Kind::Enum, e.binding_excluded));
}
for e in &surface.errors {
entries.push((e.name.clone(), e.rust_path.clone(), Kind::Error, e.binding_excluded));
}
let mut by_name: AHashMap<String, Vec<(String, Kind, bool)>> = AHashMap::new();
for (name, path, kind, bx) in entries {
by_name.entry(name).or_default().push((path, kind, bx));
}
let mut taken: AHashSet<String> = by_name.keys().cloned().collect();
let mut renames: AHashMap<String, String> = AHashMap::new();
let mut group_names: Vec<String> = by_name.keys().cloned().collect();
group_names.sort();
for name in group_names {
let mut paths = by_name.remove(&name).expect("present");
if paths.len() < 2 {
continue;
}
paths.sort_by(|a, b| a.2.cmp(&b.2).then_with(|| a.0.cmp(&b.0)));
let mut seen_paths: AHashSet<String> = AHashSet::new();
paths.retain(|(path, _kind, _bx)| seen_paths.insert(path.clone()));
if paths.len() < 2 {
continue;
}
for (path, _kind, _bx) in paths.into_iter().skip(1) {
let new_name = pick_unique_name(&name, &path, &taken);
renames.insert(path, new_name.clone());
taken.insert(new_name);
}
}
renames
}
#[derive(Copy, Clone, Debug)]
enum Kind {
Type,
Enum,
Error,
}
fn pick_unique_name(original: &str, rust_path: &str, taken: &AHashSet<String>) -> String {
let segments: Vec<&str> = rust_path.split("::").collect();
if segments.len() <= 2 {
return numeric_suffix(original, taken);
}
let module_segments = &segments[1..segments.len() - 1];
for take in 1..=module_segments.len() {
let start = module_segments.len() - take;
let prefix: String = module_segments[start..]
.iter()
.map(|s| s.to_pascal_case())
.collect::<Vec<_>>()
.join("");
let candidate = format!("{prefix}{original}");
if !taken.contains(&candidate) {
return candidate;
}
}
numeric_suffix(original, taken)
}
fn numeric_suffix(original: &str, taken: &AHashSet<String>) -> String {
let mut n: u32 = 2;
loop {
let candidate = format!("{original}{n}");
if !taken.contains(&candidate) {
return candidate;
}
n += 1;
}
}
fn apply_renames(surface: &mut ApiSurface, renames: &AHashMap<String, String>) {
for ty in &mut surface.types {
if let Some(new_name) = renames.get(&ty.rust_path) {
ty.name = new_name.clone();
}
}
for en in &mut surface.enums {
if let Some(new_name) = renames.get(&en.rust_path) {
en.name = new_name.clone();
}
}
for err in &mut surface.errors {
if let Some(new_name) = renames.get(&err.rust_path) {
err.name = new_name.clone();
}
}
let excluded: Vec<(String, String)> = surface.excluded_type_paths.drain().collect();
for (name, path) in excluded {
if let Some(new_name) = renames.get(&path) {
surface.excluded_type_paths.insert(new_name.clone(), path);
} else {
surface.excluded_type_paths.insert(name, path);
}
}
}
#[cfg(test)]
mod tests {
use crate::core::ir::{ApiSurface, EnumDef, EnumVariant, TypeDef};
use super::disambiguate_type_names;
fn make_type(name: &str, rust_path: &str) -> TypeDef {
make_type_with_bx(name, rust_path, false)
}
fn make_type_with_bx(name: &str, rust_path: &str, binding_excluded: bool) -> TypeDef {
TypeDef {
name: name.to_string(),
rust_path: rust_path.to_string(),
original_rust_path: String::new(),
fields: vec![],
methods: vec![],
is_opaque: true,
is_clone: false,
is_copy: false,
is_trait: false,
has_default: false,
has_stripped_cfg_fields: false,
is_return_type: false,
doc: String::new(),
cfg: None,
serde_rename_all: None,
has_serde: false,
super_traits: vec![],
binding_excluded,
binding_exclusion_reason: None,
is_variant_wrapper: false,
}
}
fn make_enum(name: &str, rust_path: &str) -> EnumDef {
EnumDef {
name: name.to_string(),
rust_path: rust_path.to_string(),
original_rust_path: String::new(),
variants: vec![EnumVariant {
name: "Unit".into(),
fields: vec![],
doc: String::new(),
is_default: false,
serde_rename: None,
is_tuple: false,
}],
doc: String::new(),
cfg: None,
is_copy: false,
has_serde: false,
serde_tag: None,
serde_untagged: false,
serde_rename_all: None,
binding_excluded: false,
binding_exclusion_reason: None,
}
}
fn empty_surface() -> ApiSurface {
ApiSurface::default()
}
#[test]
fn pair_collision_renames_second_with_parent_prefix() {
let mut s = empty_surface();
s.types.push(make_type("Item", "my_crate::Item"));
s.types.push(make_type("Item", "my_crate::testing::Item"));
disambiguate_type_names(&mut s);
let names: Vec<_> = s.types.iter().map(|t| t.name.as_str()).collect();
assert!(names.contains(&"Item"), "first kept original name");
assert!(names.contains(&"TestingItem"), "second renamed with PascalCase parent");
}
#[test]
fn three_way_collision_uses_each_parent_segment() {
let mut s = empty_surface();
s.types.push(make_type("Foo", "my_crate::a::Foo"));
s.types.push(make_type("Foo", "my_crate::bar::Foo"));
s.types.push(make_type("Foo", "my_crate::baz::Foo"));
disambiguate_type_names(&mut s);
let names: Vec<_> = s.types.iter().map(|t| t.name.clone()).collect();
assert_eq!(names, vec!["Foo", "BarFoo", "BazFoo"]);
}
#[test]
fn single_occurrence_unchanged() {
let mut s = empty_surface();
s.types.push(make_type("Solo", "my_crate::Solo"));
disambiguate_type_names(&mut s);
assert_eq!(s.types[0].name, "Solo");
}
#[test]
fn distinct_idents_unchanged() {
let mut s = empty_surface();
s.types.push(make_type("Alpha", "my_crate::Alpha"));
s.types.push(make_type("Beta", "my_crate::Beta"));
disambiguate_type_names(&mut s);
assert_eq!(s.types[0].name, "Alpha");
assert_eq!(s.types[1].name, "Beta");
}
#[test]
fn collision_across_type_and_enum_renames_second() {
let mut s = empty_surface();
s.types.push(make_type("Shared", "my_crate::Shared"));
s.enums.push(make_enum("Shared", "my_crate::other::Shared"));
disambiguate_type_names(&mut s);
assert_eq!(s.types[0].name, "Shared");
assert_eq!(s.enums[0].name, "OtherShared");
}
#[test]
fn cascading_collision_walks_further_up() {
let mut s = empty_surface();
s.types.push(make_type("Foo", "my_crate::Foo"));
s.types.push(make_type("Foo", "my_crate::ext::Foo"));
s.types.push(make_type("Foo", "my_crate::other::ext::Foo"));
disambiguate_type_names(&mut s);
let names: Vec<_> = s.types.iter().map(|t| t.name.clone()).collect();
assert_eq!(names, vec!["Foo", "ExtFoo", "OtherExtFoo"]);
}
#[test]
fn snake_case_parent_segment_is_pascal_cased() {
let mut s = empty_surface();
s.types.push(make_type("Event", "my_crate::Event"));
s.types.push(make_type("Event", "my_crate::sse_stream::Event"));
disambiguate_type_names(&mut s);
let names: Vec<_> = s.types.iter().map(|t| t.name.clone()).collect();
assert!(names.contains(&"SseStreamEvent".to_string()));
}
#[test]
fn bx_true_entry_yields_original_name_to_bx_false_entry() {
let mut s = empty_surface();
s.types.push(make_type_with_bx(
"EmbeddingPreset",
"my_crate::AModule::EmbeddingPreset",
true,
));
s.types.push(make_type_with_bx(
"EmbeddingPreset",
"my_crate::BModule::EmbeddingPreset",
false,
));
disambiguate_type_names(&mut s);
let names: Vec<_> = s.types.iter().map(|t| t.name.clone()).collect();
assert!(
names.contains(&"EmbeddingPreset".to_string()),
"bx=false entry must keep the original name; got: {names:?}"
);
assert!(
!names.contains(&"EmbeddingPreset2".to_string()),
"bx=false entry must not receive a numeric suffix; got: {names:?}"
);
}
#[test]
fn bx_true_shadow_with_same_path_not_counted_as_collision() {
let mut s = empty_surface();
s.types
.push(make_type_with_bx("EmbeddingPreset", "my_crate::EmbeddingPreset", false));
s.types
.push(make_type_with_bx("EmbeddingPreset", "my_crate::EmbeddingPreset", true));
disambiguate_type_names(&mut s);
let names: Vec<_> = s.types.iter().map(|t| t.name.clone()).collect();
assert!(
names.iter().all(|n| n == "EmbeddingPreset"),
"same-path shadow must not trigger a rename; got: {names:?}"
);
}
}