use crate::codegen::conversions::ConversionConfig;
use crate::codegen::shared::binding_fields;
use crate::core::ir::{EnumDef, TypeDef, TypeRef};
use ahash::AHashSet;
use minijinja::context;
use super::params::references_named_type;
#[allow(dead_code)]
pub(crate) fn gen_convertible_enum_tainted(
types: &[TypeDef],
enum_tainted: &AHashSet<String>,
enum_names: &AHashSet<String>,
enums: &[EnumDef],
) -> AHashSet<String> {
let mut unconvertible: AHashSet<String> = AHashSet::new();
for typ in types {
if !enum_tainted.contains(&typ.name) {
continue;
}
for field in binding_fields(&typ.fields) {
if let Some(enum_name) = get_direct_enum_named(&field.ty, enum_names) {
if let Some(enum_def) = enums.iter().find(|e| e.name == enum_name) {
if enum_def.variants.iter().any(|v| !v.fields.is_empty()) {
unconvertible.insert(typ.name.clone());
}
}
}
}
}
let mut changed = true;
while changed {
changed = false;
for typ in types {
if !enum_tainted.contains(&typ.name) || unconvertible.contains(&typ.name) {
continue;
}
if binding_fields(&typ.fields).any(|f| references_named_type(&f.ty, &unconvertible)) {
unconvertible.insert(typ.name.clone());
changed = true;
}
}
}
enum_tainted
.iter()
.filter(|name| !unconvertible.contains(name.as_str()))
.cloned()
.collect()
}
pub(crate) fn gen_enum_tainted_from_binding_to_core(
typ: &TypeDef,
core_import: &str,
enum_names: &AHashSet<String>,
_enum_tainted: &AHashSet<String>,
config: &ConversionConfig,
enums: &[EnumDef],
bridge_type_aliases: &AHashSet<String>,
) -> String {
let core_path = crate::codegen::conversions::core_type_path(typ, core_import);
if typ.has_lifetime_params {
if let Some(code) =
crate::codegen::conversions::gen_from_lifetime_type_constructor(typ, &core_path, &typ.name, config)
{
return code;
}
}
let mut fields: Vec<(&str, String)> = Vec::new();
for field in &typ.fields {
if field.binding_excluded {
if !typ.has_default {
fields.push((field.name.as_str(), "Default::default()".to_string()));
continue;
}
continue;
}
if field.cfg.is_some()
&& !config.never_skip_cfg_field_names.contains(&field.name)
&& config.strip_cfg_fields_from_binding_struct
{
continue;
}
let name = &field.name;
let is_bridge_named = match &field.ty {
crate::core::ir::TypeRef::Named(n) => bridge_type_aliases.contains(n.as_str()),
crate::core::ir::TypeRef::Optional(inner) => {
matches!(inner.as_ref(), crate::core::ir::TypeRef::Named(n) if bridge_type_aliases.contains(n.as_str()))
}
_ => false,
};
if is_bridge_named {
fields.push((name.as_str(), format!("val.{name}.map(|v| (*v.inner).clone())")));
} else if field.sanitized {
fields.push((name.as_str(), "Default::default()".to_string()));
} else if let Some(enum_name) = get_direct_enum_named(&field.ty, enum_names) {
let conversion =
gen_string_to_enum_expr(&format!("val.{name}"), &enum_name, field.optional, enums, core_import);
fields.push((name.as_str(), conversion));
} else if let Some(enum_name) = get_vec_enum_named(&field.ty, enum_names) {
let elem_conversion = gen_string_to_enum_expr("s", &enum_name, false, enums, core_import);
let conversion = if field.optional {
format!("val.{name}.map(|v| v.into_iter().map(|s| {elem_conversion}).collect())")
} else {
format!("val.{name}.into_iter().map(|s| {elem_conversion}).collect()")
};
fields.push((name.as_str(), conversion));
} else if !field.optional
&& matches!(field.ty, TypeRef::Duration)
&& config.option_duration_on_defaults
&& typ.has_default
{
let cast = if config.cast_large_ints_to_i64 { " as u64" } else { "" };
let conversion = crate::backends::php::template_env::render(
"php_duration_default_expr.jinja",
context! {
value_expr => &format!("val.{name}"),
cast => cast,
core_type => &core_path,
field_name => name.as_str(),
},
);
fields.push((name.as_str(), conversion));
} else if matches!(field.ty, TypeRef::Bytes)
|| matches!(&field.ty, TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Bytes))
{
let conversion = if field.optional {
format!("val.{name}.map(|v| v.into())")
} else {
format!("val.{name}.into()")
};
fields.push((name.as_str(), conversion));
} else {
let conversion =
crate::codegen::conversions::field_conversion_to_core_cfg(name, &field.ty, field.optional, config);
let conversion = if let Some(newtype_path) = &field.newtype_wrapper {
if let Some(expr) = conversion.strip_prefix(&format!("{name}: ")) {
match &field.ty {
TypeRef::Optional(_) => format!("{name}: ({expr}).map({newtype_path})"),
TypeRef::Vec(_) => {
let inner_expr = if let Some(prefix) = expr.strip_suffix(".collect()") {
format!("{prefix}.collect::<Vec<_>>()")
} else {
expr.to_string()
};
format!("{name}: ({inner_expr}).into_iter().map({newtype_path}).collect()")
}
_ if field.optional => format!("{name}: ({expr}).map({newtype_path})"),
_ => format!("{name}: {newtype_path}({expr})"),
}
} else {
conversion
}
} else {
conversion
};
let conversion = if field.is_boxed && matches!(&field.ty, TypeRef::Named(_)) {
if let Some(expr) = conversion.strip_prefix(&format!("{name}: ")) {
if field.optional {
format!("{name}: {expr}.map(Box::new)")
} else {
format!("{name}: Box::new({expr})")
}
} else {
conversion
}
} else {
conversion
};
let conversion = crate::codegen::conversions::apply_core_wrapper_to_core(
&conversion,
name,
&field.core_wrapper,
&field.vec_inner_core_wrapper,
field.optional,
);
let field_expr = conversion.strip_prefix(&format!("{name}: ")).unwrap_or(&conversion);
fields.push((name.as_str(), field_expr.to_string()));
}
}
if typ.has_private_fields {
let assignments: Vec<_> = fields
.into_iter()
.filter(|(_, expr)| expr != "Default::default()")
.map(
|(core_field, expr)| crate::codegen::conversions::construction::FieldAssign {
core_field: core_field.to_string(),
expr,
},
)
.collect();
return crate::codegen::conversions::construction::gen_private_field_from_impl(
&crate::codegen::conversions::construction::PrivateFieldImpl {
core_path: &core_path,
binding_name: &typ.name,
param: "val",
has_default: typ.has_default,
assignments: &assignments,
allow_attrs: &["clippy::useless_conversion"],
},
);
}
let emit_default_spread = typ.has_default;
let mut out = crate::backends::php::template_env::render(
"php_impl_from_begin.jinja",
context! {
binding_type => &typ.name,
core_type => &core_path,
emit_spread => emit_default_spread,
},
);
for &(field_name, ref field_expr) in &fields {
out.push_str(&crate::backends::php::template_env::render(
"php_struct_field_assignment.jinja",
context! {
field_name => field_name,
field_expr => field_expr,
},
));
}
out.push_str(&crate::backends::php::template_env::render(
"php_impl_from_end.jinja",
context! {
emit_spread => emit_default_spread,
},
));
out
}
pub(super) fn get_direct_enum_named(ty: &TypeRef, enum_names: &AHashSet<String>) -> Option<String> {
match ty {
TypeRef::Named(name) if enum_names.contains(name.as_str()) => Some(name.clone()),
TypeRef::Optional(inner) => get_direct_enum_named(inner, enum_names),
_ => None,
}
}
pub(super) fn get_vec_enum_named(ty: &TypeRef, enum_names: &AHashSet<String>) -> Option<String> {
match ty {
TypeRef::Vec(inner) => get_direct_enum_named(inner, enum_names),
TypeRef::Optional(inner) => get_vec_enum_named(inner, enum_names),
_ => None,
}
}
pub(super) fn gen_string_to_enum_expr(
val_expr: &str,
enum_name: &str,
optional: bool,
enums: &[EnumDef],
core_import: &str,
) -> String {
let enum_def = match enums.iter().find(|e| e.name == enum_name) {
Some(e) => e,
None => return "Default::default()".to_string(),
};
let core_enum_path = crate::codegen::conversions::core_enum_path(enum_def, core_import);
if enum_def.variants.is_empty() {
return "Default::default()".to_string();
}
fn variant_expr(core_path: &str, variant: &crate::core::ir::EnumVariant) -> String {
if variant.fields.is_empty() {
crate::backends::php::template_env::render(
"php_enum_variant_unit_expr.jinja",
context! {
core_path => core_path,
variant_name => &variant.name,
},
)
} else if crate::codegen::conversions::is_tuple_variant(&variant.fields) {
let defaults: Vec<&str> = variant.fields.iter().map(|_| "Default::default()").collect();
crate::backends::php::template_env::render(
"php_enum_variant_tuple_expr.jinja",
context! {
core_path => core_path,
variant_name => &variant.name,
defaults => defaults.join(", "),
},
)
} else {
let fields: Vec<String> = variant
.fields
.iter()
.map(|field| {
crate::backends::php::template_env::render(
"php_enum_variant_default_field_expr.jinja",
context! {
field_name => &field.name,
},
)
})
.collect();
crate::backends::php::template_env::render(
"php_enum_variant_struct_expr.jinja",
context! {
core_path => core_path,
variant_name => &variant.name,
fields => fields.join(", "),
},
)
}
}
let has_default_variant = enum_def.variants.iter().any(|v| v.is_default);
let fallback_expr = if has_default_variant {
"Default::default()".to_string()
} else {
variant_expr(&core_enum_path, &enum_def.variants[0])
};
let mut match_arms = String::new();
for variant in &enum_def.variants {
let expr = variant_expr(&core_enum_path, variant);
let wire_name = crate::codegen::naming::wire_variant_value(
&variant.name,
variant.serde_rename.as_deref(),
enum_def.serde_rename_all.as_deref(),
);
let variant_lower = wire_name.to_lowercase();
match_arms.push_str(&crate::backends::php::template_env::render(
"php_enum_string_match_arm.jinja",
context! {
variant_name => &wire_name,
variant_name_lower => &variant_lower,
expr => &expr,
},
));
}
match_arms.push_str(&crate::backends::php::template_env::render(
"php_enum_string_match_fallback_arm.jinja",
context! {
fallback_expr => &fallback_expr,
},
));
if optional {
crate::backends::php::template_env::render(
"php_enum_string_optional_match_expr.jinja",
context! {
val_expr => val_expr,
match_arms => &match_arms,
},
)
} else {
crate::backends::php::template_env::render(
"php_enum_string_match_expr.jinja",
context! {
val_expr => val_expr,
match_arms => &match_arms,
},
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::codegen::conversions::ConversionConfig;
use crate::core::ir::{FieldDef, TypeRef};
fn field(name: &str, binding_excluded: bool) -> FieldDef {
FieldDef {
name: name.to_string(),
ty: TypeRef::String,
optional: false,
binding_excluded,
..Default::default()
}
}
fn typ(name: &str, has_default: bool, fields: Vec<FieldDef>) -> TypeDef {
TypeDef {
name: name.to_string(),
rust_path: format!("crate::{name}"),
fields,
is_clone: true,
has_default,
..Default::default()
}
}
#[test]
fn enum_tainted_binding_excluded_with_default_uses_spread() {
let typ = typ(
"DefaultedWithExcluded",
true,
vec![field("name", false), field("attrs", true)],
);
let cfg = ConversionConfig::default();
let out = gen_enum_tainted_from_binding_to_core(
&typ,
"crate",
&AHashSet::new(),
&AHashSet::new(),
&cfg,
&[],
&AHashSet::new(),
);
assert!(
out.contains("..Default::default()"),
"spread should be emitted when has_default is true; got:\n{out}"
);
assert!(
!out.contains("attrs: Default::default()"),
"binding-excluded field should NOT be explicitly emitted when has_default is true; got:\n{out}"
);
}
#[test]
fn enum_tainted_binding_excluded_without_default_emits_explicit_default() {
let typ = typ(
"NoDefaultEnumTainted",
false,
vec![field("name", false), field("attrs", true)],
);
let cfg = ConversionConfig::default();
let out = gen_enum_tainted_from_binding_to_core(
&typ,
"crate",
&AHashSet::new(),
&AHashSet::new(),
&cfg,
&[],
&AHashSet::new(),
);
assert!(
!out.contains("..Default::default()"),
"spread must NOT be emitted when has_default is false; got:\n{out}"
);
assert!(
out.contains("attrs: Default::default()"),
"binding-excluded field must be explicitly defaulted when has_default is false; got:\n{out}"
);
}
#[test]
fn enum_tainted_fully_mirrored_with_default_still_emits_spread() {
let typ = typ(
"PlainEnumTainted",
true,
vec![field("name", false), field("value", false)],
);
let cfg = ConversionConfig::default();
let out = gen_enum_tainted_from_binding_to_core(
&typ,
"crate",
&AHashSet::new(),
&AHashSet::new(),
&cfg,
&[],
&AHashSet::new(),
);
assert!(
out.contains("..Default::default()"),
"has_default core type must always get the spread trailer; got:\n{out}"
);
assert!(
out.contains("#[allow(clippy::needless_update)]"),
"the spread over a fully-mirrored literal needs the needless_update allow; got:\n{out}"
);
}
#[test]
fn enum_tainted_fully_mirrored_without_default_keeps_exhaustive_literal() {
let typ = typ(
"NoDefaultPlain",
false,
vec![field("name", false), field("value", false)],
);
let cfg = ConversionConfig::default();
let out = gen_enum_tainted_from_binding_to_core(
&typ,
"crate",
&AHashSet::new(),
&AHashSet::new(),
&cfg,
&[],
&AHashSet::new(),
);
assert!(
!out.contains("..Default::default()"),
"the spread trailer must not be emitted when the core type has no Default \
impl — it would fail to compile (E0277); got:\n{out}"
);
}
}