use super::helpers::is_test_gated;
use crate::core::ir::{DefaultValue, FieldDef, TypeRef};
use ahash::AHashMap;
use quote::ToTokens;
use syn;
pub(crate) type ConstructorIndex<'a> = AHashMap<(String, String), &'a syn::ImplItemFn>;
const MAX_DELEGATION_DEPTH: usize = 4;
pub(crate) fn extract_default_values(
item: &syn::ItemImpl,
self_type: &str,
fields: &mut [FieldDef],
literal_consts: &AHashMap<String, DefaultValue>,
constructors: &ConstructorIndex<'_>,
) {
let default_fn = item.items.iter().find_map(|impl_item| {
if let syn::ImplItem::Fn(method) = impl_item
&& method.sig.ident == "default"
{
return Some(method);
}
None
});
let Some(default_fn) = default_fn else {
mark_unresolved(fields, "impl Default block without a `fn default()` item");
return;
};
let field_types: AHashMap<String, TypeRef> = fields
.iter()
.map(|field| (field.name.clone(), field.ty.clone()))
.collect();
let scope = EvalScope::new(self_type, literal_consts, &field_types);
let defaults = if let Some(struct_expr) = find_struct_expr(&default_fn.block) {
struct_expr_defaults(struct_expr, &scope)
} else if let Some(delegated) = follow_delegation(&default_fn.block, self_type, constructors, &scope, 0) {
delegated
} else if let Some(single_field) = single_field_const_tail_default(&default_fn.block, fields, &scope) {
single_field
} else {
let body = default_fn.block.to_token_stream().to_string();
tracing::warn!(
target: "alef::extract::defaults",
rust_type = self_type,
body = %body,
"`impl Default` body is neither a struct literal nor a constant-foldable delegation; \
field defaults are unresolved"
);
mark_unresolved(fields, &body);
return;
};
for field in fields.iter_mut() {
if let Some(default_val) = defaults.get(&field.name) {
field.typed_default = Some(default_val.clone());
} else {
field.typed_default = Some(DefaultValue::Empty);
}
}
}
fn mark_unresolved(fields: &mut [FieldDef], body: &str) {
for field in fields.iter_mut() {
field.typed_default = Some(DefaultValue::Unresolved(body.to_string()));
}
}
pub(crate) fn collect_constructors(items: &[syn::Item]) -> ConstructorIndex<'_> {
let mut index = ConstructorIndex::new();
for item in items {
let syn::Item::Impl(item_impl) = item else {
continue;
};
if item_impl.trait_.is_some() || is_test_gated(&item_impl.attrs) {
continue;
}
let Some(type_name) = path_type_name(&item_impl.self_ty) else {
continue;
};
for impl_item in &item_impl.items {
let syn::ImplItem::Fn(method) = impl_item else {
continue;
};
if matches!(method.sig.inputs.first(), Some(syn::FnArg::Receiver(_))) {
continue;
}
index.insert((type_name.clone(), method.sig.ident.to_string()), method);
}
}
index
}
fn path_type_name(ty: &syn::Type) -> Option<String> {
match ty {
syn::Type::Path(path) => path.path.segments.last().map(|segment| segment.ident.to_string()),
_ => None,
}
}
pub(crate) fn collect_literal_consts(items: &[syn::Item]) -> AHashMap<String, DefaultValue> {
let mut consts = AHashMap::new();
for item in items {
match item {
syn::Item::Const(item_const) => {
if let Some(value) = const_literal_value(&item_const.expr) {
consts.insert(item_const.ident.to_string(), value);
}
}
syn::Item::Impl(item_impl) if item_impl.trait_.is_none() && !is_test_gated(&item_impl.attrs) => {
let Some(type_name) = path_type_name(&item_impl.self_ty) else {
continue;
};
for impl_item in &item_impl.items {
if let syn::ImplItem::Const(assoc_const) = impl_item
&& let Some(value) = const_literal_value(&assoc_const.expr)
{
consts.insert(format!("{type_name}::{}", assoc_const.ident), value);
}
}
}
_ => {}
}
}
consts
}
fn const_literal_value(expr: &syn::Expr) -> Option<DefaultValue> {
match expr {
syn::Expr::Lit(lit) => match &lit.lit {
syn::Lit::Str(s) => Some(DefaultValue::StringLiteral(s.value())),
syn::Lit::Char(c) => Some(DefaultValue::StringLiteral(c.value().to_string())),
syn::Lit::Bool(b) => Some(DefaultValue::BoolLiteral(b.value)),
syn::Lit::Int(i) => i.base10_parse::<i64>().ok().map(DefaultValue::IntLiteral),
syn::Lit::Float(f) => f.base10_parse::<f64>().ok().map(DefaultValue::FloatLiteral),
_ => None,
},
syn::Expr::Unary(unary) if matches!(unary.op, syn::UnOp::Neg(_)) => match const_literal_value(&unary.expr)? {
DefaultValue::IntLiteral(v) => Some(DefaultValue::IntLiteral(-v)),
DefaultValue::FloatLiteral(v) => Some(DefaultValue::FloatLiteral(-v)),
_ => None,
},
syn::Expr::Call(call) if call.args.len() == 1 => {
let syn::Expr::Path(path) = &*call.func else {
return None;
};
let name = path.path.segments.last()?.ident.to_string();
if !name.starts_with(|c: char| c.is_ascii_uppercase()) {
return None;
}
const_literal_value(call.args.first()?)
}
_ => None,
}
}
struct EvalScope<'a> {
self_type: &'a str,
literal_consts: &'a AHashMap<String, DefaultValue>,
field_types: &'a AHashMap<String, TypeRef>,
params: AHashMap<String, DefaultValue>,
}
impl<'a> EvalScope<'a> {
fn new(
self_type: &'a str,
literal_consts: &'a AHashMap<String, DefaultValue>,
field_types: &'a AHashMap<String, TypeRef>,
) -> Self {
Self {
self_type,
literal_consts,
field_types,
params: AHashMap::new(),
}
}
fn with_params(&self, params: AHashMap<String, DefaultValue>) -> EvalScope<'a> {
EvalScope {
self_type: self.self_type,
literal_consts: self.literal_consts,
field_types: self.field_types,
params,
}
}
fn associated_const(&self, owner: &str, name: &str) -> Option<DefaultValue> {
let owner = if owner == "Self" { self.self_type } else { owner };
self.literal_consts.get(&format!("{owner}::{name}")).cloned()
}
}
fn carries_value(value: &DefaultValue) -> bool {
matches!(
value,
DefaultValue::BoolLiteral(_)
| DefaultValue::StringLiteral(_)
| DefaultValue::IntLiteral(_)
| DefaultValue::FloatLiteral(_)
| DefaultValue::EnumVariant(_)
| DefaultValue::TupleVariant(_, _)
| DefaultValue::StructVariant(_, _)
| DefaultValue::ListLiteral(_)
)
}
fn follow_delegation(
block: &syn::Block,
self_type: &str,
constructors: &ConstructorIndex<'_>,
scope: &EvalScope<'_>,
depth: usize,
) -> Option<AHashMap<String, DefaultValue>> {
if depth >= MAX_DELEGATION_DEPTH {
return None;
}
let call = tail_call_expr(block)?;
let syn::Expr::Path(path) = &*call.func else {
return None;
};
let segments: Vec<String> = path.path.segments.iter().map(|s| s.ident.to_string()).collect();
let [owner, fn_name] = segments.as_slice() else {
return None;
};
if owner.as_str() != "Self" && owner.as_str() != self_type {
return None;
}
if fn_name.as_str() == "default" {
return None;
}
let target = constructors.get(&(self_type.to_string(), fn_name.clone()))?;
let mut params = AHashMap::new();
let mut arguments = call.args.iter();
for input in &target.sig.inputs {
let syn::FnArg::Typed(pat_type) = input else {
return None;
};
let argument = arguments.next()?;
let syn::Pat::Ident(pat_ident) = pat_type.pat.as_ref() else {
continue;
};
let value = expr_to_default_value(argument, scope, None);
if carries_value(&value) {
params.insert(pat_ident.ident.to_string(), value);
}
}
if arguments.next().is_some() {
return None;
}
let inner = scope.with_params(params);
if let Some(struct_expr) = find_struct_expr(&target.block) {
return Some(struct_expr_defaults(struct_expr, &inner));
}
follow_delegation(&target.block, self_type, constructors, &inner, depth + 1)
}
fn tail_call_expr(block: &syn::Block) -> Option<&syn::ExprCall> {
match block.stmts.last()? {
syn::Stmt::Expr(expr, _) => unwrap_to_call_expr(expr),
_ => None,
}
}
fn unwrap_to_call_expr(expr: &syn::Expr) -> Option<&syn::ExprCall> {
match expr {
syn::Expr::Call(call) => Some(call),
syn::Expr::Block(b) => tail_call_expr(&b.block),
syn::Expr::Return(ret) => ret.expr.as_deref().and_then(unwrap_to_call_expr),
_ => None,
}
}
fn single_field_const_tail_default(
block: &syn::Block,
fields: &[FieldDef],
scope: &EvalScope<'_>,
) -> Option<AHashMap<String, DefaultValue>> {
let [field] = fields else {
return None;
};
let path = tail_path_expr(block)?;
let segments: Vec<String> = path.path.segments.iter().map(|s| s.ident.to_string()).collect();
let [owner, name] = segments.as_slice() else {
return None;
};
let value = scope.associated_const(owner, name)?;
let mut defaults = AHashMap::new();
defaults.insert(field.name.clone(), value);
Some(defaults)
}
fn tail_path_expr(block: &syn::Block) -> Option<&syn::ExprPath> {
match block.stmts.last()? {
syn::Stmt::Expr(expr, _) => unwrap_to_path_expr(expr),
_ => None,
}
}
fn unwrap_to_path_expr(expr: &syn::Expr) -> Option<&syn::ExprPath> {
match expr {
syn::Expr::Path(path) => Some(path),
syn::Expr::Block(b) => tail_path_expr(&b.block),
syn::Expr::Return(ret) => ret.expr.as_deref().and_then(unwrap_to_path_expr),
_ => None,
}
}
fn struct_expr_defaults(struct_expr: &syn::ExprStruct, scope: &EvalScope<'_>) -> AHashMap<String, DefaultValue> {
let mut defaults = AHashMap::new();
for field in &struct_expr.fields {
let Some(ident) = &field.member_named() else {
continue;
};
let name = ident.to_string();
let value = expr_to_default_value(&field.expr, scope, scope.field_types.get(&name));
if let DefaultValue::Unresolved(source) = &value {
tracing::debug!(
target: "alef::extract::defaults",
rust_type = scope.self_type,
field = %name,
initializer = %source,
"field initializer is not constant-foldable; its default is unresolved"
);
}
defaults.insert(name, value);
}
defaults
}
fn find_struct_expr(block: &syn::Block) -> Option<&syn::ExprStruct> {
for stmt in block.stmts.iter().rev() {
match stmt {
syn::Stmt::Expr(expr, _) => {
if let Some(s) = unwrap_to_struct_expr(expr) {
return Some(s);
}
}
syn::Stmt::Local(local) => {
if let Some(init) = &local.init
&& let Some(s) = unwrap_to_struct_expr(&init.expr)
{
return Some(s);
}
}
_ => {}
}
}
None
}
fn unwrap_to_struct_expr(expr: &syn::Expr) -> Option<&syn::ExprStruct> {
match expr {
syn::Expr::Struct(s) => Some(s),
syn::Expr::Block(b) => find_struct_expr(&b.block),
_ => None,
}
}
trait FieldMemberExt {
fn member_named(&self) -> Option<&syn::Ident>;
}
impl FieldMemberExt for syn::FieldValue {
fn member_named(&self) -> Option<&syn::Ident> {
match &self.member {
syn::Member::Named(ident) => Some(ident),
syn::Member::Unnamed(_) => None,
}
}
}
fn unreadable(expr: &syn::Expr) -> DefaultValue {
DefaultValue::Unresolved(expr.to_token_stream().to_string())
}
fn admits_enum_variant(field_ty: Option<&TypeRef>) -> bool {
match field_ty {
None | Some(TypeRef::Named(_)) => true,
Some(TypeRef::Optional(inner) | TypeRef::Vec(inner)) => admits_enum_variant(Some(&**inner)),
Some(_) => false,
}
}
fn expr_to_default_value(expr: &syn::Expr, scope: &EvalScope<'_>, field_ty: Option<&TypeRef>) -> DefaultValue {
match expr {
syn::Expr::Lit(lit) => match &lit.lit {
syn::Lit::Bool(b) => DefaultValue::BoolLiteral(b.value),
syn::Lit::Int(i) => {
if let Ok(val) = i.base10_parse::<i64>() {
DefaultValue::IntLiteral(val)
} else {
unreadable(expr)
}
}
syn::Lit::Float(f) => {
if let Ok(val) = f.base10_parse::<f64>() {
DefaultValue::FloatLiteral(val)
} else {
unreadable(expr)
}
}
syn::Lit::Char(c) => DefaultValue::StringLiteral(c.value().to_string()),
syn::Lit::Str(s) => DefaultValue::StringLiteral(s.value()),
_ => unreadable(expr),
},
syn::Expr::Reference(syn::ExprReference { expr: inner, .. })
| syn::Expr::Paren(syn::ExprParen { expr: inner, .. })
| syn::Expr::Group(syn::ExprGroup { expr: inner, .. }) => expr_to_default_value(inner, scope, field_ty),
syn::Expr::Unary(unary) if matches!(unary.op, syn::UnOp::Neg(_)) => {
match expr_to_default_value(&unary.expr, scope, field_ty) {
DefaultValue::IntLiteral(v) => DefaultValue::IntLiteral(-v),
DefaultValue::FloatLiteral(v) => DefaultValue::FloatLiteral(-v),
_ => unreadable(expr),
}
}
syn::Expr::Binary(bin) => {
let lhs = expr_to_default_value(&bin.left, scope, field_ty);
let rhs = expr_to_default_value(&bin.right, scope, field_ty);
match (lhs, rhs) {
(DefaultValue::IntLiteral(a), DefaultValue::IntLiteral(b)) => match bin.op {
syn::BinOp::Add(_) => a
.checked_add(b)
.map(DefaultValue::IntLiteral)
.unwrap_or_else(|| unreadable(expr)),
syn::BinOp::Sub(_) => a
.checked_sub(b)
.map(DefaultValue::IntLiteral)
.unwrap_or_else(|| unreadable(expr)),
syn::BinOp::Mul(_) => a
.checked_mul(b)
.map(DefaultValue::IntLiteral)
.unwrap_or_else(|| unreadable(expr)),
syn::BinOp::Div(_) if b != 0 => DefaultValue::IntLiteral(a / b),
syn::BinOp::Rem(_) if b != 0 => DefaultValue::IntLiteral(a % b),
syn::BinOp::Shl(_) if (0..63).contains(&b) => a
.checked_shl(b as u32)
.map(DefaultValue::IntLiteral)
.unwrap_or_else(|| unreadable(expr)),
syn::BinOp::Shr(_) if (0..63).contains(&b) => DefaultValue::IntLiteral(a >> (b as u32)),
syn::BinOp::BitOr(_) => DefaultValue::IntLiteral(a | b),
syn::BinOp::BitAnd(_) => DefaultValue::IntLiteral(a & b),
syn::BinOp::BitXor(_) => DefaultValue::IntLiteral(a ^ b),
_ => unreadable(expr),
},
(DefaultValue::FloatLiteral(a), DefaultValue::FloatLiteral(b)) => match bin.op {
syn::BinOp::Add(_) => DefaultValue::FloatLiteral(a + b),
syn::BinOp::Sub(_) => DefaultValue::FloatLiteral(a - b),
syn::BinOp::Mul(_) => DefaultValue::FloatLiteral(a * b),
syn::BinOp::Div(_) if b != 0.0 => DefaultValue::FloatLiteral(a / b),
_ => unreadable(expr),
},
_ => unreadable(expr),
}
}
syn::Expr::MethodCall(mc) => {
let method_name = mc.method.to_string();
match method_name.as_str() {
"to_string" | "to_owned" | "into" => {
if let syn::Expr::Lit(lit) = &*mc.receiver
&& let syn::Lit::Str(s) = &lit.lit
{
return DefaultValue::StringLiteral(s.value());
}
match resolve_ident(&mc.receiver, scope) {
Some(value @ DefaultValue::StringLiteral(_)) => value,
Some(value) if method_name == "into" => value,
_ => unreadable(expr),
}
}
_ => unreadable(expr),
}
}
syn::Expr::Call(call) => {
if let syn::Expr::Path(path) = &*call.func {
let segments: Vec<String> = path.path.segments.iter().map(|s| s.ident.to_string()).collect();
if (segments == ["Some"] || segments == ["Option", "Some"])
&& call.args.len() == 1
&& let Some(inner) = call.args.first()
{
return expr_to_default_value(inner, scope, field_ty);
}
if segments == ["String", "from"] && call.args.len() == 1 {
if let Some(syn::Expr::Lit(lit)) = call.args.first()
&& let syn::Lit::Str(s) = &lit.lit
{
return DefaultValue::StringLiteral(s.value());
}
if let Some(argument) = call.args.first()
&& let Some(value @ DefaultValue::StringLiteral(_)) = resolve_ident(argument, scope)
{
return value;
}
return unreadable(expr);
}
if segments == ["String", "new"] && call.args.is_empty() {
return DefaultValue::StringLiteral(String::new());
}
if let [.., owner, variant] = segments.as_slice()
&& owner == "Cow"
&& matches!(variant.as_str(), "Borrowed" | "Owned")
&& call.args.len() == 1
&& let Some(inner) = call.args.first()
{
return match expr_to_default_value(inner, scope, field_ty) {
DefaultValue::Unresolved(_) | DefaultValue::FunctionCall(_) => unreadable(expr),
resolved => resolved,
};
}
if segments.len() == 2 && segments[1] == "new" && call.args.is_empty() {
let type_name = &segments[0];
if matches!(
type_name.as_str(),
"Vec" | "HashMap" | "HashSet" | "BTreeMap" | "BTreeSet" | "AHashMap" | "AHashSet"
) {
return DefaultValue::Empty;
}
}
if segments == ["Duration", "from_secs"] && call.args.len() == 1 {
if let Some(syn::Expr::Lit(lit)) = call.args.first()
&& let syn::Lit::Int(i) = &lit.lit
&& let Ok(val) = i.base10_parse::<i64>()
{
return DefaultValue::IntLiteral(val * 1000);
}
return unreadable(expr);
}
if segments == ["Duration", "from_millis"] && call.args.len() == 1 {
if let Some(syn::Expr::Lit(lit)) = call.args.first()
&& let syn::Lit::Int(i) = &lit.lit
&& let Ok(val) = i.base10_parse::<i64>()
{
return DefaultValue::IntLiteral(val);
}
return unreadable(expr);
}
if segments.last().is_some_and(|s| s == "default") {
return DefaultValue::Empty;
}
if !call.args.is_empty()
&& let Some(variant) = segments.last()
&& variant.starts_with(|c: char| c.is_ascii_uppercase())
{
let mut values = Vec::with_capacity(call.args.len());
for argument in &call.args {
let value = expr_to_default_value(argument, scope, None);
if !carries_value(&value) {
return unreadable(expr);
}
values.push(value);
}
return DefaultValue::TupleVariant(variant.clone(), values);
}
if call.args.is_empty() {
return DefaultValue::FunctionCall(segments.join("::"));
}
}
unreadable(expr)
}
syn::Expr::Struct(struct_expr) => {
if struct_expr.rest.is_some() {
return unreadable(expr);
}
let Some(variant) = struct_expr.path.segments.last().map(|s| s.ident.to_string()) else {
return unreadable(expr);
};
let mut fields = Vec::with_capacity(struct_expr.fields.len());
for field_value in &struct_expr.fields {
let Some(name) = field_value.member_named() else {
return unreadable(expr);
};
let value = expr_to_default_value(&field_value.expr, scope, None);
if !carries_value(&value) {
return unreadable(expr);
}
fields.push((name.to_string(), value));
}
DefaultValue::StructVariant(variant, fields)
}
syn::Expr::Path(path) => {
if let Some(value) = resolve_ident(expr, scope) {
return value;
}
let segments: Vec<String> = path.path.segments.iter().map(|s| s.ident.to_string()).collect();
if segments.len() == 1 && segments[0] == "None" {
return DefaultValue::None;
}
if segments.len() >= 2
&& admits_enum_variant(field_ty)
&& let Some(name) = segments.last()
{
return DefaultValue::EnumVariant(name.clone());
}
unreadable(expr)
}
syn::Expr::Macro(mac) => {
let macro_name = mac
.mac
.path
.segments
.last()
.map(|s| s.ident.to_string())
.unwrap_or_default();
if !matches!(macro_name.as_str(), "vec" | "hashmap" | "hashset") {
return unreadable(expr);
}
if mac.mac.tokens.is_empty() {
return DefaultValue::Empty;
}
if macro_name != "vec" {
return unreadable(expr);
}
let Ok(elements) = mac
.mac
.parse_body_with(syn::punctuated::Punctuated::<syn::Expr, syn::Token![,]>::parse_terminated)
else {
return unreadable(expr);
};
if elements.is_empty() {
return DefaultValue::Empty;
}
let mut lowered = Vec::with_capacity(elements.len());
for element in &elements {
let value = expr_to_default_value(element, scope, field_ty);
if !carries_value(&value) {
return unreadable(expr);
}
lowered.push(value);
}
DefaultValue::ListLiteral(lowered)
}
_ => unreadable(expr),
}
}
fn resolve_ident(expr: &syn::Expr, scope: &EvalScope<'_>) -> Option<DefaultValue> {
let syn::Expr::Path(path) = expr else {
return None;
};
let segments: Vec<String> = path.path.segments.iter().map(|s| s.ident.to_string()).collect();
match segments.as_slice() {
[ident] => {
if let Some(value) = scope.params.get(ident) {
return Some(value.clone());
}
scope.literal_consts.get(ident).cloned()
}
[.., owner, name] => scope.associated_const(owner, name),
[] => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn default_value_of(expr_src: &str) -> DefaultValue {
default_value_of_with_consts(expr_src, &[])
}
fn default_value_of_with_consts(expr_src: &str, consts: &[(&str, &str)]) -> DefaultValue {
let expr: syn::Expr = syn::parse_str(expr_src).expect("valid expr");
let literal_consts: AHashMap<String, DefaultValue> = consts
.iter()
.map(|(k, v)| (k.to_string(), DefaultValue::StringLiteral(v.to_string())))
.collect();
let field_types = AHashMap::new();
expr_to_default_value(&expr, &EvalScope::new("Subject", &literal_consts, &field_types), None)
}
#[test]
fn some_int_literal_unwraps_to_inner_int() {
assert_eq!(
default_value_of("Some(50 * 1024 * 1024)"),
DefaultValue::IntLiteral(52_428_800)
);
}
#[test]
fn some_string_literal_unwraps_to_inner_string() {
assert_eq!(
default_value_of(r#"Some("hi".to_string())"#),
DefaultValue::StringLiteral("hi".to_string())
);
}
#[test]
fn qualified_option_some_unwraps() {
assert_eq!(default_value_of("Option::Some(5)"), DefaultValue::IntLiteral(5));
}
#[test]
fn bare_none_stays_none() {
assert_eq!(default_value_of("None"), DefaultValue::None);
}
#[test]
fn zero_argument_function_call_preserves_its_path() {
assert_eq!(
default_value_of("defaults::retry_limit()"),
DefaultValue::FunctionCall("defaults::retry_limit".to_string())
);
}
#[test]
fn const_to_string_resolves_to_the_consts_literal_value() {
assert_eq!(
default_value_of_with_consts(
"DEFAULT_CATALOG_URL.to_string()",
&[("DEFAULT_CATALOG_URL", "https://example.com/catalog.json")]
),
DefaultValue::StringLiteral("https://example.com/catalog.json".to_string())
);
}
#[test]
fn const_into_resolves_to_the_consts_literal_value() {
assert_eq!(
default_value_of_with_consts("HOST.into()", &[("HOST", "localhost")]),
DefaultValue::StringLiteral("localhost".to_string())
);
}
#[test]
fn bare_const_path_resolves_to_the_consts_literal_value() {
assert_eq!(
default_value_of_with_consts("HOST", &[("HOST", "localhost")]),
DefaultValue::StringLiteral("localhost".to_string())
);
}
#[test]
fn unresolvable_const_reference_is_unresolved_not_empty() {
assert!(
matches!(
default_value_of("UNKNOWN_CONST.to_string()"),
DefaultValue::Unresolved(_)
),
"an unresolvable const reference must be reported, not silently zeroed"
);
}
#[test]
fn collect_literal_consts_collects_every_literal_kind_and_nothing_computed() {
let file: syn::File = syn::parse_str(
r#"
pub const DEFAULT_CATALOG_URL: &str = "https://example.com/catalog.json";
const CACHE_DIR_NAME: &str = "sample-crate";
const RETRY_LIMIT: u32 = 3;
const DET_DB_THRESH: f32 = 0.3;
const VERBOSE: bool = false;
const MIN_OFFSET: i32 = -5;
const COMPUTED: &str = some_fn();
const WINDOW: Duration = Duration::from_secs(5);
"#,
)
.expect("valid file");
let consts = collect_literal_consts(&file.items);
assert_eq!(
consts.get("DEFAULT_CATALOG_URL"),
Some(&DefaultValue::StringLiteral(
"https://example.com/catalog.json".to_string()
))
);
assert_eq!(
consts.get("CACHE_DIR_NAME"),
Some(&DefaultValue::StringLiteral("sample-crate".to_string()))
);
assert_eq!(consts.get("RETRY_LIMIT"), Some(&DefaultValue::IntLiteral(3)));
assert_eq!(consts.get("DET_DB_THRESH"), Some(&DefaultValue::FloatLiteral(0.3)));
assert_eq!(consts.get("VERBOSE"), Some(&DefaultValue::BoolLiteral(false)));
assert_eq!(consts.get("MIN_OFFSET"), Some(&DefaultValue::IntLiteral(-5)));
assert_eq!(
consts.get("COMPUTED"),
None,
"non-literal initializers must not be collected"
);
assert_eq!(
consts.get("WINDOW"),
None,
"evaluating a const-fn initializer would be interpretation, not reading"
);
}
#[test]
fn collect_literal_consts_folds_a_tuple_struct_literal_to_its_inner_scalar() {
let file: syn::File = syn::parse_str(
r#"
impl Weight {
pub const ONE: Weight = Weight(1);
pub const MAX: Weight = Weight(u32::MAX);
}
"#,
)
.expect("valid file");
let consts = collect_literal_consts(&file.items);
assert_eq!(consts.get("Weight::ONE"), Some(&DefaultValue::IntLiteral(1)));
assert_eq!(
consts.get("Weight::MAX"),
None,
"the inner expression `u32::MAX` is not itself a literal; guessing its value is worse \
than leaving the const unindexed"
);
}
#[test]
fn collect_literal_consts_does_not_fold_a_lowercase_call_as_a_tuple_struct_literal() {
let file: syn::File = syn::parse_str(r#"const RETRY_LIMIT: u32 = compute(3);"#).expect("valid file");
let consts = collect_literal_consts(&file.items);
assert_eq!(
consts.get("RETRY_LIMIT"),
None,
"a snake_case call is a function invocation and must not be folded"
);
}
fn defaults_for(source: &str, type_name: &str, field_names: &[&str]) -> Vec<(String, DefaultValue)> {
let fields: Vec<(&str, TypeRef)> = field_names.iter().map(|name| (*name, TypeRef::Unit)).collect();
defaults_for_typed(source, type_name, &fields)
}
fn defaults_for_typed(source: &str, type_name: &str, fields: &[(&str, TypeRef)]) -> Vec<(String, DefaultValue)> {
let file: syn::File = syn::parse_str(source).expect("valid module source");
let literal_consts = collect_literal_consts(&file.items);
let constructors = collect_constructors(&file.items);
let default_impl = file
.items
.iter()
.find_map(|item| match item {
syn::Item::Impl(item_impl)
if item_impl
.trait_
.as_ref()
.is_some_and(|(path, _)| path.segments.last().is_some_and(|s| s.ident == "Default"))
&& path_type_name(&item_impl.self_ty).as_deref() == Some(type_name) =>
{
Some(item_impl)
}
_ => None,
})
.expect("module declares `impl Default` for the type");
let mut fields: Vec<FieldDef> = fields
.iter()
.map(|(name, ty)| FieldDef {
name: (*name).to_string(),
ty: ty.clone(),
..Default::default()
})
.collect();
extract_default_values(default_impl, type_name, &mut fields, &literal_consts, &constructors);
fields
.into_iter()
.map(|field| {
let value = field.typed_default.expect("every field is assigned a default");
(field.name, value)
})
.collect()
}
#[test]
fn a_default_delegating_to_a_constructor_recovers_the_constructors_literals() {
let resolved = defaults_for(
r#"
pub struct PaddleOcrConfig {
pub language: String,
pub det_db_thresh: f32,
pub det_limit_side_len: u32,
pub use_angle_cls: bool,
}
impl PaddleOcrConfig {
pub fn new(language: &str) -> Self {
Self {
language: language.to_string(),
det_db_thresh: 0.3,
det_limit_side_len: 1024,
use_angle_cls: true,
}
}
}
impl Default for PaddleOcrConfig {
fn default() -> Self {
Self::new("en")
}
}
"#,
"PaddleOcrConfig",
&["language", "det_db_thresh", "det_limit_side_len", "use_angle_cls"],
);
assert_eq!(
resolved,
vec![
("language".to_string(), DefaultValue::StringLiteral("en".to_string())),
("det_db_thresh".to_string(), DefaultValue::FloatLiteral(0.3)),
("det_limit_side_len".to_string(), DefaultValue::IntLiteral(1024)),
("use_angle_cls".to_string(), DefaultValue::BoolLiteral(true)),
],
"a delegating `fn default()` must yield the constructor's literals, never a type-zero"
);
}
#[test]
fn a_delegation_named_by_the_type_and_consumed_by_into_also_recovers() {
let resolved = defaults_for(
r#"
pub struct Client { pub endpoint: String, pub retries: u32 }
impl Client {
pub fn for_endpoint(endpoint: &str) -> Self {
Self { endpoint: endpoint.into(), retries: 5 }
}
}
impl Default for Client {
fn default() -> Self {
Client::for_endpoint("https://api.example.com")
}
}
"#,
"Client",
&["endpoint", "retries"],
);
assert_eq!(
resolved,
vec![
(
"endpoint".to_string(),
DefaultValue::StringLiteral("https://api.example.com".to_string())
),
("retries".to_string(), DefaultValue::IntLiteral(5)),
]
);
}
#[test]
fn a_delegation_passing_a_module_const_resolves_it() {
let resolved = defaults_for(
r#"
const DEFAULT_LANG: &str = "en";
pub struct Ocr { pub language: String }
impl Ocr {
pub fn new(language: &str) -> Self {
Self { language: language.to_string() }
}
}
impl Default for Ocr {
fn default() -> Self { Self::new(DEFAULT_LANG) }
}
"#,
"Ocr",
&["language"],
);
assert_eq!(
resolved,
vec![("language".to_string(), DefaultValue::StringLiteral("en".to_string()))]
);
}
#[test]
fn a_delegation_chained_through_a_second_constructor_still_resolves() {
let resolved = defaults_for(
r#"
pub struct Cfg { pub level: u32 }
impl Cfg {
pub fn new() -> Self { Self::with_level(7) }
pub fn with_level(level: u32) -> Self { Self { level } }
}
impl Default for Cfg {
fn default() -> Self { Self::new() }
}
"#,
"Cfg",
&["level"],
);
assert_eq!(resolved, vec![("level".to_string(), DefaultValue::IntLiteral(7))]);
}
#[test]
fn a_cyclic_delegation_terminates_and_reports_unresolved() {
let resolved = defaults_for(
r#"
pub struct Cfg { pub level: u32 }
impl Cfg {
pub fn new() -> Self { Self::fresh() }
pub fn fresh() -> Self { Self::new() }
}
impl Default for Cfg {
fn default() -> Self { Self::new() }
}
"#,
"Cfg",
&["level"],
);
assert!(
matches!(resolved.as_slice(), [(_, DefaultValue::Unresolved(_))]),
"a cycle must resolve to `Unresolved`, got {resolved:?}"
);
}
#[test]
fn a_default_delegating_to_a_builder_is_unresolved_not_a_type_zero() {
let resolved = defaults_for(
r#"
pub struct Cfg { pub level: u32 }
impl Cfg {
pub fn builder() -> CfgBuilder { CfgBuilder::new() }
}
impl Default for Cfg {
fn default() -> Self { Self::builder().level(9).build() }
}
"#,
"Cfg",
&["level"],
);
assert!(
matches!(resolved.as_slice(), [(_, DefaultValue::Unresolved(_))]),
"an unfollowable body must be reported, not silently zeroed; got {resolved:?}"
);
assert_ne!(
resolved[0].1,
DefaultValue::Empty,
"`Empty` would claim the default *is* the type-zero, which is the conflation this fixes"
);
}
#[test]
fn a_single_field_types_associated_const_tail_folds_to_the_consts_scalar() {
let resolved = defaults_for(
r#"
pub struct Weight(pub u32);
impl Weight {
pub const ONE: Weight = Weight(1);
}
impl Default for Weight {
fn default() -> Self {
Self::ONE
}
}
"#,
"Weight",
&["_0"],
);
assert_eq!(
resolved,
vec![("_0".to_string(), DefaultValue::IntLiteral(1))],
"a foldable associated-const tail must recover the real default, not `Unresolved` and not `0`"
);
}
#[test]
fn an_associated_consts_unfoldable_inner_value_stays_unresolved_not_a_type_zero() {
let resolved = defaults_for(
r#"
pub struct Weight(pub u32);
impl Weight {
pub const MAX: Weight = Weight(u32::MAX);
}
impl Default for Weight {
fn default() -> Self {
Self::MAX
}
}
"#,
"Weight",
&["_0"],
);
assert!(
matches!(resolved.as_slice(), [(_, DefaultValue::Unresolved(_))]),
"an unfoldable const initializer must be reported, not guessed; got {resolved:?}"
);
assert_ne!(
resolved[0].1,
DefaultValue::IntLiteral(0),
"collapsing to a zero would be silently wrong: `u32::MAX` is not `0`"
);
}
#[test]
fn a_struct_literal_default_is_unchanged_and_keeps_empty_for_genuine_zeros() {
let resolved = defaults_for(
r#"
pub struct Cfg { pub level: u32, pub tags: Vec<String> }
impl Default for Cfg {
fn default() -> Self {
Self { level: 3, tags: Vec::new() }
}
}
"#,
"Cfg",
&["level", "tags"],
);
assert_eq!(
resolved,
vec![
("level".to_string(), DefaultValue::IntLiteral(3)),
("tags".to_string(), DefaultValue::Empty),
]
);
}
#[test]
fn a_delegation_with_mismatched_arity_is_unresolved() {
let resolved = defaults_for(
r#"
pub struct Cfg { pub level: u32 }
impl Cfg {
pub fn new(level: u32, name: &str) -> Self { Self { level } }
}
impl Default for Cfg {
fn default() -> Self { Self::new(4) }
}
"#,
"Cfg",
&["level"],
);
assert!(
matches!(resolved.as_slice(), [(_, DefaultValue::Unresolved(_))]),
"got {resolved:?}"
);
}
#[test]
fn a_delegation_with_an_unfoldable_argument_reports_only_the_field_that_reads_it() {
let resolved = defaults_for(
r#"
pub struct Cfg { pub name: String, pub level: u32 }
impl Cfg {
pub fn new(name: &str) -> Self { Self { name: name.to_string(), level: 2 } }
}
impl Default for Cfg {
fn default() -> Self { Self::new(compute_name()) }
}
"#,
"Cfg",
&["name", "level"],
);
assert!(
matches!(
resolved.as_slice(),
[
(name, DefaultValue::Unresolved(_)),
(level, DefaultValue::IntLiteral(2)),
] if name == "name" && level == "level"
),
"the unfoldable argument must not poison the sibling field it does not reach, and the \
field it does reach must be reported rather than zeroed; got {resolved:?}"
);
}
#[test]
fn collect_constructors_indexes_associated_fns_and_skips_methods_and_trait_impls() {
let file: syn::File = syn::parse_str(
r#"
impl Cfg {
pub fn new() -> Self { Self {} }
pub fn tweak(&self) -> Self { Self {} }
}
impl Default for Cfg {
fn default() -> Self { Self::new() }
}
"#,
)
.expect("valid file");
let constructors = collect_constructors(&file.items);
assert!(constructors.contains_key(&("Cfg".to_string(), "new".to_string())));
assert!(
!constructors.contains_key(&("Cfg".to_string(), "tweak".to_string())),
"a `&self` method cannot be reached by `Self::name(..)` in `fn default()`"
);
assert!(
!constructors.contains_key(&("Cfg".to_string(), "default".to_string())),
"trait impls must not be indexed as constructors"
);
}
#[test]
fn a_constructor_parameter_shadows_a_module_const_of_the_same_name() {
let resolved = defaults_for(
r#"
const language: &str = "shadowed";
pub struct Cfg { pub language: String }
impl Cfg {
pub fn new(language: &str) -> Self { Self { language: language.to_string() } }
}
impl Default for Cfg {
fn default() -> Self { Self::new("en") }
}
"#,
"Cfg",
&["language"],
);
assert_eq!(
resolved,
vec![("language".to_string(), DefaultValue::StringLiteral("en".to_string()))]
);
}
fn rendered_python_default(name: &str, ty: TypeRef, value: &DefaultValue) -> String {
let field = FieldDef {
name: name.to_string(),
ty,
typed_default: Some(value.clone()),
..Default::default()
};
crate::codegen::config_gen::default_value_for_field(&field, "python")
}
#[test]
fn an_associated_const_default_on_a_string_field_resolves_to_the_consts_value() {
let resolved = defaults_for_typed(
r#"
pub struct LlmConfig { pub model: String }
impl LlmConfig {
pub const DEFAULT_MODEL: &str = "claude-sonnet-4-5";
}
impl Default for LlmConfig {
fn default() -> Self {
Self { model: Self::DEFAULT_MODEL.to_string() }
}
}
"#,
"LlmConfig",
&[("model", TypeRef::String)],
);
assert_eq!(
resolved,
vec![(
"model".to_string(),
DefaultValue::StringLiteral("claude-sonnet-4-5".to_string())
)]
);
assert_ne!(
rendered_python_default("model", TypeRef::String, &resolved[0].1),
"\"default_model\"",
"the snake-cased const name is a fabricated value; it must not reach a binding"
);
}
#[test]
fn a_bare_associated_const_path_resolves_through_the_owning_type() {
let resolved = defaults_for_typed(
r#"
pub struct LlmConfig { pub base_url: String }
impl LlmConfig {
const DEFAULT_BASE_URL: &'static str = "https://api.anthropic.com";
}
impl Default for LlmConfig {
fn default() -> Self {
Self { base_url: LlmConfig::DEFAULT_BASE_URL.into() }
}
}
"#,
"LlmConfig",
&[("base_url", TypeRef::String)],
);
assert_eq!(
resolved,
vec![(
"base_url".to_string(),
DefaultValue::StringLiteral("https://api.anthropic.com".to_string())
)]
);
}
#[test]
fn an_unreachable_associated_const_on_a_string_field_is_unresolved_not_an_enum_variant() {
let resolved = defaults_for_typed(
r#"
pub struct LlmConfig { pub model: String }
impl Default for LlmConfig {
fn default() -> Self {
Self { model: Self::DEFAULT_MODEL.to_string() }
}
}
"#,
"LlmConfig",
&[("model", TypeRef::String)],
);
let value = &resolved[0].1;
assert!(
matches!(value, DefaultValue::Unresolved(_)),
"an unreadable initializer must be reported, got {value:?}"
);
assert_ne!(
value,
&DefaultValue::EnumVariant("DEFAULT_MODEL".to_string()),
"a `String` field cannot hold an enum variant, so this lowering was never sound"
);
assert_ne!(
rendered_python_default("model", TypeRef::String, value),
"\"default_model\"",
"the fabricated snake-cased const name must be absent from generated output"
);
}
#[test]
fn a_genuine_enum_variant_default_still_lowers_to_an_enum_variant() {
let resolved = defaults_for_typed(
r#"
pub struct Cfg { pub mode: Mode, pub fallback: Option<Mode>, pub stages: Vec<Mode> }
impl Default for Cfg {
fn default() -> Self {
Self {
mode: Mode::Fast,
fallback: Some(Mode::Slow),
stages: vec![Mode::Fast, Mode::Slow],
}
}
}
"#,
"Cfg",
&[
("mode", TypeRef::Named("Mode".to_string())),
(
"fallback",
TypeRef::Optional(Box::new(TypeRef::Named("Mode".to_string()))),
),
("stages", TypeRef::Vec(Box::new(TypeRef::Named("Mode".to_string())))),
],
);
assert_eq!(
resolved,
vec![
("mode".to_string(), DefaultValue::EnumVariant("Fast".to_string())),
("fallback".to_string(), DefaultValue::EnumVariant("Slow".to_string())),
(
"stages".to_string(),
DefaultValue::ListLiteral(vec![
DefaultValue::EnumVariant("Fast".to_string()),
DefaultValue::EnumVariant("Slow".to_string()),
])
),
],
"an enum-typed field — bare, optional or in a list — must keep its variant default"
);
}
#[test]
fn an_associated_const_of_another_type_does_not_answer_for_this_one() {
let resolved = defaults_for_typed(
r#"
pub struct Other { pub model: String }
pub struct LlmConfig { pub model: String }
impl Other {
pub const DEFAULT_MODEL: &str = "not-this-one";
}
impl Default for LlmConfig {
fn default() -> Self {
Self { model: Self::DEFAULT_MODEL.to_string() }
}
}
"#,
"LlmConfig",
&[("model", TypeRef::String)],
);
assert!(
matches!(resolved.as_slice(), [(_, DefaultValue::Unresolved(_))]),
"a same-named const on a different type must not be substituted; got {resolved:?}"
);
}
#[test]
fn an_unreadable_field_initializer_is_unresolved_not_empty() {
let resolved = defaults_for(
r#"
pub struct Cfg {
pub threshold: f32,
pub name: String,
pub root: PathBuf,
pub window: [u32; 2],
pub mode: u8,
}
impl Default for Cfg {
fn default() -> Self {
Self {
threshold: compute().clamp(0.0, 1.0),
name: make_name(1, 2),
root: PathBuf::from("/tmp"),
window: [1, 2],
mode: if cfg!(unix) { 1 } else { 2 },
}
}
}
"#,
"Cfg",
&["threshold", "name", "root", "window", "mode"],
);
for (name, value) in &resolved {
assert!(
matches!(value, DefaultValue::Unresolved(_)),
"`{name}` is not readable, so it must be reported rather than zeroed; got {value:?}"
);
}
}
#[test]
fn genuine_type_zero_initializers_stay_empty() {
let resolved = defaults_for(
r#"
pub struct Cfg {
pub tags: Vec<String>,
pub index: AHashMap<String, u32>,
pub count: u32,
pub stages: Vec<String>,
}
impl Default for Cfg {
fn default() -> Self {
Self {
tags: Vec::new(),
index: AHashMap::new(),
count: u32::default(),
stages: vec![],
}
}
}
"#,
"Cfg",
&["tags", "index", "count", "stages"],
);
assert_eq!(
resolved,
vec![
("tags".to_string(), DefaultValue::Empty),
("index".to_string(), DefaultValue::Empty),
("count".to_string(), DefaultValue::Empty),
("stages".to_string(), DefaultValue::Empty),
],
"a known type-zero must stay `Empty`; only an unread value becomes `Unresolved`"
);
}
#[test]
fn a_module_const_of_any_literal_type_resolves_to_its_value() {
let resolved = defaults_for(
r#"
const DEFAULT_DETECTION_LIMIT_SIDE_LEN: u32 = 1024;
const DEFAULT_RECOGNITION_BATCH_SIZE: usize = 6;
const DEFAULT_DB_THRESH: f32 = 0.3;
const DEFAULT_VERBOSE: bool = true;
pub struct PaddleOcrConfig {
pub det_limit_side_len: u32,
pub rec_batch_num: usize,
pub det_db_thresh: f32,
pub verbose: bool,
}
impl Default for PaddleOcrConfig {
fn default() -> Self {
Self {
det_limit_side_len: DEFAULT_DETECTION_LIMIT_SIDE_LEN,
rec_batch_num: DEFAULT_RECOGNITION_BATCH_SIZE,
det_db_thresh: DEFAULT_DB_THRESH,
verbose: DEFAULT_VERBOSE,
}
}
}
"#,
"PaddleOcrConfig",
&["det_limit_side_len", "rec_batch_num", "det_db_thresh", "verbose"],
);
assert_eq!(
resolved,
vec![
("det_limit_side_len".to_string(), DefaultValue::IntLiteral(1024)),
("rec_batch_num".to_string(), DefaultValue::IntLiteral(6)),
("det_db_thresh".to_string(), DefaultValue::FloatLiteral(0.3)),
("verbose".to_string(), DefaultValue::BoolLiteral(true)),
],
"a numeric module const is readable; substituting the type-zero for it is the same \
fabrication as substituting one for an unread default"
);
}
#[test]
fn a_fully_qualified_enum_path_still_lowers_to_its_last_segment() {
let resolved = defaults_for_typed(
r#"
pub struct ExtractionConfig { pub result_format: ResultFormat }
impl Default for ExtractionConfig {
fn default() -> Self {
Self { result_format: crate::types::ResultFormat::Unified }
}
}
"#,
"ExtractionConfig",
&[("result_format", TypeRef::Named("ResultFormat".to_string()))],
);
assert_eq!(
resolved,
vec![(
"result_format".to_string(),
DefaultValue::EnumVariant("Unified".to_string())
)]
);
}
#[test]
fn a_cow_wrapped_literal_resolves_to_the_literal_it_wraps() {
let resolved = defaults_for_typed(
r#"
pub struct ProcessConfig { pub language: Cow<'static, str>, pub tag: Cow<'static, str> }
impl Default for ProcessConfig {
fn default() -> Self {
Self {
language: Cow::Borrowed(""),
tag: std::borrow::Cow::Borrowed("stable"),
}
}
}
"#,
"ProcessConfig",
&[("language", TypeRef::String), ("tag", TypeRef::String)],
);
assert_eq!(
resolved,
vec![
("language".to_string(), DefaultValue::StringLiteral(String::new())),
("tag".to_string(), DefaultValue::StringLiteral("stable".to_string())),
]
);
}
#[test]
fn a_cow_wrapping_an_unreadable_expression_stays_unresolved() {
let resolved = defaults_for_typed(
r#"
pub struct ProcessConfig { pub language: Cow<'static, str> }
impl Default for ProcessConfig {
fn default() -> Self {
Self { language: Cow::Owned(detect_language()) }
}
}
"#,
"ProcessConfig",
&[("language", TypeRef::String)],
);
assert!(
matches!(resolved.as_slice(), [(_, DefaultValue::Unresolved(_))]),
"got {resolved:?}"
);
}
#[test]
fn collect_literal_consts_indexes_associated_consts_under_their_owning_type() {
let file: syn::File = syn::parse_str(
r#"
impl LlmConfig {
pub const DEFAULT_MODEL: &str = "claude-sonnet-4-5";
pub const MAX_TOKENS: u32 = 4096;
}
impl Default for LlmConfig {
const NOT_A_CONSTRUCTOR: &str = "trait-impl";
fn default() -> Self { Self {} }
}
#[cfg(test)]
impl LlmConfig {
pub const DEFAULT_MODEL: &str = "test-only";
}
"#,
)
.expect("valid file");
let consts = collect_literal_consts(&file.items);
assert_eq!(
consts.get("LlmConfig::DEFAULT_MODEL"),
Some(&DefaultValue::StringLiteral("claude-sonnet-4-5".to_string())),
"a `#[cfg(test)]` impl must not shadow the real associated const"
);
assert_eq!(
consts.get("LlmConfig::MAX_TOKENS"),
Some(&DefaultValue::IntLiteral(4096))
);
assert_eq!(
consts
.get("Default::NOT_A_CONSTRUCTOR")
.or(consts.get("LlmConfig::NOT_A_CONSTRUCTOR")),
None,
"trait-impl associated consts are not inherent consts of the type"
);
}
#[test]
fn a_struct_variant_default_folds_with_its_field_values() {
let resolved = defaults_for_typed(
r#"
pub struct Cfg { pub kind: Kind }
impl Default for Cfg {
fn default() -> Self {
Self { kind: Kind::Curated { label: "balanced".to_string(), weight: 3 } }
}
}
"#,
"Cfg",
&[("kind", TypeRef::Named("Kind".to_string()))],
);
assert_eq!(
resolved,
vec![(
"kind".to_string(),
DefaultValue::StructVariant(
"Curated".to_string(),
vec![
("label".to_string(), DefaultValue::StringLiteral("balanced".to_string())),
("weight".to_string(), DefaultValue::IntLiteral(3)),
],
),
)],
"a struct-variant enum default must fold into its own field values, not stay Unresolved"
);
}
#[test]
fn a_tuple_variant_default_folds_with_its_argument_values() {
let resolved = defaults_for_typed(
r#"
pub struct Cfg { pub kind: Kind }
impl Default for Cfg {
fn default() -> Self {
Self { kind: Kind::Scaled(5, "x".to_string()) }
}
}
"#,
"Cfg",
&[("kind", TypeRef::Named("Kind".to_string()))],
);
assert_eq!(
resolved,
vec![(
"kind".to_string(),
DefaultValue::TupleVariant(
"Scaled".to_string(),
vec![
DefaultValue::IntLiteral(5),
DefaultValue::StringLiteral("x".to_string())
],
),
)],
"a tuple-variant enum default must fold into its own argument values, not stay Unresolved"
);
}
#[test]
fn a_unit_variant_default_still_folds_to_a_bare_enum_variant() {
let resolved = defaults_for_typed(
r#"
pub struct Cfg { pub kind: Kind }
impl Default for Cfg {
fn default() -> Self {
Self { kind: Kind::Auto }
}
}
"#,
"Cfg",
&[("kind", TypeRef::Named("Kind".to_string()))],
);
assert_eq!(
resolved,
vec![("kind".to_string(), DefaultValue::EnumVariant("Auto".to_string()))],
"a unit-variant default already folded before this change and must keep doing so"
);
}
#[test]
fn a_struct_variants_unfoldable_field_keeps_the_whole_default_unresolved_not_empty() {
let resolved = defaults_for_typed(
r#"
pub struct Cfg { pub kind: Kind }
impl Default for Cfg {
fn default() -> Self {
Self { kind: Kind::Curated { label: compute_label() } }
}
}
"#,
"Cfg",
&[("kind", TypeRef::Named("Kind".to_string()))],
);
assert!(
matches!(resolved.as_slice(), [(_, DefaultValue::Unresolved(_))]),
"an unfoldable inner field must leave the whole variant default Unresolved; got {resolved:?}"
);
assert_ne!(
resolved[0].1,
DefaultValue::Empty,
"collapsing an unfoldable struct-variant field to `Empty` is the conflation this fixes"
);
}
#[test]
fn a_tuple_variants_unfoldable_argument_keeps_the_whole_default_unresolved_not_empty() {
let resolved = defaults_for_typed(
r#"
pub struct Cfg { pub kind: Kind }
impl Default for Cfg {
fn default() -> Self {
Self { kind: Kind::Scaled(compute_scale()) }
}
}
"#,
"Cfg",
&[("kind", TypeRef::Named("Kind".to_string()))],
);
assert!(
matches!(resolved.as_slice(), [(_, DefaultValue::Unresolved(_))]),
"an unfoldable argument must leave the whole variant default Unresolved; got {resolved:?}"
);
}
#[test]
fn a_struct_variant_with_a_rest_base_stays_unresolved() {
let resolved = defaults_for_typed(
r#"
pub struct Cfg { pub kind: Kind }
impl Default for Cfg {
fn default() -> Self {
Self { kind: Kind::Curated { label: "x".to_string(), ..Default::default() } }
}
}
"#,
"Cfg",
&[("kind", TypeRef::Named("Kind".to_string()))],
);
assert!(
matches!(resolved.as_slice(), [(_, DefaultValue::Unresolved(_))]),
"a `..base` spread can carry fields this pass never saw; folding without it would guess; \
got {resolved:?}"
);
}
}