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
1 change: 0 additions & 1 deletion mordant-baseline.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
"reimplemented_helper:src/react_compiler/reactive_scopes/propagate_early_returns.rs" = 1

[bun_runtime]
"bare_bool_args:src/runtime/api.rs" = 2
"defaulted_failure:src/runtime/ffi/ffi_body.rs" = 1
"defaulted_failure:src/runtime/server/RequestContext.rs" = 1
"defaulted_failure:src/runtime/test_runner/pretty_format.rs" = 4
Expand Down
52 changes: 39 additions & 13 deletions src/runtime/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,21 +218,47 @@ fn with_text_format_source<R>(
global: &bun_jsc::JSGlobalObject,
frame: &bun_jsc::CallFrame,
path: &'static [u8],
accept_blob_or_buffer: bool,
reject_nullish: bool,
blob_or_buffer_input: BlobOrBufferInput,
nullish_input: NullishInput,
f: impl FnOnce(&bun_alloc::Arena, &mut bun_ast::Log, &bun_ast::Source) -> bun_jsc::JsResult<R>,
) -> bun_jsc::JsResult<R> {
with_text_format_source_encoded(
global,
frame,
path,
accept_blob_or_buffer,
reject_nullish,
false,
blob_or_buffer_input,
nullish_input,
StringInput::Utf8,
|arena, log, source, _| f(arena, log, source),
)
}

/// What `parse` does with a `Blob`, `ArrayBuffer`, typed array or `DataView`.
#[derive(Clone, Copy, PartialEq, Eq)]
enum BlobOrBufferInput {
/// Parses its bytes.
Bytes,
/// Stringifies it like any other argument, as `JSON.parse` would.
ToString,
}

/// What `parse` does with an `undefined` or `null` argument.
#[derive(Clone, Copy, PartialEq, Eq)]
enum NullishInput {
Throw,
/// Parses the text `"undefined"` / `"null"`.
ToString,
}

/// What `parse` hands the closure for a string argument.
#[derive(Clone, Copy, PartialEq, Eq)]
enum StringInput {
/// The string re-encoded as UTF-8 ([`SourceEncoding::Utf8Text`]).
Utf8,
/// The string's own storage, Latin-1 or UTF-16, as is.
AsIs,
}

/// How the bytes handed to the closure of
/// [`with_text_format_source_encoded`] are encoded.
#[derive(Clone, Copy, PartialEq, Eq)]
Expand All @@ -241,20 +267,20 @@ enum SourceEncoding {
Bytes,
/// A JS string, re-encoded as UTF-8.
Utf8Text,
/// A Latin-1 JS string, borrowed as is (only when `string_passthrough`).
/// A Latin-1 JS string, borrowed as is (only under [`StringInput::AsIs`]).
Latin1Text,
/// A UTF-16 JS string, borrowed as is: the bytes are its code units
/// (only when `string_passthrough`).
/// (only under [`StringInput::AsIs`]).
Utf16Text,
}

fn with_text_format_source_encoded<R>(
global: &bun_jsc::JSGlobalObject,
frame: &bun_jsc::CallFrame,
path: &'static [u8],
accept_blob_or_buffer: bool,
reject_nullish: bool,
string_passthrough: bool,
blob_or_buffer_input: BlobOrBufferInput,
nullish_input: NullishInput,
string_input: StringInput,
f: impl FnOnce(
&bun_alloc::Arena,
&mut bun_ast::Log,
Expand Down Expand Up @@ -286,7 +312,7 @@ fn with_text_format_source_encoded<R>(
let _ast_scope = ast_memory_allocator.enter();

let input_value = frame.argument(0);
if reject_nullish && input_value.is_empty_or_undefined_or_null() {
if nullish_input == NullishInput::Throw && input_value.is_empty_or_undefined_or_null() {
return Err(global.throw_invalid_arguments(format_args!("Expected a string to parse")));
}

Expand All @@ -299,15 +325,15 @@ fn with_text_format_source_encoded<R>(
let _latin1_hold: bun_core::OwnedString;
let mut encoding = SourceEncoding::Utf8Text;
let bytes: &[u8] = 'bytes: {
if accept_blob_or_buffer && !input_value.is_string() {
if blob_or_buffer_input == BlobOrBufferInput::Bytes && !input_value.is_string() {
if let Some(v) = BlobOrStringOrBuffer::from_js(global, input_value)? {
_blob_hold = v;
encoding = SourceEncoding::Bytes;
break 'bytes _blob_hold.slice();
}
}
let mut s = input_value.to_bun_string(global)?;
if string_passthrough {
if string_input == StringInput::AsIs {
_latin1_hold = bun_core::OwnedString::new(s);
if _latin1_hold.is_8bit() {
encoding = SourceEncoding::Latin1Text;
Expand Down
4 changes: 2 additions & 2 deletions src/runtime/api/JSON5Object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ pub(crate) fn parse(global: &JSGlobalObject, frame: &CallFrame) -> JsResult<JSVa
global,
frame,
b"input.json5",
true,
true,
super::BlobOrBufferInput::Bytes,
super::NullishInput::Throw,
|bump, log, source| {
let root = match json5::JSON5Parser::parse(source, log, bump) {
Ok(r) => r,
Expand Down
4 changes: 2 additions & 2 deletions src/runtime/api/JSONCObject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ pub(crate) fn parse(global: &JSGlobalObject, frame: &CallFrame) -> JsResult<JSVa
global,
frame,
b"input.jsonc",
false,
true,
super::BlobOrBufferInput::ToString,
super::NullishInput::Throw,
|_arena, log, source| {
// parse_jsonc maps empty input to {}; the public API rejects it like JSON.parse.
if source.contents.is_empty() {
Expand Down
4 changes: 2 additions & 2 deletions src/runtime/api/TOMLObject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ pub(crate) fn parse(global: &JSGlobalObject, frame: &CallFrame) -> JsResult<JSVa
global,
frame,
b"input.toml",
true,
true,
super::BlobOrBufferInput::Bytes,
super::NullishInput::Throw,
|arena, log, source| {
let root = match TOML::parse(source, log, arena, false) {
Ok(v) => v,
Expand Down
6 changes: 3 additions & 3 deletions src/runtime/api/XMLObject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ pub(crate) fn parse(global: &JSGlobalObject, frame: &CallFrame) -> JsResult<JSVa
global,
frame,
b"input.xml",
true,
true,
true,
super::BlobOrBufferInput::Bytes,
super::NullishInput::Throw,
super::StringInput::AsIs,
|arena, log, source, source_encoding| {
let encoding = match source_encoding {
super::SourceEncoding::Bytes => xml::InputEncoding::Bytes,
Expand Down
6 changes: 3 additions & 3 deletions src/runtime/api/YAMLObject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1026,13 +1026,13 @@ fn is_inf_suffix(str: &BunString, i: usize) -> bool {

#[bun_jsc::host_fn]
pub(crate) fn parse(global: &JSGlobalObject, call_frame: &CallFrame) -> JsResult<JSValue> {
// reject_nullish=false preserves YAML's coerce-undefined-to-"undefined" behavior.
// `NullishInput::ToString` preserves YAML's coerce-undefined-to-"undefined" behavior.
super::with_text_format_source(
global,
call_frame,
b"input.yaml",
true,
false,
super::BlobOrBufferInput::Bytes,
super::NullishInput::ToString,
|arena, log, source| {
// `ParserCtx::to_js` materializes each `E::Array`/`E::Object`
// once by pointer identity, so a cyclic graph is fine here.
Expand Down
16 changes: 16 additions & 0 deletions test/js/bun/jsonc/jsonc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,22 @@ test("Bun.JSONC.parse throws a SyntaxError on invalid input", () => {
}
});

test("Bun.JSONC.parse throws on undefined and null input", () => {
expect(() => Bun.JSONC.parse(undefined as any)).toThrow("Expected a string to parse");
expect(() => (Bun.JSONC.parse as any)()).toThrow("Expected a string to parse");
expect(() => Bun.JSONC.parse(null as any)).toThrow("Expected a string to parse");
});

test("Bun.JSONC.parse stringifies non-string input like JSON.parse does", () => {
expect(Bun.JSONC.parse(42 as any)).toBe(42);
expect(Bun.JSONC.parse({ toString: () => "[1, 2,] // ok" } as any)).toEqual([1, 2]);
// A Buffer stringifies to its text; a Blob stringifies to "[object Blob]"
// rather than being read as bytes (unlike Bun.TOML.parse and friends).
expect(Bun.JSONC.parse(Buffer.from('{"a": 1, /* c */}') as any)).toEqual({ a: 1 });
expect(() => Bun.JSONC.parse(new Blob(['{"a": 1}']) as any)).toThrow(SyntaxError);
expect(() => JSON.parse(new Blob(['{"a": 1}']) as any)).toThrow(SyntaxError);
});

test("Bun.JSONC.parse SyntaxError names the actual error, not a preceding warning", () => {
let thrown: unknown;
try {
Expand Down
9 changes: 9 additions & 0 deletions test/js/bun/yaml/yaml.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,15 @@ development:
});
});

test("stringifies any other input instead of throwing, undefined and null included", () => {
// Unlike TOML/JSONC/JSON5/XML, YAML.parse(undefined) parses the text "undefined".
expect(YAML.parse(undefined as any)).toBe("undefined");
expect((YAML.parse as any)()).toBe("undefined");
expect(YAML.parse(null as any)).toBe(null);
expect(YAML.parse(42 as any)).toBe(42);
expect(YAML.parse({ toString: () => "a: 1" } as any)).toEqual({ a: 1 });
});

test("complex nested structure from various input types", () => {
const complexYaml = `
version: "1.0"
Expand Down
Loading