diff --git a/crates/oxc_ast/src/serialize.rs b/crates/oxc_ast/src/serialize.rs index 46fc8c9a68d71..82d71dabddfa9 100644 --- a/crates/oxc_ast/src/serialize.rs +++ b/crates/oxc_ast/src/serialize.rs @@ -17,6 +17,73 @@ use crate::ast::*; /// Constant value that will be serialized as `null` in JSON. pub(crate) const NULL: () = (); +impl Program<'_> { + /// Serialize AST to JSON. + // + // Should not panic if everything is working correctly. + // Serializing into a `Vec` should be infallible. + #[expect(clippy::missing_panics_doc)] + pub fn to_json(&self) -> String { + let buf = Vec::new(); + let ser = self.to_json_into_writer(buf).unwrap(); + let buf = ser.into_inner(); + // SAFETY: `serde_json` outputs valid UTF-8. + // `serde_json::to_string` also uses `from_utf8_unchecked`. + // https://github.com/serde-rs/json/blob/1174c5f57db44c26460951b525c6ede50984b655/src/ser.rs#L2209-L2219 + unsafe { String::from_utf8_unchecked(buf) } + } + + /// Serialize AST into a "black hole" writer. + /// + /// Only useful for testing, to make sure serialization completes successfully. + /// Should be faster than [`Program::to_json`], as does not actually produce any output. + /// + /// # Errors + /// Returns `Err` if serialization fails. + #[doc(hidden)] + pub fn test_to_json(&self) -> Result<(), serde_json::Error> { + struct BlackHole; + + #[expect(clippy::inline_always)] + impl Write for BlackHole { + #[inline(always)] + fn write(&mut self, buf: &[u8]) -> Result { + Ok(buf.len()) + } + + #[inline(always)] + fn flush(&mut self) -> Result<(), std::io::Error> { + Ok(()) + } + } + + self.to_json_into_writer(BlackHole).map(|_| ()) + } + + /// Serialize AST into the provided writer. + fn to_json_into_writer( + &self, + writer: W, + ) -> Result, serde_json::Error> { + let mut ser = serde_json::Serializer::with_formatter(writer, EcmaFormatter); + self.serialize(&mut ser)?; + Ok(ser) + } +} + +/// `serde_json` formatter which uses `ryu_js` to serialize `f64`. +pub struct EcmaFormatter; + +impl serde_json::ser::Formatter for EcmaFormatter { + fn write_f64(&mut self, writer: &mut W, value: f64) -> std::io::Result<()> + where + W: ?Sized + std::io::Write, + { + use oxc_syntax::number::ToJsString; + writer.write_all(value.to_js_string().as_bytes()) + } +} + // -------------------- // Literals // -------------------- @@ -81,72 +148,9 @@ impl Serialize for RegExpPattern<'_> { } } -pub struct EcmaFormatter; - -/// Serialize f64 with `ryu_js` -impl serde_json::ser::Formatter for EcmaFormatter { - fn write_f64(&mut self, writer: &mut W, value: f64) -> std::io::Result<()> - where - W: ?Sized + std::io::Write, - { - use oxc_syntax::number::ToJsString; - writer.write_all(value.to_js_string().as_bytes()) - } -} - -impl Program<'_> { - /// Serialize AST to JSON. - // - // Should not panic if everything is working correctly. - // Serializing into a `Vec` should be infallible. - #[expect(clippy::missing_panics_doc)] - pub fn to_json(&self) -> String { - let buf = Vec::new(); - let ser = self.to_json_into_writer(buf).unwrap(); - let buf = ser.into_inner(); - // SAFETY: `serde_json` outputs valid UTF-8. - // `serde_json::to_string` also uses `from_utf8_unchecked`. - // https://github.com/serde-rs/json/blob/1174c5f57db44c26460951b525c6ede50984b655/src/ser.rs#L2209-L2219 - unsafe { String::from_utf8_unchecked(buf) } - } - - /// Serialize AST into a "black hole" writer. - /// - /// Only useful for testing, to make sure serialization completes successfully. - /// Should be faster than [`Program::to_json`], as does not actually produce any output. - /// - /// # Errors - /// Returns `Err` if serialization fails. - #[doc(hidden)] - pub fn test_to_json(&self) -> Result<(), serde_json::Error> { - struct BlackHole; - - #[expect(clippy::inline_always)] - impl Write for BlackHole { - #[inline(always)] - fn write(&mut self, buf: &[u8]) -> Result { - Ok(buf.len()) - } - - #[inline(always)] - fn flush(&mut self) -> Result<(), std::io::Error> { - Ok(()) - } - } - - self.to_json_into_writer(BlackHole).map(|_| ()) - } - - /// Serialize AST into the provided writer. - fn to_json_into_writer( - &self, - writer: W, - ) -> Result, serde_json::Error> { - let mut ser = serde_json::Serializer::with_formatter(writer, EcmaFormatter); - self.serialize(&mut ser)?; - Ok(ser) - } -} +// -------------------- +// Various +// -------------------- /// Serialize `ArrayExpressionElement::Elision` variant as `null`. impl Serialize for Elision { @@ -205,6 +209,7 @@ impl Serialize for ElementsAndRest<'_, E, R> { } } +/// Wrap an `Option>` so that it's serialized as an empty array (`[]`) if the `Option` is `None`. pub struct OptionVecDefault<'a, 'b, T: Serialize>(pub &'b Option>); impl Serialize for OptionVecDefault<'_, '_, T> { @@ -266,36 +271,10 @@ struct DirectiveAsStatement<'a, 'b> { expression: &'b StringLiteral<'a>, } -impl Serialize for JSXElementName<'_> { - fn serialize(&self, serializer: S) -> Result { - match self { - Self::Identifier(ident) => ident.serialize(serializer), - Self::IdentifierReference(ident) => { - JSXIdentifier { span: ident.span, name: ident.name }.serialize(serializer) - } - Self::NamespacedName(name) => name.serialize(serializer), - Self::MemberExpression(expr) => expr.serialize(serializer), - Self::ThisExpression(expr) => { - JSXIdentifier { span: expr.span, name: "this".into() }.serialize(serializer) - } - } - } -} - -impl Serialize for JSXMemberExpressionObject<'_> { - fn serialize(&self, serializer: S) -> Result { - match self { - Self::IdentifierReference(ident) => { - JSXIdentifier { span: ident.span, name: ident.name }.serialize(serializer) - } - Self::MemberExpression(expr) => expr.serialize(serializer), - Self::ThisExpression(expr) => { - JSXIdentifier { span: expr.span, name: "this".into() }.serialize(serializer) - } - } - } -} - +/// Serializer for `ArrowFunctionExpression`'s `body` field. +/// +/// Serializes as either an expression (if `expression` property is set), +/// or a `BlockStatement` (if it's not). pub struct ArrowFunctionExpressionBody<'a>(pub &'a ArrowFunctionExpression<'a>); impl Serialize for ArrowFunctionExpressionBody<'_> { @@ -308,6 +287,8 @@ impl Serialize for ArrowFunctionExpressionBody<'_> { } } +/// Serializer for `AssignmentTargetPropertyIdentifier`'s `init` field +/// (which is renamed to `value` in ESTree AST). pub struct AssignmentTargetPropertyIdentifierValue<'a>( pub &'a AssignmentTargetPropertyIdentifier<'a>, ); @@ -327,3 +308,37 @@ impl Serialize for AssignmentTargetPropertyIdentifierValue<'_> { } } } + +// -------------------- +// JSX +// -------------------- + +impl Serialize for JSXElementName<'_> { + fn serialize(&self, serializer: S) -> Result { + match self { + Self::Identifier(ident) => ident.serialize(serializer), + Self::IdentifierReference(ident) => { + JSXIdentifier { span: ident.span, name: ident.name }.serialize(serializer) + } + Self::NamespacedName(name) => name.serialize(serializer), + Self::MemberExpression(expr) => expr.serialize(serializer), + Self::ThisExpression(expr) => { + JSXIdentifier { span: expr.span, name: "this".into() }.serialize(serializer) + } + } + } +} + +impl Serialize for JSXMemberExpressionObject<'_> { + fn serialize(&self, serializer: S) -> Result { + match self { + Self::IdentifierReference(ident) => { + JSXIdentifier { span: ident.span, name: ident.name }.serialize(serializer) + } + Self::MemberExpression(expr) => expr.serialize(serializer), + Self::ThisExpression(expr) => { + JSXIdentifier { span: expr.span, name: "this".into() }.serialize(serializer) + } + } + } +}