use crate::core::ir::{
ApiSurface, CoreWrapper, NewtypeConversion, NewtypeWrapperMetadata, ReceiverKind, TypeDef, TypeRef,
UnsupportedPublicItem,
};
use ahash::{AHashMap, AHashSet};
use super::ResolvedNewtype;
pub(super) type NewtypeCandidates = AHashMap<String, Vec<(String, TypeRef, ResolvedNewtype)>>;
pub(super) type NewtypeMap = AHashMap<String, (TypeRef, ResolvedNewtype)>;
pub(super) fn extracted_named_paths(surface: &ApiSurface) -> AHashMap<String, Vec<String>> {
let mut paths: AHashMap<String, Vec<String>> = AHashMap::new();
for typ in &surface.types {
paths.entry(typ.name.clone()).or_default().push(typ.rust_path.clone());
}
for enum_def in &surface.enums {
paths
.entry(enum_def.name.clone())
.or_default()
.push(enum_def.rust_path.clone());
}
for error in &surface.errors {
paths
.entry(error.name.clone())
.or_default()
.push(error.rust_path.clone());
}
paths
.into_iter()
.map(|(name, paths)| {
let mut paths = paths;
paths.sort_unstable();
(name, paths)
})
.collect()
}
pub(super) fn select_unambiguous_candidates(
candidates: NewtypeCandidates,
extracted_names: &AHashMap<String, Vec<String>>,
diagnostics: &mut Vec<UnsupportedPublicItem>,
) -> (NewtypeMap, AHashSet<String>) {
let mut newtype_map = AHashMap::new();
let mut resolved_paths = AHashSet::new();
for (name, matching) in candidates {
let extracted_paths = extracted_names.get(&name).map(Vec::as_slice).unwrap_or_default();
let mut unique_paths = extracted_paths.to_vec();
unique_paths.sort_unstable();
unique_paths.dedup();
if let [rust_path] = unique_paths.as_slice() {
if matching.len() != extracted_paths.len()
|| !matching
.iter()
.all(|(candidate_path, _, _)| candidate_path == rust_path)
{
diagnostics.push(incompatible_cfg_definitions(rust_path));
continue;
}
let (_, inner, wrapper) = &matching[0];
if matching
.iter()
.all(|(_, candidate_inner, candidate_wrapper)| candidate_inner == inner && candidate_wrapper == wrapper)
{
resolved_paths.insert(rust_path.clone());
newtype_map.insert(name, (inner.clone(), wrapper.clone()));
continue;
}
diagnostics.push(incompatible_cfg_definitions(rust_path));
continue;
}
let reason = format!(
"ambiguous transparent newtype name `{name}` resolves to multiple Rust paths: {}",
unique_paths.join(", ")
);
let mut reported_paths = AHashSet::new();
for (rust_path, _, _) in matching {
if !reported_paths.insert(rust_path.clone()) {
continue;
}
diagnostics.push(UnsupportedPublicItem {
item_kind: "struct".to_string(),
item_path: rust_path,
reason: reason.clone(),
suggested_fix: "rename one wrapper so every binding-visible newtype has a unique short name"
.to_string(),
});
}
}
(newtype_map, resolved_paths)
}
fn incompatible_cfg_definitions(rust_path: &str) -> UnsupportedPublicItem {
UnsupportedPublicItem {
item_kind: "struct".to_string(),
item_path: rust_path.to_string(),
reason: format!("incompatible transparent newtype definitions for Rust path `{rust_path}` across cfg branches"),
suggested_fix: "use the same inner type and transparent_string conversion methods in every cfg branch"
.to_string(),
}
}
pub(super) fn validate_transparent_string_methods(
typ: &TypeDef,
metadata: &[NewtypeWrapperMetadata],
) -> Result<(), String> {
let [metadata] = metadata else {
return Err("transparent_string wrapper metadata must contain exactly one conversion path".to_string());
};
let NewtypeConversion::TransparentString { from, into, .. } = &metadata.conversion else {
return Err("transparent_string wrapper metadata has the wrong conversion kind".to_string());
};
validate_from_method(typ, from)?;
validate_into_method(typ, into)
}
fn validate_from_method(typ: &TypeDef, from: &str) -> Result<(), String> {
let methods: Vec<_> = typ.methods.iter().filter(|method| method.name == from).collect();
if methods.len() != 1 {
return Err(format!(
"transparent_string requires exactly one public `{from}(String) -> Self` method"
));
}
let method = methods[0];
let returns_self = matches!(&method.return_type, TypeRef::Named(name) if name == "Self" || name == &typ.name);
if method.is_static
&& !method.is_async
&& method.error_type.is_none()
&& method.params.len() == 1
&& !method.params[0].optional
&& !method.params[0].is_ref
&& !method.params[0].is_mut
&& method.params[0].core_wrapper == CoreWrapper::None
&& method.params[0].ty == TypeRef::String
&& returns_self
&& !method.returns_ref
&& !method.returns_cow
&& method.trait_source.is_none()
&& !method.binding_excluded
{
return Ok(());
}
Err(format!(
"transparent_string `{from}` must be a public synchronous `fn(String) -> Self`"
))
}
fn validate_into_method(typ: &TypeDef, into: &str) -> Result<(), String> {
let methods: Vec<_> = typ.methods.iter().filter(|method| method.name == into).collect();
if methods.len() != 1 {
return Err(format!(
"transparent_string requires exactly one public `{into}(self) -> String` method"
));
}
let method = methods[0];
if !method.is_static
&& !method.is_async
&& method.error_type.is_none()
&& method.receiver == Some(ReceiverKind::Owned)
&& method.params.is_empty()
&& method.return_type == TypeRef::String
&& !method.returns_ref
&& !method.returns_cow
&& method.trait_source.is_none()
&& !method.binding_excluded
{
return Ok(());
}
Err(format!(
"transparent_string `{into}` must be a public synchronous `fn(self) -> String`"
))
}