use crate::codegen::field_init::struct_field_init;
use crate::core::ir::{DefaultValue, FieldDef, MethodDef, ParamDef, PrimitiveType, ReceiverKind, TypeDef, TypeRef};
use ahash::AHashSet;
use std::collections::{HashMap, HashSet};
use std::sync::LazyLock;
pub use super::crate_attributes::{format_crate_attributes, format_extra_clippy_allows};
static BARE_ZERO_ARG_CALL: LazyLock<regex::Regex> = LazyLock::new(|| {
regex::Regex::new(r"^[A-Za-z_][A-Za-z0-9_]*(?:::[A-Za-z_][A-Za-z0-9_]*)*\(\)$").expect("valid regex")
});
fn unwrap_or_else_default(binding_name: &str, field_name: &str, default_val: &str) -> String {
if BARE_ZERO_ARG_CALL.is_match(default_val) {
let path = default_val
.strip_suffix("()")
.expect("BARE_ZERO_ARG_CALL guarantees a `()` suffix");
return format!("{binding_name}: {field_name}.unwrap_or_else({path})");
}
format!("{binding_name}: {field_name}.unwrap_or_else(|| {default_val})")
}
pub fn substitute_excluded_types(ty: &TypeRef, excluded: &HashSet<&str>) -> TypeRef {
match ty {
TypeRef::Named(name) if excluded.contains(name.as_str()) => TypeRef::Json,
TypeRef::Optional(inner) => TypeRef::Optional(Box::new(substitute_excluded_types(inner, excluded))),
TypeRef::Vec(inner) => TypeRef::Vec(Box::new(substitute_excluded_types(inner, excluded))),
TypeRef::Map(k, v) => TypeRef::Map(
Box::new(substitute_excluded_types(k, excluded)),
Box::new(substitute_excluded_types(v, excluded)),
),
other => other.clone(),
}
}
pub fn substitute_trait_interfaces(ty: &TypeRef, trait_interfaces: &HashSet<&str>) -> TypeRef {
match ty {
TypeRef::Named(name) if trait_interfaces.contains(name.as_str()) => TypeRef::Named(format!("_{name}")),
TypeRef::Optional(inner) => TypeRef::Optional(Box::new(substitute_trait_interfaces(inner, trait_interfaces))),
TypeRef::Vec(inner) => TypeRef::Vec(Box::new(substitute_trait_interfaces(inner, trait_interfaces))),
TypeRef::Map(k, v) => TypeRef::Map(
Box::new(substitute_trait_interfaces(k, trait_interfaces)),
Box::new(substitute_trait_interfaces(v, trait_interfaces)),
),
other => other.clone(),
}
}
pub fn binding_fields(fields: &[FieldDef]) -> impl Iterator<Item = &FieldDef> {
fields.iter().filter(|field| !field.binding_excluded)
}
pub fn is_promoted_optional(params: &[ParamDef], idx: usize) -> bool {
if params[idx].optional {
return false;
}
params[..idx].iter().any(|p| p.optional)
}
pub fn can_auto_delegate_function(func: &crate::core::ir::FunctionDef, opaque_types: &AHashSet<String>) -> bool {
!func.sanitized
&& func.params.iter().all(|p| {
!p.sanitized
&& is_delegatable_param_with_slices(&p.ty, opaque_types)
&& !is_named_ref_param(p, opaque_types)
})
&& is_delegatable_return(&func.return_type)
}
pub fn can_auto_delegate(method: &MethodDef, opaque_types: &AHashSet<String>) -> bool {
if matches!(method.receiver, Some(ReceiverKind::RefMut)) && method.trait_source.is_none() {
return false;
}
!method.sanitized
&& method.params.iter().all(|p| {
!p.sanitized
&& is_delegatable_param_with_slices(&p.ty, opaque_types)
&& !is_named_ref_param(p, opaque_types)
})
&& is_delegatable_return(&method.return_type)
}
pub fn can_auto_delegate_with_named_let_bindings(method: &MethodDef, opaque_types: &AHashSet<String>) -> bool {
if matches!(method.receiver, Some(ReceiverKind::RefMut)) && method.trait_source.is_none() {
return false;
}
!method.sanitized
&& method
.params
.iter()
.all(|p| !p.sanitized && is_delegatable_param_with_slices(&p.ty, opaque_types))
&& is_delegatable_return(&method.return_type)
}
pub fn is_named_ref_param_pub(p: &crate::core::ir::ParamDef, opaque_types: &AHashSet<String>) -> bool {
is_named_ref_param(p, opaque_types)
}
fn is_named_ref_param(p: &crate::core::ir::ParamDef, opaque_types: &AHashSet<String>) -> bool {
if !p.is_ref {
return false;
}
match &p.ty {
TypeRef::Named(name) => !opaque_types.contains(name.as_str()),
TypeRef::Vec(inner) => match inner.as_ref() {
TypeRef::String | TypeRef::Char => true,
TypeRef::Named(name) => !opaque_types.contains(name.as_str()),
_ => false,
},
_ => false,
}
}
pub fn is_delegatable_param(ty: &TypeRef, _opaque_types: &AHashSet<String>) -> bool {
is_delegatable_param_with_slices(ty, _opaque_types)
}
fn is_delegatable_param_with_slices(ty: &TypeRef, _opaque_types: &AHashSet<String>) -> bool {
match ty {
TypeRef::Primitive(_)
| TypeRef::String
| TypeRef::Char
| TypeRef::Bytes
| TypeRef::Path
| TypeRef::Unit
| TypeRef::Duration
| TypeRef::Json => true,
TypeRef::Named(_) => true,
TypeRef::Optional(inner) => is_delegatable_param_with_slices(inner, _opaque_types),
TypeRef::Vec(inner) => is_delegatable_param_with_slices(inner, _opaque_types),
TypeRef::Map(k, v) => {
is_delegatable_param_with_slices(k, _opaque_types) && is_delegatable_param_with_slices(v, _opaque_types)
}
}
}
pub fn is_delegatable_return(ty: &TypeRef) -> bool {
match ty {
TypeRef::Primitive(_)
| TypeRef::String
| TypeRef::Char
| TypeRef::Bytes
| TypeRef::Path
| TypeRef::Unit
| TypeRef::Duration
| TypeRef::Json => true,
TypeRef::Named(_) => true,
TypeRef::Optional(inner) | TypeRef::Vec(inner) => is_delegatable_return(inner),
TypeRef::Map(k, v) => is_delegatable_return(k) && is_delegatable_return(v),
}
}
pub fn is_delegatable_type(ty: &TypeRef) -> bool {
match ty {
TypeRef::Primitive(_)
| TypeRef::String
| TypeRef::Char
| TypeRef::Bytes
| TypeRef::Path
| TypeRef::Unit
| TypeRef::Duration => true,
TypeRef::Named(_) => false,
TypeRef::Optional(inner) | TypeRef::Vec(inner) => is_delegatable_type(inner),
TypeRef::Map(k, v) => is_delegatable_type(k) && is_delegatable_type(v),
TypeRef::Json => false,
}
}
pub fn is_opaque_delegatable_type(ty: &TypeRef) -> bool {
match ty {
TypeRef::Primitive(_)
| TypeRef::String
| TypeRef::Char
| TypeRef::Bytes
| TypeRef::Path
| TypeRef::Unit
| TypeRef::Duration
| TypeRef::Json => true,
TypeRef::Named(_) => true,
TypeRef::Optional(inner) | TypeRef::Vec(inner) => is_opaque_delegatable_type(inner),
TypeRef::Map(k, v) => is_opaque_delegatable_type(k) && is_opaque_delegatable_type(v),
}
}
pub fn is_simple_type(ty: &TypeRef) -> bool {
match ty {
TypeRef::Primitive(_)
| TypeRef::String
| TypeRef::Char
| TypeRef::Bytes
| TypeRef::Path
| TypeRef::Unit
| TypeRef::Duration => true,
TypeRef::Optional(inner) | TypeRef::Vec(inner) => is_simple_type(inner),
TypeRef::Map(k, v) => is_simple_type(k) && is_simple_type(v),
TypeRef::Named(_) | TypeRef::Json => false,
}
}
pub fn partition_methods(methods: &[MethodDef]) -> (Vec<&MethodDef>, Vec<&MethodDef>) {
let instance: Vec<_> = methods.iter().filter(|m| m.receiver.is_some()).collect();
let statics: Vec<_> = methods.iter().filter(|m| m.receiver.is_none()).collect();
(instance, statics)
}
pub fn constructor_parts(fields: &[FieldDef], type_mapper: &dyn Fn(&TypeRef) -> String) -> (String, String, String) {
constructor_parts_with_renames_and_cfg_restore(fields, type_mapper, None, &[])
}
pub fn constructor_parts_with_renames(
fields: &[FieldDef],
type_mapper: &dyn Fn(&TypeRef) -> String,
field_renames: Option<&HashMap<String, String>>,
) -> (String, String, String) {
constructor_parts_with_renames_and_cfg_restore(fields, type_mapper, field_renames, &[])
}
pub fn constructor_parts_with_renames_and_cfg_restore(
fields: &[FieldDef],
type_mapper: &dyn Fn(&TypeRef) -> String,
field_renames: Option<&HashMap<String, String>>,
never_skip_cfg_field_names: &[String],
) -> (String, String, String) {
let mut sorted_fields: Vec<&FieldDef> = fields
.iter()
.filter(|f| !f.binding_excluded)
.filter(|f| f.cfg.is_none() || never_skip_cfg_field_names.contains(&f.name))
.collect();
sorted_fields.sort_by_key(|f| (f.optional || f.cfg.is_some()) as u8);
let params: Vec<String> = sorted_fields
.iter()
.map(|f| {
let is_optional = f.optional || f.cfg.is_some();
let ty = if is_optional {
match &f.ty {
TypeRef::Optional(_) => type_mapper(&f.ty),
_ => format!("Option<{}>", type_mapper(&f.ty)),
}
} else {
type_mapper(&f.ty)
};
format!("{}: {}", f.name, ty)
})
.collect();
let defaults: Vec<String> = sorted_fields
.iter()
.map(|f| {
if f.optional || f.cfg.is_some() {
format!("{}=None", f.name)
} else {
f.name.clone()
}
})
.collect();
let assignments: Vec<String> = fields
.iter()
.filter(|f| !f.binding_excluded)
.map(|f| {
let binding_name = field_renames
.and_then(|r| r.get(&f.name))
.map_or_else(|| f.name.as_str(), |s| s.as_str());
if f.cfg.is_some() && !never_skip_cfg_field_names.contains(&f.name) {
return format!("{}: Default::default()", binding_name);
}
if binding_name != f.name {
return binding_name.to_string();
}
f.name.clone()
})
.collect();
let single_line = params.join(", ");
let param_list = if single_line.len() > 100 {
format!("\n {},\n ", params.join(",\n "))
} else {
single_line
};
(param_list, defaults.join(", "), assignments.join(", "))
}
pub fn function_params(params: &[ParamDef], type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
function_params_vec(params, type_mapper).join(", ")
}
pub fn function_params_vec(params: &[ParamDef], type_mapper: &dyn Fn(&TypeRef) -> String) -> Vec<String> {
let mut seen_optional = false;
params
.iter()
.map(|p| {
if p.optional {
seen_optional = true;
}
let ty = if p.optional || seen_optional {
format!("Option<{}>", type_mapper(&p.ty))
} else {
type_mapper(&p.ty)
};
format!("{}: {}", p.name, ty)
})
.collect::<Vec<_>>()
}
pub fn function_sig_defaults(params: &[ParamDef]) -> String {
let mut seen_optional = false;
params
.iter()
.map(|p| {
if p.optional {
seen_optional = true;
}
if p.optional {
format!("{}=None", p.name)
} else if seen_optional {
let default = match &p.ty {
TypeRef::Primitive(PrimitiveType::Bool) => "false",
TypeRef::Primitive(_) => "0",
_ => "None",
};
format!("{}={}", p.name, default)
} else {
p.name.clone()
}
})
.collect::<Vec<_>>()
.join(", ")
}
pub fn float_literal_digits(value: f64) -> Option<String> {
if value.is_nan() || value.is_infinite() {
return None;
}
Some(if value.fract() == 0.0 {
format!("{value:.1}")
} else {
value.to_string()
})
}
fn rust_scalar_default(item: &DefaultValue) -> Option<String> {
match item {
DefaultValue::BoolLiteral(b) => Some(format!("{b}")),
DefaultValue::StringLiteral(s) => Some(format!("\"{}\".to_string()", s.escape_default())),
DefaultValue::IntLiteral(i) => Some(format!("{i}")),
DefaultValue::FloatLiteral(f) => {
let s = format!("{f}");
Some(if s.contains('.') || s.contains('e') || s.contains('E') {
s
} else {
format!("{s}.0")
})
}
DefaultValue::EnumVariant(v) => Some(v.clone()),
DefaultValue::TupleVariant(_, _)
| DefaultValue::StructVariant(_, _)
| DefaultValue::ListLiteral(_)
| DefaultValue::Empty
| DefaultValue::Unresolved(_)
| DefaultValue::None
| DefaultValue::FunctionCall(_)
| DefaultValue::PublicFunctionCall(_) => None,
}
}
pub fn maps_to_js_value(mapped_ty: &str) -> bool {
mapped_ty
.trim()
.trim_start_matches("::")
.rsplit("::")
.next()
.is_some_and(|segment| segment == "JsValue")
}
pub(crate) fn core_default_field_access(field: &FieldDef, typ: &TypeDef) -> Option<String> {
if !typ.has_default || typ.rust_path.is_empty() {
return None;
}
let core_path = typ.rust_path.replace('-', "_");
Some(format!(
"<{core_path} as ::core::default::Default>::default().{}",
field.name
))
}
fn convert_core_default_expr(field: &FieldDef, mapped_ty: &str, expr: String) -> String {
if !matches!(field.ty, TypeRef::Named(_)) {
return expr;
}
if maps_to_js_value(mapped_ty) {
format!("serde_wasm_bindgen::to_value(&{expr}).unwrap_or(wasm_bindgen::JsValue::NULL)")
} else {
format!("{expr}.into()")
}
}
pub fn format_default_value(field: &FieldDef, typ: &TypeDef, mapped_ty: &str) -> String {
let default = field
.typed_default
.as_ref()
.expect("format_default_value: caller must have already confirmed field.typed_default is Some");
match default {
DefaultValue::BoolLiteral(b) => format!("{}", b),
DefaultValue::StringLiteral(s) => format!("\"{}\".to_string()", s.escape_default()),
DefaultValue::IntLiteral(i) => format!("{}", i),
DefaultValue::FloatLiteral(f) => {
let s = format!("{}", f);
if s.contains('.') || s.contains('e') || s.contains('E') {
s
} else {
format!("{s}.0")
}
}
DefaultValue::EnumVariant(v) => match core_default_field_access(field, typ) {
Some(access) => convert_core_default_expr(field, mapped_ty, access),
None => v.clone(),
},
DefaultValue::TupleVariant(_, _) | DefaultValue::StructVariant(_, _) => {
match core_default_field_access(field, typ) {
Some(access) => convert_core_default_expr(field, mapped_ty, access),
None => "Default::default()".to_string(),
}
}
DefaultValue::ListLiteral(items) => {
let rendered: Option<Vec<String>> = items.iter().map(rust_scalar_default).collect();
match rendered {
Some(values) => format!("vec![{}]", values.join(", ")),
None => "Default::default()".to_string(),
}
}
DefaultValue::Empty | DefaultValue::Unresolved(_) => "Default::default()".to_string(),
DefaultValue::None => "None".to_string(),
DefaultValue::FunctionCall(_) | DefaultValue::PublicFunctionCall(_) => {
let recovered = crate::codegen::config_gen::default_value_for_field_in_type(field, "rust", typ);
if recovered.starts_with("compile_error!") {
return recovered;
}
convert_core_default_expr(field, mapped_ty, recovered)
}
}
}
pub fn config_constructor_parts_with_options(
fields: &[FieldDef],
type_mapper: &dyn Fn(&TypeRef) -> String,
option_duration_on_defaults: bool,
typ: &TypeDef,
) -> (String, String, String) {
config_constructor_parts_with_options_cfg(fields, type_mapper, option_duration_on_defaults, false, typ)
}
pub fn config_constructor_parts_with_options_cfg(
fields: &[FieldDef],
type_mapper: &dyn Fn(&TypeRef) -> String,
option_duration_on_defaults: bool,
optionalize_all_defaults: bool,
typ: &TypeDef,
) -> (String, String, String) {
config_constructor_parts_inner(
fields,
type_mapper,
option_duration_on_defaults,
optionalize_all_defaults,
None,
&[],
typ,
)
}
pub fn config_constructor_parts_with_renames(
fields: &[FieldDef],
type_mapper: &dyn Fn(&TypeRef) -> String,
option_duration_on_defaults: bool,
field_renames: Option<&HashMap<String, String>>,
typ: &TypeDef,
) -> (String, String, String) {
config_constructor_parts_inner(
fields,
type_mapper,
option_duration_on_defaults,
false,
field_renames,
&[],
typ,
)
}
pub fn config_constructor_parts_with_renames_and_cfg_restore(
fields: &[FieldDef],
type_mapper: &dyn Fn(&TypeRef) -> String,
option_duration_on_defaults: bool,
field_renames: Option<&HashMap<String, String>>,
never_skip_cfg_field_names: &[String],
typ: &TypeDef,
) -> (String, String, String) {
config_constructor_parts_inner(
fields,
type_mapper,
option_duration_on_defaults,
false,
field_renames,
never_skip_cfg_field_names,
typ,
)
}
pub fn config_constructor_parts(
fields: &[FieldDef],
type_mapper: &dyn Fn(&TypeRef) -> String,
typ: &TypeDef,
) -> (String, String, String) {
config_constructor_parts_inner(fields, type_mapper, false, false, None, &[], typ)
}
fn config_constructor_parts_inner(
fields: &[FieldDef],
type_mapper: &dyn Fn(&TypeRef) -> String,
option_duration_on_defaults: bool,
optionalize_all_defaults: bool,
field_renames: Option<&HashMap<String, String>>,
never_skip_cfg_field_names: &[String],
typ: &TypeDef,
) -> (String, String, String) {
let mut sorted_fields: Vec<&FieldDef> = fields
.iter()
.filter(|f| !f.binding_excluded)
.filter(|f| f.cfg.is_none() || never_skip_cfg_field_names.contains(&f.name))
.collect();
sorted_fields.sort_by_key(|f| f.optional as u8);
let params: Vec<String> = sorted_fields
.iter()
.map(|f| {
let ty = type_mapper(&f.ty);
if matches!(f.ty, TypeRef::Optional(_)) {
format!("{}: {}", f.name, ty)
} else {
format!("{}: Option<{}>", f.name, ty)
}
})
.collect();
let defaults = sorted_fields
.iter()
.map(|f| format!("{}=None", f.name))
.collect::<Vec<_>>()
.join(", ");
let assignments: Vec<String> = fields
.iter()
.filter(|f| !f.binding_excluded)
.map(|f| {
let binding_name = field_renames
.and_then(|r| r.get(&f.name))
.map_or_else(|| f.name.as_str(), |s| s.as_str());
if f.cfg.is_some() {
if never_skip_cfg_field_names.contains(&f.name) {
if f.optional || matches!(&f.ty, TypeRef::Optional(_)) {
return struct_field_init(binding_name, &f.name);
}
return format!("{}: {}.unwrap_or_default()", binding_name, f.name);
}
return format!("{}: Default::default()", binding_name);
}
if (option_duration_on_defaults && matches!(f.ty, TypeRef::Duration)) || optionalize_all_defaults {
return struct_field_init(binding_name, &f.name);
}
if f.optional || matches!(&f.ty, TypeRef::Optional(_)) {
struct_field_init(binding_name, &f.name)
} else if let Some(ref typed_default) = f.typed_default {
match typed_default {
DefaultValue::Empty => {
format!("{}: {}.unwrap_or_default()", binding_name, f.name)
}
DefaultValue::EnumVariant(_) if core_default_field_access(f, typ).is_none() => {
format!("{}: {}.unwrap_or_default()", binding_name, f.name)
}
_ => {
let default_val = format_default_value(f, typ, &type_mapper(&f.ty));
match typed_default {
DefaultValue::BoolLiteral(_)
| DefaultValue::IntLiteral(_)
| DefaultValue::FloatLiteral(_) => {
format!("{}: {}.unwrap_or({})", binding_name, f.name, default_val)
}
_ => unwrap_or_else_default(binding_name, &f.name, &default_val),
}
}
}
} else {
format!("{}: {}.unwrap_or_default()", binding_name, f.name)
}
})
.collect();
let single_line = params.join(", ");
let param_list = if single_line.len() > 100 {
format!("\n {},\n ", params.join(",\n "))
} else {
single_line
};
(param_list, defaults, assignments.join(", "))
}
#[cfg(test)]
mod tests;