diff --git a/mordant-baseline.toml b/mordant-baseline.toml index ed3d11479351..41e0f42195f9 100644 --- a/mordant-baseline.toml +++ b/mordant-baseline.toml @@ -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 diff --git a/src/runtime/api.rs b/src/runtime/api.rs index 680029fafff1..8e2f9ec34edc 100644 --- a/src/runtime/api.rs +++ b/src/runtime/api.rs @@ -218,21 +218,47 @@ fn with_text_format_source( 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, ) -> bun_jsc::JsResult { 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)] @@ -241,10 +267,10 @@ 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, } @@ -252,9 +278,9 @@ fn with_text_format_source_encoded( 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, @@ -286,7 +312,7 @@ fn with_text_format_source_encoded( 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"))); } @@ -299,7 +325,7 @@ fn with_text_format_source_encoded( 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; @@ -307,7 +333,7 @@ fn with_text_format_source_encoded( } } 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; diff --git a/src/runtime/api/JSON5Object.rs b/src/runtime/api/JSON5Object.rs index 233918df8c25..0344526b0562 100644 --- a/src/runtime/api/JSON5Object.rs +++ b/src/runtime/api/JSON5Object.rs @@ -49,8 +49,8 @@ pub(crate) fn parse(global: &JSGlobalObject, frame: &CallFrame) -> JsResult r, diff --git a/src/runtime/api/JSONCObject.rs b/src/runtime/api/JSONCObject.rs index 5c0b8f5d9acd..19a745bfe7dc 100644 --- a/src/runtime/api/JSONCObject.rs +++ b/src/runtime/api/JSONCObject.rs @@ -14,8 +14,8 @@ pub(crate) fn parse(global: &JSGlobalObject, frame: &CallFrame) -> JsResult JsResult v, diff --git a/src/runtime/api/XMLObject.rs b/src/runtime/api/XMLObject.rs index b53543d22420..f3f9e562f6aa 100644 --- a/src/runtime/api/XMLObject.rs +++ b/src/runtime/api/XMLObject.rs @@ -52,9 +52,9 @@ pub(crate) fn parse(global: &JSGlobalObject, frame: &CallFrame) -> JsResult xml::InputEncoding::Bytes, diff --git a/src/runtime/api/YAMLObject.rs b/src/runtime/api/YAMLObject.rs index 06ab34da89da..b5ac84aa14e9 100644 --- a/src/runtime/api/YAMLObject.rs +++ b/src/runtime/api/YAMLObject.rs @@ -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 { - // 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. diff --git a/test/js/bun/jsonc/jsonc.test.ts b/test/js/bun/jsonc/jsonc.test.ts index bce489dde5d9..d42e1d6646ed 100644 --- a/test/js/bun/jsonc/jsonc.test.ts +++ b/test/js/bun/jsonc/jsonc.test.ts @@ -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 { diff --git a/test/js/bun/yaml/yaml.test.ts b/test/js/bun/yaml/yaml.test.ts index db877dbca7df..5c6851b1b605 100644 --- a/test/js/bun/yaml/yaml.test.ts +++ b/test/js/bun/yaml/yaml.test.ts @@ -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"