diff --git a/crates/oxc_ast/src/generated/derive_estree.rs b/crates/oxc_ast/src/generated/derive_estree.rs index 1f783c99a4c06..9427683a9707a 100644 --- a/crates/oxc_ast/src/generated/derive_estree.rs +++ b/crates/oxc_ast/src/generated/derive_estree.rs @@ -4,7 +4,7 @@ #![allow(unused_imports, clippy::match_same_arms, clippy::semicolon_if_nothing_returned)] use oxc_estree::{ - Concat2, ESTree, FlatStructSerializer, JsonSafeString, Serializer, StructSerializer, + Concat2, Concat3, ESTree, FlatStructSerializer, JsonSafeString, Serializer, StructSerializer, }; use crate::ast::comment::*; diff --git a/crates/oxc_estree/src/serialize/concat.rs b/crates/oxc_estree/src/serialize/concat.rs index 927ea93aba240..ec099b15103b6 100644 --- a/crates/oxc_estree/src/serialize/concat.rs +++ b/crates/oxc_estree/src/serialize/concat.rs @@ -26,3 +26,20 @@ impl ESTree for Concat2<'_, C1, C2> { seq.end(); } } + +/// Helper struct for concatenating 3 elements into a sequence. +pub struct Concat3<'t, C1: ConcatElement, C2: ConcatElement, C3: ConcatElement>( + pub &'t C1, + pub &'t C2, + pub &'t C3, +); + +impl ESTree for Concat3<'_, C1, C2, C3> { + fn serialize(&self, serializer: S) { + let mut seq = serializer.serialize_sequence(); + self.0.push_to_sequence(&mut seq); + self.1.push_to_sequence(&mut seq); + self.2.push_to_sequence(&mut seq); + seq.end(); + } +} diff --git a/crates/oxc_estree/src/serialize/mod.rs b/crates/oxc_estree/src/serialize/mod.rs index 275182fc8e9b4..1e32cc11f32a7 100644 --- a/crates/oxc_estree/src/serialize/mod.rs +++ b/crates/oxc_estree/src/serialize/mod.rs @@ -20,7 +20,7 @@ use formatter::{CompactFormatter, Formatter, PrettyFormatter}; use sequences::ESTreeSequenceSerializer; use structs::ESTreeStructSerializer; -pub use concat::{Concat2, ConcatElement}; +pub use concat::{Concat2, Concat3, ConcatElement}; pub use sequences::SequenceSerializer; pub use strings::{JsonSafeString, LoneSurrogatesString}; pub use structs::{FlatStructSerializer, StructSerializer}; diff --git a/crates/oxc_span/src/generated/derive_estree.rs b/crates/oxc_span/src/generated/derive_estree.rs index b8e9e4c798be0..f82c5f70cc9ec 100644 --- a/crates/oxc_span/src/generated/derive_estree.rs +++ b/crates/oxc_span/src/generated/derive_estree.rs @@ -4,7 +4,7 @@ #![allow(unused_imports, clippy::match_same_arms, clippy::semicolon_if_nothing_returned)] use oxc_estree::{ - Concat2, ESTree, FlatStructSerializer, JsonSafeString, Serializer, StructSerializer, + Concat2, Concat3, ESTree, FlatStructSerializer, JsonSafeString, Serializer, StructSerializer, }; use crate::source_type::*; diff --git a/crates/oxc_syntax/src/generated/derive_estree.rs b/crates/oxc_syntax/src/generated/derive_estree.rs index c4ebb07acfbc9..40139563a60e0 100644 --- a/crates/oxc_syntax/src/generated/derive_estree.rs +++ b/crates/oxc_syntax/src/generated/derive_estree.rs @@ -4,7 +4,7 @@ #![allow(unused_imports, clippy::match_same_arms, clippy::semicolon_if_nothing_returned)] use oxc_estree::{ - Concat2, ESTree, FlatStructSerializer, JsonSafeString, Serializer, StructSerializer, + Concat2, Concat3, ESTree, FlatStructSerializer, JsonSafeString, Serializer, StructSerializer, }; use crate::module_record::*; diff --git a/napi/parser/src/generated/derive_estree.rs b/napi/parser/src/generated/derive_estree.rs index 658e7977d1ac1..46ed2e71ee925 100644 --- a/napi/parser/src/generated/derive_estree.rs +++ b/napi/parser/src/generated/derive_estree.rs @@ -4,7 +4,7 @@ #![allow(unused_imports, clippy::match_same_arms, clippy::semicolon_if_nothing_returned)] use oxc_estree::{ - Concat2, ESTree, FlatStructSerializer, JsonSafeString, Serializer, StructSerializer, + Concat2, Concat3, ESTree, FlatStructSerializer, JsonSafeString, Serializer, StructSerializer, }; use crate::raw_transfer_types::*; diff --git a/tasks/ast_tools/src/derives/estree.rs b/tasks/ast_tools/src/derives/estree.rs index 935f955548e5f..7ab4cc970ed9e 100644 --- a/tasks/ast_tools/src/derives/estree.rs +++ b/tasks/ast_tools/src/derives/estree.rs @@ -71,7 +71,8 @@ impl Derive for DeriveESTree { ///@@line_break use oxc_estree::{ - Concat2, ESTree, FlatStructSerializer, JsonSafeString, Serializer, StructSerializer, + Concat2, Concat3, ESTree, FlatStructSerializer, + JsonSafeString, Serializer, StructSerializer, }; } } @@ -152,8 +153,8 @@ fn parse_estree_attr(location: AttrLocation, part: AttrPart) -> Result<()> { AttrPart::String("via", value) => { struct_def.fields[field_index].estree.via = Some(value); } - AttrPart::String("append_to", value) => { - // Find field this field is to be appended to + AttrPart::String(attr @ ("prepend_to" | "append_to"), value) => { + // Find field this field is to be prepended/appended to let target_field_index = struct_def .fields .iter() @@ -162,15 +163,20 @@ fn parse_estree_attr(location: AttrLocation, part: AttrPart) -> Result<()> { .map(|(field_index, _)| field_index) .ok_or(())?; if target_field_index == field_index { - // Can't append field to itself + // Can't prepend/append field to itself return Err(()); } let target_field = &mut struct_def.fields[target_field_index]; - if target_field.estree.append_field_index.is_some() { - // Can't append twice to same field + let other_field_index_mut = if attr == "prepend_to" { + &mut target_field.estree.prepend_field_index + } else { + &mut target_field.estree.append_field_index + }; + if other_field_index_mut.is_some() { + // Can't prepend/append twice to same field return Err(()); } - target_field.estree.append_field_index = Some(field_index); + *other_field_index_mut = Some(field_index); struct_def.fields[field_index].estree.skip = true; } AttrPart::String("ts_type", value) => { @@ -441,6 +447,18 @@ impl<'s> StructSerializerGenerator<'s> { let value = if let Some(converter_name) = &field.estree.via { let converter_path = get_converter_path(converter_name, self.krate, self.schema); quote!( #converter_path(#self_path) ) + } else if let Some(prepend_field_index) = field.estree.prepend_field_index { + let prepend_from_ident = struct_def.fields[prepend_field_index].ident(); + if let Some(append_field_index) = field.estree.append_field_index { + let append_from_ident = struct_def.fields[append_field_index].ident(); + quote! { + Concat3(&#self_path.#prepend_from_ident, &#self_path.#field_name_ident, &#self_path.#append_from_ident) + } + } else { + quote! { + Concat2(&#self_path.#prepend_from_ident, &#self_path.#field_name_ident) + } + } } else if let Some(append_field_index) = field.estree.append_field_index { let append_from_ident = struct_def.fields[append_field_index].ident(); quote! { diff --git a/tasks/ast_tools/src/generators/raw_transfer.rs b/tasks/ast_tools/src/generators/raw_transfer.rs index 05f84c9280ad4..75975be5221f4 100644 --- a/tasks/ast_tools/src/generators/raw_transfer.rs +++ b/tasks/ast_tools/src/generators/raw_transfer.rs @@ -305,40 +305,62 @@ impl<'s> StructDeserializerGenerator<'s> { return; } - let value_fn = field_type.deser_name(self.schema); - let pos = pos_offset(field_offset); - let mut value = format!("{value_fn}({pos})"); - - if let Some(appended_field_index) = field.estree.append_field_index { - self.preamble.push(format!("const {field_name} = {value};")); - - let appended_field = &struct_def.fields[appended_field_index]; - let appended_field_type = appended_field.type_def(self.schema); - match appended_field_type { - TypeDef::Vec(vec_def) => { - let appended_field_fn = vec_def.deser_name(self.schema); - let appended_pos = pos_offset(struct_offset + appended_field.offset_64()); - self.preamble.push(format!( - "{field_name}.push(...{appended_field_fn}({appended_pos}));" - )); - } - TypeDef::Option(option_def) => { - let appended_field_name = get_struct_field_name(appended_field).to_string(); - let appended_field_fn = option_def.deser_name(self.schema); - let appended_pos = pos_offset(struct_offset + appended_field.offset_64()); - self.preamble.push(format!(" - const {appended_field_name} = {appended_field_fn}({appended_pos}); - if ({appended_field_name} !== null) {field_name}.push({appended_field_name}); - ")); + // Get fields to concatenate + // (if fields marked `#[estree(prepend_to)]` or `#[estree(append_to)]` targeting this field) + let mut concat_fields = [field; 3]; + let mut concat_field_count = 1; + if let Some(prepend_field_index) = field.estree.prepend_field_index { + concat_fields[0] = &struct_def.fields[prepend_field_index]; + concat_field_count = 2; + } + if let Some(append_field_index) = field.estree.append_field_index { + concat_fields[concat_field_count] = &struct_def.fields[append_field_index]; + concat_field_count += 1; + } + + let value = if concat_field_count > 1 { + // Concatenate fields + for (index, &field) in concat_fields[..concat_field_count].iter().enumerate() { + let field_pos = pos_offset(struct_offset + field.offset_64()); + match field.type_def(self.schema) { + TypeDef::Vec(vec_def) => { + let field_fn = vec_def.deser_name(self.schema); + if index == 0 { + self.preamble + .push(format!("const {field_name} = {field_fn}({field_pos});")); + } else { + self.preamble + .push(format!("{field_name}.push(...{field_fn}({field_pos}));")); + } + } + TypeDef::Option(option_def) => { + let option_field_name = get_struct_field_name(field).to_string(); + let field_fn = option_def.deser_name(self.schema); + self.preamble + .push(format!("const {option_field_name} = {field_fn}({field_pos});")); + if index == 0 { + self.preamble.push(format!( + "const {field_name} = {option_field_name} === null ? [] : [{option_field_name}];" + )); + } else { + self.preamble.push(format!( + "if ({option_field_name} !== null) {field_name}.push({option_field_name});" + )); + } + } + _ => panic!("Cannot append: `{}::{}`", struct_def.name(), field.name()), } - _ => panic!("Cannot append: `{}::{}`", struct_def.name(), field.name()), } - value.clone_from(&field_name); + field_name.clone() } else if let Some(converter_name) = &field.estree.via { let converter = self.schema.meta_by_name(converter_name); - value = self.apply_converter(converter, struct_def, struct_offset).unwrap(); - } + self.apply_converter(converter, struct_def, struct_offset).unwrap() + } else { + let value_fn = field_type.deser_name(self.schema); + let pos = pos_offset(field_offset); + format!("{value_fn}({pos})") + }; self.fields.insert(field_name, value); } diff --git a/tasks/ast_tools/src/generators/typescript.rs b/tasks/ast_tools/src/generators/typescript.rs index b833f0c0f8fce..dcdeeb2cbb016 100644 --- a/tasks/ast_tools/src/generators/typescript.rs +++ b/tasks/ast_tools/src/generators/typescript.rs @@ -192,50 +192,50 @@ fn generate_ts_type_def_for_struct_field_impl<'s>( output_as_type: &mut bool, schema: &'s Schema, ) { - let field_type_name = if let Some(append_field_index) = field.estree.append_field_index { - let appended_field = &struct_def.fields[append_field_index]; - let appended_type = appended_field.type_def(schema); - let appended_type = match appended_type { - TypeDef::Option(option_def) => option_def.inner_type(schema), - TypeDef::Vec(vec_def) => vec_def.inner_type(schema), - _ => panic!( - "Appended field must be `Option` or `Vec`: `{}::{}`", - struct_def.name(), - appended_field.name() - ), - }; - let appended_type_name = ts_type_name(appended_type, schema); + // Get fields to concatenate + // (if fields marked `#[estree(prepend_to)]` or `#[estree(append_to)]` targeting this field) + let mut concat_fields = [field; 3]; + let mut concat_field_count = 1; + if let Some(prepend_field_index) = field.estree.prepend_field_index { + concat_fields[0] = &struct_def.fields[prepend_field_index]; + concat_field_count = 2; + } + if let Some(append_field_index) = field.estree.append_field_index { + concat_fields[concat_field_count] = &struct_def.fields[append_field_index]; + concat_field_count += 1; + } - let field_type = field.type_def(schema); - let (vec_def, is_option) = match field_type { - TypeDef::Vec(vec_def) => (vec_def, false), - TypeDef::Option(option_def) => { - let vec_def = option_def.inner_type(schema).as_vec().unwrap(); - (vec_def, true) + let field_type_name = if concat_field_count > 1 { + // Combine types of concatenated fields + let mut field_type_name = "Array<".to_string(); + let mut include_null = false; + for (index, &field) in concat_fields[..concat_field_count].iter().enumerate() { + let field_type = match field.type_def(schema) { + TypeDef::Option(option_def) => option_def.inner_type(schema), + TypeDef::Vec(vec_def) => match vec_def.inner_type(schema) { + TypeDef::Option(option_def) => { + include_null = true; + option_def.inner_type(schema) + } + field_type => field_type, + }, + _ => panic!( + "Appended field must be `Option` or `Vec`: `{}::{}`", + struct_def.name(), + field.name() + ), + }; + + if index > 0 { + field_type_name.push_str(" | "); } - _ => panic!( - "Can only append a field to a `Vec` or `Option>`: `{}::{}`", - struct_def.name(), - field.name() - ), - }; - - let mut inner_type = vec_def.inner_type(schema); - let mut inner_is_option = false; - if let TypeDef::Option(option_def) = inner_type { - inner_is_option = true; - inner_type = option_def.inner_type(schema); + field_type_name.push_str(&ts_type_name(field_type, schema)); } - let inner_type_name = ts_type_name(inner_type, schema); - let mut field_type_name = format!("Array<{inner_type_name} | {appended_type_name}"); - if inner_is_option { + + if include_null { field_type_name.push_str(" | null"); } field_type_name.push('>'); - if is_option { - field_type_name.push_str(" | null"); - } - Cow::Owned(field_type_name) } else if let Some(converter_name) = &field.estree.via { let Some(ts_type) = get_ts_type_for_converter(converter_name, schema) else { diff --git a/tasks/ast_tools/src/schema/extensions/estree.rs b/tasks/ast_tools/src/schema/extensions/estree.rs index f2ef11df8e137..3571c9dcac99d 100644 --- a/tasks/ast_tools/src/schema/extensions/estree.rs +++ b/tasks/ast_tools/src/schema/extensions/estree.rs @@ -66,6 +66,8 @@ pub struct ESTreeStructField { pub via: Option, /// TS type of this field. pub ts_type: Option, + /// Field index of field to prepend to this one + pub prepend_field_index: Option, /// Field index of field to append to this one pub append_field_index: Option, /// Skip this struct field.