use crate::core::ir::{ApiSurface, DefaultValue, EnumDef, FieldDef, TypeRef};
use ahash::AHashMap;
use super::SerdeDefaultsByType;
pub(super) fn enum_default_variant_names(enums: &[EnumDef]) -> AHashMap<String, String> {
enums
.iter()
.filter(|enum_def| enum_def.has_default)
.filter_map(|enum_def| {
let variant = enum_def.variants.iter().find(|variant| {
variant.is_default && variant.fields.is_empty() && !variant.originally_had_data_fields
})?;
Some((enum_def.name.clone(), variant.name.clone()))
})
.collect()
}
pub(super) fn resolve_public_default_functions(surface: &mut ApiSurface) {
let public_methods: AHashMap<(String, String), String> = surface
.types
.iter()
.flat_map(|typ| {
typ.methods
.iter()
.filter(|method| method.is_static && method.params.is_empty() && !method.binding_excluded)
.map(|method| {
(
(typ.name.clone(), method.name.clone()),
format!("{}::{}", typ.rust_path.replace('-', "_"), method.name),
)
})
})
.collect();
for typ in &mut surface.types {
for field in &mut typ.fields {
let Some(DefaultValue::FunctionCall(path)) = &field.typed_default else {
continue;
};
let segments: Vec<_> = path.split("::").collect();
let [.., owner, method] = segments.as_slice() else {
continue;
};
if let Some(resolved_path) = public_methods.get(&(owner.to_string(), method.to_string())) {
field.typed_default = Some(DefaultValue::PublicFunctionCall(resolved_path.clone()));
}
}
}
}
pub(super) fn resolve_enum_field_defaults(surface: &mut ApiSurface) {
let enum_default_variants = enum_default_variant_names(&surface.enums);
if enum_default_variants.is_empty() {
return;
}
for typ in &mut surface.types {
for field in &mut typ.fields {
if field.optional {
continue;
}
if !matches!(&field.typed_default, Some(DefaultValue::Empty)) {
continue;
}
let TypeRef::Named(name) = &field.ty else {
continue;
};
if let Some(variant) = enum_default_variants.get(name) {
field.typed_default = Some(DefaultValue::EnumVariant(variant.clone()));
}
}
}
}
fn is_simple_type(ty: &TypeRef) -> bool {
matches!(
ty,
TypeRef::Primitive(_)
| TypeRef::String
| TypeRef::Bytes
| TypeRef::Path
| TypeRef::Unit
| TypeRef::Duration
| TypeRef::Json
)
}
pub(super) fn resolve_newtypes(surface: &mut ApiSurface) {
let newtype_map: AHashMap<String, TypeRef> = surface
.types
.iter()
.filter(|t| t.fields.len() == 1 && t.fields[0].name == "_0" && is_simple_type(&t.fields[0].ty))
.map(|t| (t.name.clone(), t.fields[0].ty.clone()))
.collect();
if newtype_map.is_empty() {
return;
}
let newtype_rust_paths: AHashMap<String, String> = surface
.types
.iter()
.filter(|t| newtype_map.contains_key(&t.name))
.map(|t| (t.name.clone(), t.rust_path.replace('-', "_")))
.collect();
surface.types.retain(|t| !newtype_map.contains_key(&t.name));
for typ in &mut surface.types {
for field in &mut typ.fields {
if let TypeRef::Named(name) = &field.ty
&& let Some(rust_path) = newtype_rust_paths.get(name.as_str())
{
field.newtype_wrapper = Some(rust_path.clone());
}
if let TypeRef::Optional(inner) = &field.ty
&& let TypeRef::Named(name) = inner.as_ref()
&& let Some(rust_path) = newtype_rust_paths.get(name.as_str())
{
field.newtype_wrapper = Some(rust_path.clone());
}
if let TypeRef::Vec(inner) = &field.ty
&& let TypeRef::Named(name) = inner.as_ref()
&& let Some(rust_path) = newtype_rust_paths.get(name.as_str())
{
field.newtype_wrapper = Some(rust_path.clone());
}
resolve_typeref(&newtype_map, &mut field.ty);
}
for method in &mut typ.methods {
for param in &mut method.params {
if let TypeRef::Named(name) = ¶m.ty
&& let Some(rust_path) = newtype_rust_paths.get(name.as_str())
{
param.newtype_wrapper = Some(rust_path.clone());
}
resolve_typeref(&newtype_map, &mut param.ty);
}
if let TypeRef::Named(name) = &method.return_type
&& let Some(rust_path) = newtype_rust_paths.get(name.as_str())
{
method.return_newtype_wrapper = Some(rust_path.clone());
}
resolve_typeref(&newtype_map, &mut method.return_type);
}
}
for func in &mut surface.functions {
for param in &mut func.params {
if let TypeRef::Named(name) = ¶m.ty
&& let Some(rust_path) = newtype_rust_paths.get(name.as_str())
{
param.newtype_wrapper = Some(rust_path.clone());
}
resolve_typeref(&newtype_map, &mut param.ty);
}
if let TypeRef::Named(name) = &func.return_type
&& let Some(rust_path) = newtype_rust_paths.get(name.as_str())
{
func.return_newtype_wrapper = Some(rust_path.clone());
}
resolve_typeref(&newtype_map, &mut func.return_type);
}
for enum_def in &mut surface.enums {
for variant in &mut enum_def.variants {
for field in &mut variant.fields {
resolve_typeref(&newtype_map, &mut field.ty);
}
}
}
}
fn resolve_typeref(newtype_map: &AHashMap<String, TypeRef>, ty: &mut TypeRef) {
match ty {
TypeRef::Named(name) => {
if let Some(inner) = newtype_map.get(name.as_str()) {
*ty = inner.clone();
}
}
TypeRef::Optional(inner) => resolve_typeref(newtype_map, inner),
TypeRef::Vec(inner) => resolve_typeref(newtype_map, inner),
TypeRef::Map(k, v) => {
resolve_typeref(newtype_map, k);
resolve_typeref(newtype_map, v);
}
_ => {}
}
}
pub(super) fn resolve_trait_sources(surface: &mut ApiSurface) {
let mut trait_method_map: AHashMap<String, Vec<(String, String)>> = AHashMap::new();
let mut trait_methods_set: AHashMap<String, Vec<String>> = AHashMap::new();
for typ in &surface.types {
if !typ.is_trait {
continue;
}
let method_names: Vec<String> = typ.methods.iter().map(|m| m.name.clone()).collect();
trait_methods_set.insert(typ.name.clone(), method_names.clone());
for method_name in &method_names {
trait_method_map
.entry(method_name.clone())
.or_default()
.push((typ.name.clone(), typ.rust_path.replace('-', "_")));
}
}
if trait_method_map.is_empty() {
return;
}
for typ in &mut surface.types {
if typ.is_trait {
continue;
}
let unresolved_names: Vec<String> = typ
.methods
.iter()
.filter(|m| m.trait_source.is_none())
.map(|m| m.name.clone())
.collect();
for method in &mut typ.methods {
if method.trait_source.is_some() {
continue;
}
let Some(candidates) = trait_method_map.get(&method.name) else {
continue;
};
if candidates.len() == 1 {
method.trait_source = Some(candidates[0].1.clone());
} else {
let best = candidates.iter().max_by_key(|(trait_name, _)| {
trait_methods_set
.get(trait_name)
.map(|trait_methods| {
trait_methods
.iter()
.filter(|method_name| unresolved_names.contains(method_name))
.count()
})
.unwrap_or(0)
});
if let Some((_, rust_path)) = best {
method.trait_source = Some(rust_path.clone());
}
}
}
}
}
fn is_fully_known(value: &DefaultValue) -> bool {
match value {
DefaultValue::Unresolved(_) | DefaultValue::FunctionCall(_) | DefaultValue::PublicFunctionCall(_) => false,
DefaultValue::BoolLiteral(_)
| DefaultValue::StringLiteral(_)
| DefaultValue::IntLiteral(_)
| DefaultValue::FloatLiteral(_)
| DefaultValue::EnumVariant(_)
| DefaultValue::Empty
| DefaultValue::None => true,
DefaultValue::TupleVariant(_, args) => args.iter().all(is_fully_known),
DefaultValue::StructVariant(_, fields) => fields.iter().all(|(_, value)| is_fully_known(value)),
DefaultValue::ListLiteral(items) => items.iter().all(is_fully_known),
}
}
fn denotes_type_zero(value: &DefaultValue) -> bool {
matches!(
value,
DefaultValue::Empty | DefaultValue::None | DefaultValue::BoolLiteral(false) | DefaultValue::IntLiteral(0)
) || matches!(value, DefaultValue::StringLiteral(s) if s.is_empty())
|| matches!(value, DefaultValue::FloatLiteral(f) if *f == 0.0)
}
fn agrees_via_enum_default(
serde_default: &DefaultValue,
actual_default: &DefaultValue,
field_type: &TypeRef,
enum_default_variants: &AHashMap<String, String>,
) -> bool {
let DefaultValue::EnumVariant(actual_variant) = actual_default else {
return false;
};
if !matches!(serde_default, DefaultValue::Empty) {
return false;
}
let TypeRef::Named(type_name) = field_type else {
return false;
};
enum_default_variants
.get(type_name)
.is_some_and(|default_variant| default_variant == actual_variant)
}
pub(crate) fn warn_on_default_disagreement(
rust_path: &str,
fields: &[FieldDef],
serde_defaults: &AHashMap<String, DefaultValue>,
enum_default_variants: &AHashMap<String, String>,
) {
for field in fields {
let Some(serde_default) = serde_defaults.get(&field.name) else {
continue;
};
let Some(actual_default) = &field.typed_default else {
continue;
};
if !is_fully_known(serde_default) || !is_fully_known(actual_default) {
tracing::debug!(
target: "alef::extract::defaults",
rust_type = rust_path,
field = %field.name,
"field default comparison skipped: serde default or resolved default is not fully known"
);
continue;
}
let agrees = serde_default == actual_default
|| (denotes_type_zero(serde_default) && denotes_type_zero(actual_default))
|| agrees_via_enum_default(serde_default, actual_default, &field.ty, enum_default_variants);
if !agrees {
tracing::warn!(
target: "alef::extract::defaults",
rust_type = rust_path,
field = %field.name,
serde_default = ?serde_default,
resolved_default = ?actual_default,
"field's `#[serde(default)]` value disagrees with its `#[derive(Default)]`/`impl Default` value"
);
}
}
}
pub(super) fn warn_on_default_disagreements(surface: &ApiSurface, pending_serde_defaults: &SerdeDefaultsByType) {
if pending_serde_defaults.is_empty() {
return;
}
let enum_default_variants = enum_default_variant_names(&surface.enums);
for typ in &surface.types {
let Some(serde_defaults) = pending_serde_defaults.get(&typ.rust_path) else {
continue;
};
warn_on_default_disagreement(&typ.rust_path, &typ.fields, serde_defaults, &enum_default_variants);
}
}
#[cfg(test)]
mod default_disagreement_tests {
use super::*;
use tracing_test::traced_test;
fn field(name: &str, typed_default: Option<DefaultValue>) -> FieldDef {
FieldDef {
name: name.to_string(),
typed_default,
..Default::default()
}
}
fn serde_defaults(entries: &[(&str, DefaultValue)]) -> AHashMap<String, DefaultValue> {
entries
.iter()
.map(|(name, value)| ((*name).to_string(), value.clone()))
.collect()
}
#[test]
#[traced_test]
fn genuine_disagreement_between_two_known_values_is_reported() {
let fields = vec![field("retries", Some(DefaultValue::IntLiteral(5)))];
let defaults = serde_defaults(&[("retries", DefaultValue::IntLiteral(3))]);
warn_on_default_disagreement("my_crate::Config", &fields, &defaults, &AHashMap::new());
assert!(logs_contain("my_crate::Config"));
assert!(logs_contain("retries"));
assert!(logs_contain("IntLiteral(3)"));
assert!(logs_contain("IntLiteral(5)"));
}
#[test]
#[traced_test]
fn derived_defaults_empty_disagreeing_with_a_known_serde_default_is_reported() {
let fields = vec![field("host", Some(DefaultValue::Empty))];
let defaults = serde_defaults(&[("host", DefaultValue::StringLiteral("localhost".to_string()))]);
warn_on_default_disagreement("my_crate::Client", &fields, &defaults, &AHashMap::new());
assert!(logs_contain("my_crate::Client"));
assert!(logs_contain("host"));
assert!(logs_contain("StringLiteral(\"localhost\")"));
assert!(logs_contain("Empty"));
}
#[test]
#[traced_test]
fn agreeing_known_defaults_are_not_reported() {
let fields = vec![field("retries", Some(DefaultValue::IntLiteral(3)))];
let defaults = serde_defaults(&[("retries", DefaultValue::IntLiteral(3))]);
warn_on_default_disagreement("my_crate::Config", &fields, &defaults, &AHashMap::new());
assert!(!logs_contain("disagrees"));
}
#[test]
#[traced_test]
fn matching_empty_defaults_on_both_sides_are_not_reported() {
let fields = vec![field("count", Some(DefaultValue::Empty))];
let defaults = serde_defaults(&[("count", DefaultValue::Empty)]);
warn_on_default_disagreement("my_crate::Config", &fields, &defaults, &AHashMap::new());
assert!(!logs_contain("disagrees"));
}
fn enum_typed_field(name: &str, typed_default: Option<DefaultValue>, enum_type_name: &str) -> FieldDef {
FieldDef {
name: name.to_string(),
typed_default,
ty: TypeRef::Named(enum_type_name.to_string()),
..Default::default()
}
}
fn enum_default_variants(entries: &[(&str, &str)]) -> AHashMap<String, String> {
entries
.iter()
.map(|(enum_name, variant_name)| ((*enum_name).to_string(), (*variant_name).to_string()))
.collect()
}
#[test]
#[traced_test]
fn empty_serde_default_agreeing_with_the_enum_default_variant_is_not_reported() {
let fields = vec![enum_typed_field(
"tier_strategy",
Some(DefaultValue::EnumVariant("Auto".to_string())),
"TierStrategy",
)];
let defaults = serde_defaults(&[("tier_strategy", DefaultValue::Empty)]);
let enums = enum_default_variants(&[("TierStrategy", "Auto")]);
warn_on_default_disagreement("my_crate::ConversionOptions", &fields, &defaults, &enums);
assert!(!logs_contain("disagrees"));
}
#[test]
#[traced_test]
fn empty_serde_default_disagreeing_with_a_non_default_enum_variant_is_reported() {
let fields = vec![enum_typed_field(
"tier_strategy",
Some(DefaultValue::EnumVariant("Tier2".to_string())),
"TierStrategy",
)];
let defaults = serde_defaults(&[("tier_strategy", DefaultValue::Empty)]);
let enums = enum_default_variants(&[("TierStrategy", "Auto")]);
warn_on_default_disagreement("my_crate::ConversionOptions", &fields, &defaults, &enums);
assert!(logs_contain("disagrees"));
assert!(logs_contain("tier_strategy"));
}
#[test]
#[traced_test]
fn empty_serde_default_against_an_unknown_enum_is_reported_conservatively() {
let fields = vec![enum_typed_field(
"tier_strategy",
Some(DefaultValue::EnumVariant("Auto".to_string())),
"TierStrategy",
)];
let defaults = serde_defaults(&[("tier_strategy", DefaultValue::Empty)]);
warn_on_default_disagreement("my_crate::ConversionOptions", &fields, &defaults, &AHashMap::new());
assert!(logs_contain("disagrees"));
}
#[test]
#[traced_test]
fn unresolved_resolved_default_is_not_reported() {
let fields = vec![field(
"level",
Some(DefaultValue::Unresolved("Self::builder().level(9).build()".to_string())),
)];
let defaults = serde_defaults(&[("level", DefaultValue::IntLiteral(9))]);
warn_on_default_disagreement("my_crate::Cfg", &fields, &defaults, &AHashMap::new());
assert!(!logs_contain("disagrees"));
}
#[test]
#[traced_test]
fn unresolved_serde_default_is_not_reported() {
let fields = vec![field("level", Some(DefaultValue::IntLiteral(9)))];
let defaults = serde_defaults(&[("level", DefaultValue::Unresolved("compute_default()".to_string()))]);
warn_on_default_disagreement("my_crate::Cfg", &fields, &defaults, &AHashMap::new());
assert!(!logs_contain("disagrees"));
}
#[test]
#[traced_test]
fn nested_unresolved_inside_a_tuple_variant_is_not_reported() {
let fields = vec![field(
"mode",
Some(DefaultValue::TupleVariant(
"Custom".to_string(),
vec![DefaultValue::Unresolved("compute()".to_string())],
)),
)];
let defaults = serde_defaults(&[(
"mode",
DefaultValue::TupleVariant("Custom".to_string(), vec![DefaultValue::IntLiteral(5)]),
)]);
warn_on_default_disagreement("my_crate::Cfg", &fields, &defaults, &AHashMap::new());
assert!(!logs_contain("disagrees"));
}
#[test]
#[traced_test]
fn nested_unresolved_inside_a_struct_variant_is_not_reported() {
let fields = vec![field(
"kind",
Some(DefaultValue::StructVariant(
"Curated".to_string(),
vec![("label".to_string(), DefaultValue::Unresolved("compute()".to_string()))],
)),
)];
let defaults = serde_defaults(&[(
"kind",
DefaultValue::StructVariant(
"Curated".to_string(),
vec![("label".to_string(), DefaultValue::StringLiteral("balanced".to_string()))],
),
)]);
warn_on_default_disagreement("my_crate::Cfg", &fields, &defaults, &AHashMap::new());
assert!(!logs_contain("disagrees"));
}
#[test]
#[traced_test]
fn function_call_default_is_not_compared_even_against_a_differing_literal() {
let fields = vec![field("token", Some(DefaultValue::StringLiteral("abc".to_string())))];
let defaults = serde_defaults(&[("token", DefaultValue::FunctionCall("generate_token".to_string()))]);
warn_on_default_disagreement("my_crate::Auth", &fields, &defaults, &AHashMap::new());
assert!(!logs_contain("disagrees"));
}
#[test]
#[traced_test]
fn field_absent_from_serde_defaults_is_skipped() {
let fields = vec![field("untouched", Some(DefaultValue::IntLiteral(1)))];
let defaults = serde_defaults(&[]);
warn_on_default_disagreement("my_crate::Cfg", &fields, &defaults, &AHashMap::new());
assert!(!logs_contain("disagrees"));
}
#[test]
#[traced_test]
fn a_zero_valued_serde_default_matching_the_rust_default_does_not_warn() {
let fields = vec![field("count", Some(DefaultValue::IntLiteral(0)))];
let defaults = serde_defaults(&[("count", DefaultValue::Empty)]);
warn_on_default_disagreement("my_crate::Cfg", &fields, &defaults, &AHashMap::new());
assert!(!logs_contain("disagrees"));
}
#[test]
#[traced_test]
fn zero_value_equivalence_across_default_value_spellings_is_evaluated_correctly() {
struct Case {
type_name: &'static str,
serde_default: DefaultValue,
actual_default: DefaultValue,
should_warn: bool,
}
let cases = [
Case {
type_name: "case::EmptySerdeZeroIntActual",
serde_default: DefaultValue::Empty,
actual_default: DefaultValue::IntLiteral(0),
should_warn: false,
},
Case {
type_name: "case::EmptySerdeZeroFloatActual",
serde_default: DefaultValue::Empty,
actual_default: DefaultValue::FloatLiteral(0.0),
should_warn: false,
},
Case {
type_name: "case::EmptySerdeFalseActual",
serde_default: DefaultValue::Empty,
actual_default: DefaultValue::BoolLiteral(false),
should_warn: false,
},
Case {
type_name: "case::EmptySerdeEmptyStringActual",
serde_default: DefaultValue::Empty,
actual_default: DefaultValue::StringLiteral(String::new()),
should_warn: false,
},
Case {
type_name: "case::EmptySerdeNoneActual",
serde_default: DefaultValue::Empty,
actual_default: DefaultValue::None,
should_warn: false,
},
Case {
type_name: "case::IntZeroSerdeEmptyActual",
serde_default: DefaultValue::IntLiteral(0),
actual_default: DefaultValue::Empty,
should_warn: false,
},
Case {
type_name: "case::EmptySerdeNonZeroIntActual",
serde_default: DefaultValue::Empty,
actual_default: DefaultValue::IntLiteral(5),
should_warn: true,
},
Case {
type_name: "case::NonZeroIntSerdeNonZeroIntActual",
serde_default: DefaultValue::IntLiteral(3),
actual_default: DefaultValue::IntLiteral(5),
should_warn: true,
},
Case {
type_name: "case::EmptySerdeNonEmptyStringActual",
serde_default: DefaultValue::Empty,
actual_default: DefaultValue::StringLiteral("localhost".to_string()),
should_warn: true,
},
];
for case in cases {
let fields = vec![field("value", Some(case.actual_default.clone()))];
let defaults = serde_defaults(&[("value", case.serde_default.clone())]);
warn_on_default_disagreement(case.type_name, &fields, &defaults, &AHashMap::new());
let warned = logs_contain(case.type_name) && logs_contain("disagrees");
assert_eq!(
warned, case.should_warn,
"case {} (serde={:?}, actual={:?}) expected should_warn={} but got {}",
case.type_name, case.serde_default, case.actual_default, case.should_warn, warned
);
}
}
}