pub mod checkpoint;
pub mod context;
pub mod diagnostics;
pub mod lexer;
pub mod precedence;
pub mod scanner;
pub mod token;
pub mod type_data;
pub mod type_emit;
pub mod type_parse;
pub mod type_stringify;
pub use checkpoint::{Checkpoint, FenceState, QuoteKind};
pub use context::{
InlineTagFormatData, ParsedBlock, ParsedDiagnostic, ParserContext, emit_block,
parse_block_into_data,
};
pub use diagnostics::{
ParserDiagnosticKind, TypeDiagnosticKind, parser_diagnostic_message, type_diagnostic_message,
};
use oxc_allocator::{Allocator, Vec as ArenaVec};
use oxc_span::Span;
use crate::decoder::nodes::comment_ast::LazyJsdocBlock;
use crate::decoder::source_file::LazySourceFile;
use crate::writer::BinaryWriter;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ParseOptions {
pub compat_mode: bool,
pub base_offset: u32,
pub fence_aware: bool,
pub parse_types: bool,
pub type_parse_mode: type_data::ParseMode,
pub preserve_whitespace: bool,
}
impl Default for ParseOptions {
fn default() -> Self {
Self {
compat_mode: false,
base_offset: 0,
fence_aware: true,
parse_types: false,
type_parse_mode: type_data::ParseMode::Jsdoc,
preserve_whitespace: false,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct Diagnostic {
pub message: &'static str,
pub span: Option<Span>,
}
#[derive(Debug)]
pub struct ParseResult<'arena> {
pub binary_bytes: &'arena [u8],
pub lazy_root: Option<LazyJsdocBlock<'arena>>,
pub source_file: LazySourceFile<'arena>,
pub diagnostics: ArenaVec<'arena, Diagnostic>,
}
#[derive(Debug, Clone, Copy)]
pub struct OwnedDiagnostic {
pub message: &'static str,
pub span: Option<Span>,
}
#[derive(Debug)]
pub struct ParseBytesResult {
pub binary_bytes: Vec<u8>,
pub diagnostics: Vec<OwnedDiagnostic>,
}
pub fn parse<'arena>(
arena: &'arena Allocator,
source: &'arena str,
options: ParseOptions,
) -> ParseResult<'arena> {
let mut writer = BinaryWriter::new(arena);
parse_with_writer(arena, source, options, &mut writer)
}
pub fn parse_into<'arena>(
arena: &'arena Allocator,
source: &'arena str,
options: ParseOptions,
writer: &mut BinaryWriter<'arena>,
) -> ParseResult<'arena> {
writer.reset();
parse_with_writer(arena, source, options, writer)
}
fn parse_with_writer<'arena>(
arena: &'arena Allocator,
source: &'arena str,
options: ParseOptions,
writer: &mut BinaryWriter<'arena>,
) -> ParseResult<'arena> {
if options.compat_mode {
writer.set_compat_mode(true);
}
if options.preserve_whitespace {
writer.set_preserve_whitespace_span(true);
}
let parser_options = ParseOptions {
base_offset: 0,
..options
};
let parsed = context::parse_block_into_data(arena, source, 0, parser_options);
let source_offset = writer.append_source_text(source);
let root_node_index = if parsed.is_failure() {
0
} else {
context::emit_block(writer, &parsed).unwrap_or(0)
};
writer.push_root(root_node_index, source_offset, options.base_offset);
let mut arena_diagnostics: ArenaVec<'arena, Diagnostic> = ArenaVec::new_in(arena);
for diag in parsed.diagnostics() {
let message = diag.message();
writer.push_diagnostic(0, message);
arena_diagnostics.push(Diagnostic {
message,
span: diag.span,
});
}
let binary_bytes: &'arena [u8] = writer.finish_into_arena_reusing();
let source_file_owned = LazySourceFile::new(binary_bytes)
.expect("BinaryWriter::finish_into_arena_reusing() always produces a header-valid buffer");
let source_file_ref: &'arena LazySourceFile<'arena> = arena.alloc(source_file_owned);
let lazy_root = if root_node_index == 0 {
None
} else {
source_file_ref.asts().next().flatten()
};
ParseResult {
binary_bytes,
lazy_root,
source_file: *source_file_ref,
diagnostics: arena_diagnostics,
}
}
#[must_use]
pub fn parse_to_bytes(source: &str, options: ParseOptions) -> ParseBytesResult {
use crate::writer::BinaryWriter;
let arena = Allocator::default();
let parser_options = ParseOptions {
base_offset: 0,
..options
};
let parsed = context::parse_block_into_data(&arena, source, 0, parser_options);
let mut writer = BinaryWriter::new(&arena);
if options.compat_mode {
writer.set_compat_mode(true);
}
if options.preserve_whitespace {
writer.set_preserve_whitespace_span(true);
}
let source_offset = writer.append_source_text(source);
let root_node_index = if parsed.is_failure() {
0
} else {
context::emit_block(&mut writer, &parsed).unwrap_or(0)
};
writer.push_root(root_node_index, source_offset, options.base_offset);
for diag in parsed.diagnostics() {
writer.push_diagnostic(0, diag.message());
}
let diagnostics: Vec<OwnedDiagnostic> = parsed
.diagnostics()
.iter()
.map(|d| OwnedDiagnostic {
message: d.message(),
span: d.span,
})
.collect();
let binary_bytes = writer.finish();
ParseBytesResult {
binary_bytes,
diagnostics,
}
}
#[derive(Debug, Clone, Copy)]
pub struct BatchItem<'a> {
pub source_text: &'a str,
pub base_offset: u32,
}
#[derive(Debug, Clone, Copy)]
pub struct BatchDiagnostic {
pub message: &'static str,
pub span: Option<Span>,
pub root_index: u32,
}
#[derive(Debug, Clone, Copy)]
pub struct OwnedBatchDiagnostic {
pub message: &'static str,
pub span: Option<Span>,
pub root_index: u32,
}
#[derive(Debug)]
pub struct BatchResult<'arena> {
pub binary_bytes: &'arena [u8],
pub lazy_roots: ArenaVec<'arena, Option<LazyJsdocBlock<'arena>>>,
pub source_file: LazySourceFile<'arena>,
pub diagnostics: ArenaVec<'arena, BatchDiagnostic>,
}
#[derive(Debug)]
pub struct ParseBatchBytesResult {
pub binary_bytes: Vec<u8>,
pub diagnostics: Vec<OwnedBatchDiagnostic>,
}
pub fn parse_batch<'arena>(
arena: &'arena Allocator,
items: &[BatchItem<'_>],
options: ParseOptions,
) -> BatchResult<'arena> {
let mut writer = BinaryWriter::new(arena);
parse_batch_with_writer(arena, items, options, &mut writer)
}
pub fn parse_batch_into<'arena>(
arena: &'arena Allocator,
items: &[BatchItem<'_>],
options: ParseOptions,
writer: &mut BinaryWriter<'arena>,
) -> BatchResult<'arena> {
writer.reset();
parse_batch_with_writer(arena, items, options, writer)
}
fn parse_batch_with_writer<'arena>(
arena: &'arena Allocator,
items: &[BatchItem<'_>],
options: ParseOptions,
writer: &mut BinaryWriter<'arena>,
) -> BatchResult<'arena> {
if options.compat_mode {
writer.set_compat_mode(true);
}
if options.preserve_whitespace {
writer.set_preserve_whitespace_span(true);
}
let parser_options = ParseOptions {
base_offset: 0,
..options
};
let mut arena_diagnostics: ArenaVec<'arena, BatchDiagnostic> = ArenaVec::new_in(arena);
for (index, item) in items.iter().enumerate() {
let root_index = index as u32;
let source_offset_in_data = writer.append_source_text(item.source_text);
let parsed = context::parse_block_into_data(arena, item.source_text, 0, parser_options);
let root_node_index = if parsed.is_failure() {
0
} else {
context::emit_block(writer, &parsed).unwrap_or(0)
};
writer.push_root(root_node_index, source_offset_in_data, item.base_offset);
for diag in parsed.diagnostics() {
let message = diag.message();
writer.push_diagnostic(root_index, message);
arena_diagnostics.push(BatchDiagnostic {
message,
span: diag.span,
root_index,
});
}
}
let binary_bytes: &'arena [u8] = writer.finish_into_arena_reusing();
let source_file_owned = LazySourceFile::new(binary_bytes)
.expect("BinaryWriter::finish_into_arena_reusing() always produces a header-valid buffer");
let source_file_ref: &'arena LazySourceFile<'arena> = arena.alloc(source_file_owned);
let mut lazy_roots: ArenaVec<'arena, Option<LazyJsdocBlock<'arena>>> = ArenaVec::new_in(arena);
for root in source_file_ref.asts() {
lazy_roots.push(root);
}
BatchResult {
binary_bytes,
lazy_roots,
source_file: *source_file_ref,
diagnostics: arena_diagnostics,
}
}
#[must_use]
pub fn parse_type_expression(
type_text: &str,
mode: type_data::ParseMode,
) -> Option<String> {
let arena = Allocator::default();
let mut ctx = context::ParserContext::new(&arena, type_text, 0, ParseOptions::default());
let node = ctx.parse_type_expression(type_text, 0, mode)?;
Some(type_stringify::stringify_type_data(&node))
}
#[must_use]
pub fn parse_type_check(type_text: &str, mode: type_data::ParseMode) -> bool {
let arena = Allocator::default();
let mut ctx = context::ParserContext::new(&arena, type_text, 0, ParseOptions::default());
ctx.parse_type_expression(type_text, 0, mode).is_some()
}
#[must_use]
pub fn parse_batch_to_bytes(
items: &[BatchItem<'_>],
options: ParseOptions,
) -> ParseBatchBytesResult {
use crate::writer::BinaryWriter;
let arena = Allocator::default();
let mut writer = BinaryWriter::new(&arena);
if options.compat_mode {
writer.set_compat_mode(true);
}
if options.preserve_whitespace {
writer.set_preserve_whitespace_span(true);
}
let parser_options = ParseOptions {
base_offset: 0,
..options
};
let mut diagnostics: Vec<OwnedBatchDiagnostic> = Vec::new();
for (index, item) in items.iter().enumerate() {
let root_index = index as u32;
let source_offset_in_data = writer.append_source_text(item.source_text);
let parsed = context::parse_block_into_data(&arena, item.source_text, 0, parser_options);
let root_node_index = if parsed.is_failure() {
0
} else {
context::emit_block(&mut writer, &parsed).unwrap_or(0)
};
writer.push_root(root_node_index, source_offset_in_data, item.base_offset);
for diag in parsed.diagnostics() {
writer.push_diagnostic(root_index, diag.message());
diagnostics.push(OwnedBatchDiagnostic {
message: diag.message(),
span: diag.span,
root_index,
});
}
}
let binary_bytes = writer.finish();
ParseBatchBytesResult {
binary_bytes,
diagnostics,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_options_default_is_non_compat_zero_offset() {
let opts = ParseOptions::default();
assert!(!opts.compat_mode);
assert_eq!(opts.base_offset, 0);
}
#[test]
fn parse_options_is_copy() {
fn assert_copy<T: Copy>() {}
assert_copy::<ParseOptions>();
}
#[test]
fn diagnostic_is_copy() {
fn assert_copy<T: Copy>() {}
assert_copy::<Diagnostic>();
assert_copy::<BatchDiagnostic>();
}
#[test]
fn batch_item_is_copy() {
fn assert_copy<T: Copy>() {}
assert_copy::<BatchItem<'static>>();
}
#[test]
fn parse_simple_block_emits_lazy_root() {
let arena = Allocator::default();
let result = parse(&arena, "/** ok */", ParseOptions::default());
assert!(result.diagnostics.is_empty());
let root = result.lazy_root.expect("root present");
assert_eq!(root.description(), Some("ok"));
}
#[test]
fn parse_param_tag_round_trips_through_lazy_decoder() {
let arena = Allocator::default();
let result = parse(
&arena,
"/**\n * @param {string} id - The user ID\n */",
ParseOptions::default(),
);
assert!(result.diagnostics.is_empty());
let root = result.lazy_root.expect("root present");
let tags: Vec<_> = root.tags().collect();
assert_eq!(tags.len(), 1);
let tag = tags[0];
assert_eq!(tag.tag().value(), "param");
assert_eq!(tag.description(), Some("The user ID"));
}
#[test]
fn parse_failure_yields_diagnostic_and_no_root() {
let arena = Allocator::default();
let result = parse(&arena, "/* plain */", ParseOptions::default());
assert!(result.lazy_root.is_none());
assert_eq!(result.diagnostics.len(), 1);
assert!(result.diagnostics[0].message.contains("not a JSDoc block"));
}
#[test]
fn parse_with_parsed_type_emits_type_name() {
use crate::decoder::nodes::type_node::LazyTypeNode;
let arena = Allocator::default();
let mut opts = ParseOptions::default();
opts.parse_types = true;
let result = parse(&arena, "/**\n * @param {string} id\n */", opts);
assert!(result.diagnostics.is_empty());
let root = result.lazy_root.unwrap();
let tag = root.tags().next().expect("tag present");
let parsed = tag.parsed_type().expect("parsedType emitted");
match parsed {
LazyTypeNode::Name(n) => assert_eq!(n.value(), "string"),
other => panic!("expected TypeName, got {other:?}"),
}
}
#[test]
fn parse_with_parsed_type_emits_union() {
use crate::decoder::nodes::type_node::LazyTypeNode;
let arena = Allocator::default();
let mut opts = ParseOptions::default();
opts.parse_types = true;
opts.type_parse_mode = crate::parser::type_data::ParseMode::Typescript;
let result = parse(&arena, "/**\n * @param {string | number} id\n */", opts);
assert!(result.diagnostics.is_empty());
let root = result.lazy_root.unwrap();
let tag = root.tags().next().expect("tag present");
let parsed = tag.parsed_type().expect("parsedType emitted");
match parsed {
LazyTypeNode::Union(u) => assert_eq!(u.elements().count(), 2),
other => panic!("expected TypeUnion, got {other:?}"),
}
}
#[test]
fn parse_with_parsed_type_emits_function_type() {
use crate::decoder::nodes::type_node::LazyTypeNode;
let arena = Allocator::default();
let mut opts = ParseOptions::default();
opts.parse_types = true;
opts.type_parse_mode = crate::parser::type_data::ParseMode::Jsdoc;
let result = parse(
&arena,
"/**\n * @returns {function(string): number} ok\n */",
opts,
);
assert!(result.diagnostics.is_empty());
let root = result.lazy_root.unwrap();
let tag = root.tags().next().expect("tag present");
let parsed = tag.parsed_type().expect("parsedType emitted");
assert!(matches!(parsed, LazyTypeNode::Function(_)));
}
#[test]
fn parse_handles_generic_dot_notation() {
use crate::decoder::nodes::type_node::LazyTypeNode;
let arena = Allocator::default();
let mut opts = ParseOptions::default();
opts.parse_types = true;
let result = parse(&arena, "/**\n * @param {Array.<string>} ids\n */", opts);
assert!(result.diagnostics.is_empty());
let root = result.lazy_root.unwrap();
let tag = root.tags().next().expect("tag present");
match tag.parsed_type().expect("parsedType emitted") {
LazyTypeNode::Generic(g) => {
assert!(g.dot());
assert_eq!(g.elements().count(), 1);
}
other => panic!("expected TypeGeneric, got {other:?}"),
}
}
#[test]
fn parse_handles_template_literal_type() {
use crate::decoder::nodes::type_node::LazyTypeNode;
let arena = Allocator::default();
let mut opts = ParseOptions::default();
opts.parse_types = true;
opts.type_parse_mode = crate::parser::type_data::ParseMode::Typescript;
let result = parse(&arena, "/**\n * @param {`hello-${T}`} value\n */", opts);
assert!(result.diagnostics.is_empty());
let root = result.lazy_root.unwrap();
let tag = root.tags().next().expect("tag present");
assert!(matches!(
tag.parsed_type().expect("parsedType emitted"),
LazyTypeNode::TemplateLiteral(_)
));
}
#[test]
fn parse_to_bytes_matches_parse_for_simple_block() {
let arena = Allocator::default();
let opts = ParseOptions::default();
let typed = parse(&arena, "/** ok */", opts);
let bytes_only = parse_to_bytes("/** ok */", opts);
assert_eq!(typed.binary_bytes, bytes_only.binary_bytes.as_slice());
assert_eq!(typed.diagnostics.len(), bytes_only.diagnostics.len());
}
#[test]
fn parse_to_bytes_matches_parse_for_param_tag() {
let arena = Allocator::default();
let mut opts = ParseOptions::default();
opts.parse_types = true;
let src = "/**\n * @param {string | number} id - The user ID\n */";
let typed = parse(&arena, src, opts);
let bytes_only = parse_to_bytes(src, opts);
assert_eq!(typed.binary_bytes, bytes_only.binary_bytes.as_slice());
}
#[test]
fn parse_to_bytes_matches_parse_with_compat_mode() {
let arena = Allocator::default();
let mut opts = ParseOptions::default();
opts.compat_mode = true;
let src = "/**\n * Description.\n * @param x The value\n */";
let typed = parse(&arena, src, opts);
let bytes_only = parse_to_bytes(src, opts);
assert_eq!(typed.binary_bytes, bytes_only.binary_bytes.as_slice());
}
#[test]
fn parse_to_bytes_matches_parse_for_failure() {
let arena = Allocator::default();
let opts = ParseOptions::default();
let typed = parse(&arena, "/* plain */", opts);
let bytes_only = parse_to_bytes("/* plain */", opts);
assert_eq!(typed.binary_bytes, bytes_only.binary_bytes.as_slice());
assert_eq!(typed.diagnostics.len(), bytes_only.diagnostics.len());
assert_eq!(
typed.diagnostics[0].message,
bytes_only.diagnostics[0].message
);
}
#[test]
fn parse_to_bytes_preserves_base_offset() {
let opts = ParseOptions {
base_offset: 12345,
..ParseOptions::default()
};
let arena = Allocator::default();
let typed = parse(&arena, "/** ok */", opts);
let bytes_only = parse_to_bytes("/** ok */", opts);
assert_eq!(typed.binary_bytes, bytes_only.binary_bytes.as_slice());
}
#[test]
fn parse_batch_empty_items_returns_empty_result() {
let arena = Allocator::default();
let result = parse_batch(&arena, &[], ParseOptions::default());
assert_eq!(result.lazy_roots.len(), 0);
assert_eq!(result.diagnostics.len(), 0);
assert_eq!(result.source_file.root_count, 0);
}
#[test]
fn parse_batch_single_item_emits_root() {
let arena = Allocator::default();
let items = [BatchItem {
source_text: "/** ok */",
base_offset: 0,
}];
let result = parse_batch(&arena, &items, ParseOptions::default());
assert_eq!(result.lazy_roots.len(), 1);
let root = result.lazy_roots[0].expect("root present");
assert_eq!(root.description(), Some("ok"));
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn parse_batch_multiple_items_each_produces_root() {
let arena = Allocator::default();
let items = [
BatchItem {
source_text: "/** first */",
base_offset: 0,
},
BatchItem {
source_text: "/**\n * @param {string} id - second\n */",
base_offset: 100,
},
BatchItem {
source_text: "/** third */",
base_offset: 200,
},
];
let result = parse_batch(&arena, &items, ParseOptions::default());
assert_eq!(result.lazy_roots.len(), 3);
assert_eq!(result.lazy_roots[0].unwrap().description(), Some("first"));
let second_tag = result.lazy_roots[1].unwrap().tags().next().expect("tag");
assert_eq!(second_tag.tag().value(), "param");
assert_eq!(result.lazy_roots[2].unwrap().description(), Some("third"));
}
#[test]
fn parse_batch_failure_yields_none_root_and_diagnostic() {
let arena = Allocator::default();
let items = [
BatchItem {
source_text: "/** good */",
base_offset: 0,
},
BatchItem {
source_text: "/* not jsdoc */",
base_offset: 50,
},
BatchItem {
source_text: "/** also good */",
base_offset: 100,
},
];
let result = parse_batch(&arena, &items, ParseOptions::default());
assert_eq!(result.lazy_roots.len(), 3);
assert!(result.lazy_roots[0].is_some());
assert!(result.lazy_roots[1].is_none(), "failed parse → None");
assert!(result.lazy_roots[2].is_some());
assert!(result.diagnostics.iter().any(|d| d.root_index == 1));
}
#[test]
fn parse_batch_diagnostics_carry_root_index() {
let arena = Allocator::default();
let items = [
BatchItem {
source_text: "/* first not jsdoc */",
base_offset: 0,
},
BatchItem {
source_text: "/** ok */",
base_offset: 100,
},
BatchItem {
source_text: "/* third not jsdoc */",
base_offset: 200,
},
];
let result = parse_batch(&arena, &items, ParseOptions::default());
let indices: Vec<u32> = result.diagnostics.iter().map(|d| d.root_index).collect();
assert!(indices.contains(&0));
assert!(indices.contains(&2));
assert!(!indices.contains(&1));
}
#[test]
fn parse_batch_preserves_base_offset_per_item() {
let arena = Allocator::default();
let items = [
BatchItem {
source_text: "/** a */",
base_offset: 1000,
},
BatchItem {
source_text: "/** b */",
base_offset: 2000,
},
];
let result = parse_batch(&arena, &items, ParseOptions::default());
assert_eq!(result.source_file.get_root_base_offset(0), 1000);
assert_eq!(result.source_file.get_root_base_offset(1), 2000);
}
#[test]
fn parse_batch_to_bytes_matches_parse_batch() {
let arena = Allocator::default();
let items = [
BatchItem {
source_text: "/** alpha */",
base_offset: 10,
},
BatchItem {
source_text: "/**\n * @param {string} x - one\n * @returns {number} two\n */",
base_offset: 200,
},
BatchItem {
source_text: "/* parse failure */",
base_offset: 500,
},
];
let opts = ParseOptions::default();
let typed = parse_batch(&arena, &items, opts);
let bytes_only = parse_batch_to_bytes(&items, opts);
assert_eq!(typed.binary_bytes, bytes_only.binary_bytes.as_slice());
assert_eq!(typed.diagnostics.len(), bytes_only.diagnostics.len());
for (t, b) in typed.diagnostics.iter().zip(bytes_only.diagnostics.iter()) {
assert_eq!(t.message, b.message);
assert_eq!(t.root_index, b.root_index);
}
}
#[test]
fn parse_batch_dedups_strings_across_roots() {
let single_src = "/**\n * @param {string} id\n */";
let single = parse_to_bytes(single_src, ParseOptions::default());
let single_size = single.binary_bytes.len();
let mut items: Vec<BatchItem<'_>> = Vec::with_capacity(50);
for _ in 0..50 {
items.push(BatchItem {
source_text: single_src,
base_offset: 0,
});
}
let batch = parse_batch_to_bytes(&items, ParseOptions::default());
let lazy = LazySourceFile::new(&batch.binary_bytes).unwrap();
assert_eq!(lazy.root_count, 50);
let per_comment = batch.binary_bytes.len() / 50;
assert!(
per_comment < single_size,
"per-comment size {per_comment} should be < standalone size {single_size}"
);
}
#[test]
fn parse_handles_conditional_type() {
use crate::decoder::nodes::type_node::LazyTypeNode;
let arena = Allocator::default();
let mut opts = ParseOptions::default();
opts.parse_types = true;
opts.type_parse_mode = crate::parser::type_data::ParseMode::Typescript;
let result = parse(&arena, "/**\n * @param {T extends U ? X : Y} v\n */", opts);
assert!(result.diagnostics.is_empty());
let root = result.lazy_root.unwrap();
let tag = root.tags().next().expect("tag present");
assert!(matches!(
tag.parsed_type().expect("parsedType emitted"),
LazyTypeNode::Conditional(_)
));
}
#[test]
fn parse_into_matches_parse_for_simple_block() {
let arena1 = Allocator::default();
let arena2 = Allocator::default();
let opts = ParseOptions::default();
let baseline = parse(&arena1, "/** ok */", opts);
let mut writer = BinaryWriter::new(&arena2);
let recycled = parse_into(&arena2, "/** ok */", opts, &mut writer);
assert_eq!(baseline.binary_bytes, recycled.binary_bytes);
assert_eq!(
baseline.lazy_root.unwrap().description(),
recycled.lazy_root.unwrap().description()
);
}
#[test]
fn parse_into_recycles_writer_without_state_leak() {
let inputs: &[&str] = &[
"/** first */",
"/**\n * @param {string} id - The user ID\n */",
"/** third with lots of text describing things */",
"/* parse failure (not jsdoc) */",
"/**\n * @returns {number} four\n */",
"/** fifth */",
];
let arena_baseline = Allocator::default();
let baselines: Vec<Vec<u8>> = inputs
.iter()
.map(|src| {
let r = parse(&arena_baseline, src, ParseOptions::default());
r.binary_bytes.to_vec()
})
.collect();
let arena_recycle = Allocator::default();
let mut writer = BinaryWriter::new(&arena_recycle);
for (src, expected) in inputs.iter().zip(baselines.iter()) {
let r = parse_into(&arena_recycle, src, ParseOptions::default(), &mut writer);
assert_eq!(
r.binary_bytes, expected,
"recycled writer produced different bytes for `{src}`"
);
}
}
#[test]
fn parse_batch_into_matches_parse_batch() {
let items = [
BatchItem {
source_text: "/** alpha */",
base_offset: 10,
},
BatchItem {
source_text: "/**\n * @param {string} x - one\n */",
base_offset: 200,
},
BatchItem {
source_text: "/* parse failure */",
base_offset: 500,
},
];
let arena1 = Allocator::default();
let baseline = parse_batch(&arena1, &items, ParseOptions::default());
let arena2 = Allocator::default();
let mut writer = BinaryWriter::new(&arena2);
let recycled = parse_batch_into(&arena2, &items, ParseOptions::default(), &mut writer);
assert_eq!(baseline.binary_bytes, recycled.binary_bytes);
}
#[test]
fn parse_batch_into_recycles_writer_across_batches() {
let arena_baseline = Allocator::default();
let arena_recycle = Allocator::default();
let mut writer = BinaryWriter::new(&arena_recycle);
for round in 0..3 {
let items = [
BatchItem {
source_text: "/** round */",
base_offset: round * 100,
},
BatchItem {
source_text: "/** @param {number} n */",
base_offset: round * 100 + 50,
},
];
let baseline = parse_batch(&arena_baseline, &items, ParseOptions::default());
let recycled =
parse_batch_into(&arena_recycle, &items, ParseOptions::default(), &mut writer);
assert_eq!(
baseline.binary_bytes, recycled.binary_bytes,
"round {round}: bytes diverged"
);
}
}
#[test]
fn parse_into_clears_compat_mode_between_calls() {
let arena = Allocator::default();
let mut writer = BinaryWriter::new(&arena);
let mut compat_opts = ParseOptions::default();
compat_opts.compat_mode = true;
let compat = parse_into(&arena, "/** ok */", compat_opts, &mut writer);
assert!(compat.source_file.compat_mode);
let plain = parse_into(&arena, "/** ok */", ParseOptions::default(), &mut writer);
assert!(
!plain.source_file.compat_mode,
"writer.reset() must clear the compat_mode flag"
);
}
}