feat(ast_tools): support #[estree(prepend_to)] (#10849)

Add an `#[estree(prepend_to = ...)]` attribute to accompany `#[estree(append_to = ...)]`.

* `#[estree(prepend_to = target)]` prepends the field to *start* of the target field.
* `#[estree(append_to = target)]` appends the field to *end* of the target field.

This also allow both prepending *and* appending to the same field. e.g. combining `TSThisParameter`, `FormalParameter`s, and `BindingRestElement` into a single array.
This commit is contained in:
overlookmotel committed 2025-05-07 13:25:17 +00:00
1 parent b16331ecfa
commit d066516b6a
10 files changed
+137 -78

No files matched your search

@@ -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::*;
+17
View File
@@ -26,3 +26,20 @@ impl<C1: ConcatElement, C2: ConcatElement> 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<C1: ConcatElement, C2: ConcatElement, C3: ConcatElement> ESTree for Concat3<'_, C1, C2, C3> {
fn serialize<S: Serializer>(&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();
}
}
+1 -1
View File
@@ -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};
@@ -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::*;
@@ -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::*;
+1 -1
View File
@@ -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::*;
+25 -7
View File
@@ -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! {
+50 -28
View File
@@ -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})");
// 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;
}
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}));"
));
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()),
}
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});
"));
}
_ => 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);
}
+38 -38
View File
@@ -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<T>` or `Vec<T>`: `{}::{}`",
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<T>` or `Vec<T>`: `{}::{}`",
struct_def.name(),
field.name()
),
};
if index > 0 {
field_type_name.push_str(" | ");
}
_ => panic!(
"Can only append a field to a `Vec<T>` or `Option<Vec<T>>`: `{}::{}`",
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 {
@@ -66,6 +66,8 @@ pub struct ESTreeStructField {
pub via: Option<String>,
/// TS type of this field.
pub ts_type: Option<String>,
/// Field index of field to prepend to this one
pub prepend_field_index: Option<usize>,
/// Field index of field to append to this one
pub append_field_index: Option<usize>,
/// Skip this struct field.