use std::collections::{HashMap, HashSet};
use crate::codegen::shared::binding_fields;
use crate::core::ir::{FieldDef, TypeDef, TypeRef};
use crate::e2e::codegen::call_ir::named_type;
use super::parse::{parse_path, segment_name};
use super::types::IrResultFieldMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum OptionalityRule {
DeclaredType,
Napi,
}
impl OptionalityRule {
pub(crate) fn for_language(language: &str) -> Self {
match language {
"node" | "typescript" => Self::Napi,
_ => Self::DeclaredType,
}
}
fn applies_to(self, field: &FieldDef, owner: &TypeDef) -> bool {
match self {
Self::DeclaredType => field.optional,
Self::Napi => crate::backends::napi::napi_field_is_optional(field, owner),
}
}
}
pub(super) fn build_ir_result_field_map(type_defs: &[TypeDef], rule: OptionalityRule) -> IrResultFieldMap {
let struct_names: HashSet<&str> = type_defs.iter().map(|type_def| type_def.name.as_str()).collect();
let mut field_types: HashMap<String, HashMap<String, String>> = HashMap::new();
let mut optional_fields: HashMap<String, HashSet<String>> = HashMap::new();
let mut declared_fields: HashMap<String, HashSet<String>> = HashMap::new();
let mut unresolvable_named_fields: HashMap<String, HashSet<String>> = HashMap::new();
let mut display_safe_fields: HashMap<String, HashSet<String>> = HashMap::new();
for type_def in type_defs {
for field in binding_fields(&type_def.fields) {
declared_fields
.entry(type_def.name.clone())
.or_default()
.insert(field.name.clone());
if rule.applies_to(field, type_def) {
optional_fields
.entry(type_def.name.clone())
.or_default()
.insert(field.name.clone());
}
if type_ref_is_display_safe(&field.ty) {
display_safe_fields
.entry(type_def.name.clone())
.or_default()
.insert(field.name.clone());
}
let Some(named) = named_type(&field.ty) else {
continue;
};
if struct_names.contains(named) {
field_types
.entry(type_def.name.clone())
.or_default()
.insert(field.name.clone(), named.to_string());
} else {
unresolvable_named_fields
.entry(type_def.name.clone())
.or_default()
.insert(field.name.clone());
}
}
}
IrResultFieldMap {
field_types,
optional_fields,
declared_fields,
unresolvable_named_fields,
display_safe_fields,
root_type: None,
}
}
pub(super) fn type_ref_is_display_safe(ty: &TypeRef) -> bool {
matches!(ty, TypeRef::String | TypeRef::Char | TypeRef::Primitive(_))
}
pub(super) fn is_optional_path(map: &IrResultFieldMap, path: &str) -> bool {
let Some((owner, leaf)) = walk_to_owner(map, path) else {
return false;
};
map.optional_fields
.get(owner)
.is_some_and(|fields| fields.contains(&leaf))
}
pub(super) fn leaf_is_named_type(map: &IrResultFieldMap, path: &str) -> bool {
let Some((owner, leaf)) = walk_to_owner(map, path) else {
return false;
};
map.field_types
.get(owner)
.is_some_and(|fields| fields.contains_key(&leaf))
}
pub(super) fn root_declares_first_segment(map: &IrResultFieldMap, first_segment: &str) -> Option<bool> {
let root = map.root_type.as_deref()?;
let declared = map.declared_fields.get(root)?;
Some(declared.contains(first_segment))
}
pub(super) fn root_declares_path(map: &IrResultFieldMap, path: &str) -> Option<bool> {
let root = map.root_type.as_deref()?;
type_declares_path(map, root, path)
}
pub(super) fn type_declares_path(map: &IrResultFieldMap, owner_type: &str, path: &str) -> Option<bool> {
let segments = parse_path(path);
let (last, prefix) = segments.split_last()?;
let mut owner = owner_type;
for segment in prefix {
let name = segment_name(segment)?;
if !map.declared_fields.get(owner)?.contains(name) {
return Some(false);
}
owner = map.field_types.get(owner)?.get(name)?.as_str();
}
Some(map.declared_fields.get(owner)?.contains(segment_name(last)?))
}
pub(super) fn path_crosses_unwalkable_field(map: &IrResultFieldMap, path: &str) -> bool {
let Some(root) = map.root_type.as_deref() else {
return false;
};
let segments = parse_path(path);
let Some((_last, prefix)) = segments.split_last() else {
return false;
};
let mut owner = root;
for segment in prefix {
let Some(name) = segment_name(segment) else {
return false;
};
let Some(declared) = map.declared_fields.get(owner) else {
return false;
};
if !declared.contains(name) {
return false;
}
if map
.unresolvable_named_fields
.get(owner)
.is_some_and(|fields| fields.contains(name))
{
return true;
}
match map.field_types.get(owner).and_then(|fields| fields.get(name)) {
Some(next) => owner = next.as_str(),
None => return false,
}
}
false
}
fn walk_to_owner<'a>(map: &'a IrResultFieldMap, path: &str) -> Option<(&'a str, String)> {
let root = map.root_type.as_deref()?;
let segments = parse_path(path);
let (last, prefix) = segments.split_last()?;
let mut owner = root;
for segment in prefix {
let name = segment_name(segment)?;
owner = map.field_types.get(owner)?.get(name)?.as_str();
}
Some((owner, segment_name(last)?.to_string()))
}
#[cfg(test)]
mod display_safe_field_tests {
use super::*;
use crate::core::ir::PrimitiveType;
fn field(name: &str, ty: TypeRef) -> FieldDef {
FieldDef {
name: name.to_string(),
ty,
..FieldDef::default()
}
}
#[test]
fn type_ref_is_display_safe_only_for_bare_scalars() {
let cases: &[(&str, TypeRef, bool)] = &[
("string", TypeRef::String, true),
("char", TypeRef::Char, true),
("bool", TypeRef::Primitive(PrimitiveType::Bool), true),
("i32", TypeRef::Primitive(PrimitiveType::I32), true),
("f64", TypeRef::Primitive(PrimitiveType::F64), true),
(
"option_of_string_is_unsafe",
TypeRef::Optional(Box::new(TypeRef::String)),
false,
),
(
"vec_of_string_is_unsafe",
TypeRef::Vec(Box::new(TypeRef::String)),
false,
),
(
"nested_vec_of_string_is_unsafe",
TypeRef::Vec(Box::new(TypeRef::Vec(Box::new(TypeRef::String)))),
false,
),
(
"map_is_unsafe",
TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::String)),
false,
),
("bytes_is_unsafe", TypeRef::Bytes, false),
("named_is_unsafe", TypeRef::Named("Widget".to_string()), false),
("path_is_unsafe", TypeRef::Path, false),
("json_is_unsafe", TypeRef::Json, false),
("duration_is_unsafe", TypeRef::Duration, false),
("unit_is_unsafe", TypeRef::Unit, false),
];
for (name, ty, expected) in cases {
assert_eq!(
type_ref_is_display_safe(ty),
*expected,
"case `{name}` expected display-safe={expected}"
);
}
}
#[test]
fn build_ir_result_field_map_populates_display_safe_fields_per_owner_type() {
let type_defs = vec![TypeDef {
name: "Table".to_string(),
fields: vec![
field("name", TypeRef::String),
field("cells", TypeRef::Vec(Box::new(TypeRef::Vec(Box::new(TypeRef::String))))),
],
..TypeDef::default()
}];
let map = build_ir_result_field_map(&type_defs, OptionalityRule::DeclaredType);
assert!(map.display_safe_fields.get("Table").is_some_and(|f| f.contains("name")));
assert!(
!map.display_safe_fields
.get("Table")
.is_some_and(|f| f.contains("cells"))
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::ir::{FieldDef, TypeDef};
fn field(name: &str, ty: crate::core::ir::TypeRef) -> FieldDef {
FieldDef {
name: name.to_string(),
ty,
..FieldDef::default()
}
}
fn type_defs_with_unresolvable_variant_field() -> Vec<TypeDef> {
vec![
TypeDef {
name: "Envelope".to_string(),
fields: vec![field(
"metadata",
crate::core::ir::TypeRef::Named("Metadata".to_string()),
)],
..TypeDef::default()
},
TypeDef {
name: "Metadata".to_string(),
fields: vec![
field("format", crate::core::ir::TypeRef::Named("VariantInfo".to_string())),
field("title", crate::core::ir::TypeRef::String),
],
..TypeDef::default()
},
]
}
fn anchored_map(type_defs: &[TypeDef]) -> IrResultFieldMap {
let mut map = build_ir_result_field_map(type_defs, OptionalityRule::DeclaredType);
map.root_type = Some("Envelope".to_string());
map
}
#[test]
fn a_path_continuing_past_a_declared_but_unwalkable_field_crosses() {
let map = anchored_map(&type_defs_with_unresolvable_variant_field());
assert!(path_crosses_unwalkable_field(&map, "metadata.format.variant.detail"));
}
#[test]
fn a_path_stopping_at_the_unwalkable_field_does_not_cross() {
let map = anchored_map(&type_defs_with_unresolvable_variant_field());
assert!(!path_crosses_unwalkable_field(&map, "metadata.format"));
}
#[test]
fn a_path_through_a_real_struct_field_does_not_cross() {
let map = anchored_map(&type_defs_with_unresolvable_variant_field());
assert!(!path_crosses_unwalkable_field(&map, "metadata.title"));
}
#[test]
fn a_path_through_an_undeclared_segment_does_not_cross() {
let map = anchored_map(&type_defs_with_unresolvable_variant_field());
assert!(!path_crosses_unwalkable_field(&map, "not_a_real_field.anything"));
}
#[test]
fn a_path_through_a_field_with_no_named_type_at_all_does_not_cross() {
let type_defs = vec![TypeDef {
name: "Envelope".to_string(),
fields: vec![field("payload", crate::core::ir::TypeRef::Json)],
..TypeDef::default()
}];
let map = anchored_map(&type_defs);
assert!(!path_crosses_unwalkable_field(&map, "payload.anything"));
}
#[test]
fn no_anchored_root_never_crosses() {
let mut map = build_ir_result_field_map(
&type_defs_with_unresolvable_variant_field(),
OptionalityRule::DeclaredType,
);
map.root_type = None;
assert!(!path_crosses_unwalkable_field(&map, "metadata.format.variant.detail"));
}
}