Skip to content

Commit

Permalink
refactor(ast/estree): re-arrange and comment custom serialization code
Browse files Browse the repository at this point in the history
  • Loading branch information
overlookmotel committed Feb 10, 2025
1 parent 000ac1f commit 3cd4374
Showing 1 changed file with 111 additions and 96 deletions.
207 changes: 111 additions & 96 deletions crates/oxc_ast/src/serialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u8>` 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<usize, std::io::Error> {
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<W: Write>(
&self,
writer: W,
) -> Result<serde_json::Serializer<W, EcmaFormatter>, 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<W>(&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
// --------------------
Expand Down Expand Up @@ -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<W>(&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<u8>` 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<usize, std::io::Error> {
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<W: Write>(
&self,
writer: W,
) -> Result<serde_json::Serializer<W, EcmaFormatter>, 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 {
Expand Down Expand Up @@ -205,6 +209,7 @@ impl<E: Serialize, R: Serialize> Serialize for ElementsAndRest<'_, E, R> {
}
}

/// Wrap an `Option<Vec<T>>` so that it's serialized as an empty array (`[]`) if the `Option` is `None`.
pub struct OptionVecDefault<'a, 'b, T: Serialize>(pub &'b Option<ArenaVec<'a, T>>);

impl<T: Serialize> Serialize for OptionVecDefault<'_, '_, T> {
Expand Down Expand Up @@ -266,36 +271,10 @@ struct DirectiveAsStatement<'a, 'b> {
expression: &'b StringLiteral<'a>,
}

impl Serialize for JSXElementName<'_> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
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<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
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<'_> {
Expand All @@ -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>,
);
Expand All @@ -327,3 +308,37 @@ impl Serialize for AssignmentTargetPropertyIdentifierValue<'_> {
}
}
}

// --------------------
// JSX
// --------------------

impl Serialize for JSXElementName<'_> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
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<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
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)
}
}
}
}

0 comments on commit 3cd4374

Please sign in to comment.