Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 42 additions & 8 deletions crates/oxc_ast/src/serialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,22 @@ use cow_utils::CowUtils;
use crate::ast::*;
use oxc_ast_macros::ast_meta;
use oxc_estree::{
CompactJSSerializer, CompactTSSerializer, ESTree, FlatStructSerializer, JsonSafeString,
LoneSurrogatesString, PrettyJSSerializer, PrettyTSSerializer, SequenceSerializer, Serializer,
StructSerializer, ser::AppendToConcat,
CompactFixesJSSerializer, CompactFixesTSSerializer, CompactJSSerializer, CompactTSSerializer,
ESTree, FlatStructSerializer, JsonSafeString, LoneSurrogatesString, PrettyFixesJSSerializer,
PrettyFixesTSSerializer, PrettyJSSerializer, PrettyTSSerializer, SequenceSerializer,
Serializer, StructSerializer, ser::AppendToConcat,
};
use oxc_span::GetSpan;

/// Main serialization methods for `Program`.
///
/// Note: 4 separate methods for the different serialization options, rather than 1 method
/// with behavior controlled by flags (e.g. `fn to_estree_json(&self, with_ts: bool, pretty: bool`)
/// Note: 8 separate methods for the different serialization options, rather than 1 method
/// with behavior controlled by flags
/// (e.g. `fn to_estree_json(&self, with_ts: bool, pretty: bool, fixes: bool)`)
/// to avoid bloating binary size.
///
/// Most consumers (and Oxc crates) will use only 1 of these methods, so we don't want to needlessly
/// compile all 4 serializers when only 1 is used.
/// compile all 8 serializers when only 1 is used.
///
/// Initial capacity for serializer's buffer is an estimate based on our benchmark fixtures
/// of ratio of source text size to JSON size.
Expand Down Expand Up @@ -70,6 +72,34 @@ impl Program<'_> {
self.serialize(&mut serializer);
serializer.into_string()
}

/// Serialize AST to ESTree JSON, including TypeScript fields, with list of fixes.
pub fn to_estree_ts_json_with_fixes(&self) -> String {
let capacity = self.source_text.len() * JSON_CAPACITY_RATIO_COMPACT;
let serializer = CompactFixesTSSerializer::with_capacity(capacity);
serializer.serialize_with_fixes(self)
}

/// Serialize AST to ESTree JSON, without TypeScript fields, with list of fixes.
pub fn to_estree_js_json_with_fixes(&self) -> String {
let capacity = self.source_text.len() * JSON_CAPACITY_RATIO_COMPACT;
let serializer = CompactFixesJSSerializer::with_capacity(capacity);
serializer.serialize_with_fixes(self)
}

/// Serialize AST to pretty-printed ESTree JSON, including TypeScript fields, with list of fixes.
pub fn to_pretty_estree_ts_json_with_fixes(&self) -> String {
let capacity = self.source_text.len() * JSON_CAPACITY_RATIO_PRETTY;
let serializer = PrettyFixesTSSerializer::with_capacity(capacity);
serializer.serialize_with_fixes(self)
}

/// Serialize AST to pretty-printed ESTree JSON, without TypeScript fields, with list of fixes.
pub fn to_pretty_estree_js_json_with_fixes(&self) -> String {
let capacity = self.source_text.len() * JSON_CAPACITY_RATIO_PRETTY;
let serializer = PrettyFixesJSSerializer::with_capacity(capacity);
serializer.serialize_with_fixes(self)
}
}

// --------------------
Expand Down Expand Up @@ -409,7 +439,9 @@ impl ESTree for BigIntLiteralBigint<'_, '_> {
pub struct BigIntLiteralValue<'a, 'b>(#[expect(dead_code)] pub &'b BigIntLiteral<'a>);

impl ESTree for BigIntLiteralValue<'_, '_> {
fn serialize<S: Serializer>(&self, serializer: S) {
fn serialize<S: Serializer>(&self, mut serializer: S) {
// Record that this node needs fixing on JS side
serializer.record_fix_path();
Null(()).serialize(serializer);
}
}
Expand All @@ -431,7 +463,9 @@ impl ESTree for BigIntLiteralValue<'_, '_> {
pub struct RegExpLiteralValue<'a, 'b>(#[expect(dead_code)] pub &'b RegExpLiteral<'a>);

impl ESTree for RegExpLiteralValue<'_, '_> {
fn serialize<S: Serializer>(&self, serializer: S) {
fn serialize<S: Serializer>(&self, mut serializer: S) {
// Record that this node needs fixing on JS side
serializer.record_fix_path();
Null(()).serialize(serializer);
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/oxc_estree/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ workspace = true
doctest = false

[dependencies]
oxc_data_structures = { workspace = true, features = ["code_buffer"], optional = true }
oxc_data_structures = { workspace = true, features = ["code_buffer", "stack"], optional = true }

itoa = { workspace = true, optional = true }
ryu-js = { workspace = true, optional = true }
Expand Down
30 changes: 30 additions & 0 deletions crates/oxc_estree/src/serialize/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
pub trait Config {
/// `true` if output should contain TS fields
const INCLUDE_TS_FIELDS: bool;
/// `true` if should record paths to `Literal` nodes that need fixing on JS side
const FIXES: bool;

fn new() -> Self;
}
Expand All @@ -11,6 +13,7 @@ pub struct ConfigTS;

impl Config for ConfigTS {
const INCLUDE_TS_FIELDS: bool = true;
const FIXES: bool = false;

#[inline(always)]
fn new() -> Self {
Expand All @@ -23,6 +26,33 @@ pub struct ConfigJS;

impl Config for ConfigJS {
const INCLUDE_TS_FIELDS: bool = false;
const FIXES: bool = false;

#[inline(always)]
fn new() -> Self {
Self
}
}

/// Config for serializing AST with TypeScript fields, with fixes.
pub struct ConfigFixesTS;

impl Config for ConfigFixesTS {
const INCLUDE_TS_FIELDS: bool = true;
const FIXES: bool = true;

#[inline(always)]
fn new() -> Self {
Self
}
}

/// Config for serializing AST without TypeScript fields, with fixes.
pub struct ConfigFixesJS;

impl Config for ConfigFixesJS {
const INCLUDE_TS_FIELDS: bool = false;
const FIXES: bool = true;

#[inline(always)]
fn new() -> Self {
Expand Down
124 changes: 120 additions & 4 deletions crates/oxc_estree/src/serialize/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
// Methods which are trivial or just delegate to other methods are marked `#[inline(always)]`
#![expect(clippy::inline_always)]

use oxc_data_structures::code_buffer::CodeBuffer;
use std::mem;

use itoa::Buffer as ItoaBuffer;

use oxc_data_structures::{code_buffer::CodeBuffer, stack::NonEmptyStack};

mod blanket;
mod config;
Expand All @@ -10,7 +14,7 @@ mod primitives;
mod sequences;
mod strings;
mod structs;
use config::{Config, ConfigJS, ConfigTS};
use config::{Config, ConfigFixesJS, ConfigFixesTS, ConfigJS, ConfigTS};
use formatter::{CompactFormatter, Formatter, PrettyFormatter};
use sequences::ESTreeSequenceSerializer;
use structs::ESTreeStructSerializer;
Expand Down Expand Up @@ -43,6 +47,13 @@ pub trait Serializer: SerializerPrivate {

/// Serialize sequence.
fn serialize_sequence(self) -> Self::SequenceSerializer;

/// Record path to current node in `fixes_buffer`.
///
/// Used by serializers for the `value` field of `BigIntLiteral` and `RegExpLiteral`.
/// These nodes cannot be serialized to JSON, because JSON doesn't support `BigInt`s or `RegExp`s.
/// "Fix paths" can be used on JS side to locate these nodes and set their `value` fields correctly.
fn record_fix_path(&mut self);
}

/// Trait containing internal methods of [`Serializer`]s that we don't want to expose outside this crate.
Expand All @@ -69,23 +80,81 @@ pub type PrettyTSSerializer = ESTreeSerializer<ConfigTS, PrettyFormatter>;
/// ESTree serializer which produces pretty JSON, excluding TypeScript fields.
pub type PrettyJSSerializer = ESTreeSerializer<ConfigJS, PrettyFormatter>;

/// ESTree serializer which produces compact JSON, including TypeScript fields.
pub type CompactFixesTSSerializer = ESTreeSerializer<ConfigFixesTS, CompactFormatter>;

/// ESTree serializer which produces compact JSON, excluding TypeScript fields.
pub type CompactFixesJSSerializer = ESTreeSerializer<ConfigFixesJS, CompactFormatter>;

/// ESTree serializer which produces pretty JSON, including TypeScript fields.
pub type PrettyFixesTSSerializer = ESTreeSerializer<ConfigFixesTS, PrettyFormatter>;

/// ESTree serializer which produces pretty JSON, excluding TypeScript fields.
pub type PrettyFixesJSSerializer = ESTreeSerializer<ConfigFixesJS, PrettyFormatter>;

/// ESTree serializer.
pub struct ESTreeSerializer<C: Config, F: Formatter> {
buffer: CodeBuffer,
formatter: F,
trace_path: NonEmptyStack<TracePathPart>,
fixes_buffer: CodeBuffer,
#[expect(unused)]
config: C,
}

impl<C: Config, F: Formatter> ESTreeSerializer<C, F> {
/// Create new [`ESTreeSerializer`].
pub fn new() -> Self {
Self { buffer: CodeBuffer::new(), formatter: F::new(), config: C::new() }
Self {
buffer: CodeBuffer::new(),
formatter: F::new(),
trace_path: NonEmptyStack::new(TracePathPart::Index(0)),
fixes_buffer: CodeBuffer::new(),
config: C::new(),
}
}

/// Create new [`ESTreeSerializer`] with specified buffer capacity.
pub fn with_capacity(capacity: usize) -> Self {
Self { buffer: CodeBuffer::with_capacity(capacity), formatter: F::new(), config: C::new() }
Self {
buffer: CodeBuffer::with_capacity(capacity),
formatter: F::new(),
trace_path: NonEmptyStack::new(TracePathPart::Index(0)),
fixes_buffer: CodeBuffer::new(),
config: C::new(),
}
}

/// Serialize `node` and output a `JSON` string containing
/// `{ "node": { ... }, "fixes": [ ... ]}`, where `node` is the serialized AST node,
/// and `fixes` is a list of paths to any `Literal`s which are `BigInt`s or `RegExp`s.
///
/// The `value` field of these nodes cannot be serialized to JSON, because JSON doesn't support
/// `BigInt`s or `RegExp`s. The `fixes` paths can be used on JS side to locate these nodes
/// and set their `value` fields correctly.
pub fn serialize_with_fixes<T: ESTree>(mut self, node: &T) -> String {
const {
assert!(
C::FIXES,
"Cannot call `serialize_with_fixes` on a serializer without fixes enabled"
);
}

self.buffer.print_str(r#"{"node":"#);

node.serialize(&mut self);

debug_assert_eq!(self.trace_path.len(), 1);
debug_assert_eq!(self.trace_path[0], TracePathPart::DUMMY);

self.buffer.print_str(r#","fixes":["#);
if !self.fixes_buffer.is_empty() {
let traces_buffer = mem::take(&mut self.fixes_buffer).into_string();
self.buffer.print_str(&traces_buffer[1..]);
}
self.buffer.print_str("]}");

self.buffer.into_string()
}

/// Consume this [`ESTreeSerializer`] and convert buffer to string.
Expand Down Expand Up @@ -119,6 +188,42 @@ impl<'s, C: Config, F: Formatter> Serializer for &'s mut ESTreeSerializer<C, F>
fn serialize_sequence(self) -> ESTreeSequenceSerializer<'s, C, F> {
ESTreeSequenceSerializer::new(self)
}

/// Record path to current node in `fixes_buffer`.
///
/// Used by serializers for the `value` field of `BigIntLiteral` and `RegExpLiteral`.
/// These nodes cannot be serialized to JSON, because JSON doesn't support `BigInt`s or `RegExp`s.
/// "Fix paths" can be used on JS side to locate these nodes and set their `value` fields correctly.
fn record_fix_path(&mut self) {
if !C::FIXES {
return;
}

self.fixes_buffer.print_str(",[");

// First part is a dummy, last part is `"value"`, so skip them
let parts = self.trace_path.as_slice();
let parts = &parts[1..parts.len() - 1];
for (index, part) in parts.iter().enumerate() {
if index > 0 {
self.fixes_buffer.print_ascii_byte(b',');
}
match *part {
TracePathPart::Key(key) => {
self.fixes_buffer.print_ascii_byte(b'"');
self.fixes_buffer.print_str(key);
self.fixes_buffer.print_ascii_byte(b'"');
}
TracePathPart::Index(index) => {
let mut buffer = ItoaBuffer::new();
let s = buffer.format(index);
self.fixes_buffer.print_str(s);
}
}
}

self.fixes_buffer.print_ascii_byte(b']');
}
}

impl<C: Config, F: Formatter> SerializerPrivate for &mut ESTreeSerializer<C, F> {
Expand All @@ -136,3 +241,14 @@ impl<C: Config, F: Formatter> SerializerPrivate for &mut ESTreeSerializer<C, F>
(&mut self.buffer, &mut self.formatter)
}
}

/// Element of a trace path.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TracePathPart {
Key(&'static str),
Index(usize),
}

impl TracePathPart {
pub const DUMMY: Self = TracePathPart::Index(0);
}
Loading
Loading