Skip to content

Bun.JSONC.parse: throw SyntaxError instead of BuildMessage on invalid input - #35066

Merged
dylan-conway merged 8 commits into
mainfrom
farm/71c173a2/jsonc-syntaxerror
Aug 6, 2026
Merged

dylan-conway merged 8 commits into
mainfrom
farm/71c173a2/jsonc-syntaxerror

Conversation

@robobun

@robobun robobun commented Jul 22, 2026 •

Copy link
Copy Markdown
Collaborator

What

Bun.JSONC.parse now throws a SyntaxError on invalid input, matching JSON.parse, Bun.JSON5.parse, Bun.YAML.parse, Bun.TOML.parse, and Bun.JSONL.parse. Previously it threw a BuildMessage, which is not an Error, so catch (e) { if (e instanceof SyntaxError) } (or e instanceof Error) missed JSONC alone out of the whole parser family.

Also: Bun.JSONC.parse("") now throws SyntaxError to match JSON.parse("") and Bun.JSON5.parse(""). The old behavior was accidental: Bun.JSONC.parse("") returned {} while Bun.JSONC.parse(" ") threw, because the internal parser's empty-file special case (there so an empty tsconfig.json / package.json parses as {}) leaked through the public API and whitespace-only input missed it. Empty input is invalid in every JSON dialect: the official JSONTestSuite lists it as a must-reject case (n_structure_no_data, moved to the invalid list in this PR), and VS Code's jsonc-parser reports a parse error on it. The internal tsconfig/package.json path keeps its lenient empty-file -> {} behavior (the check is in the Bun.JSONC.parse host function, not the shared parser).

Repro

for (const [n, f] of [["JSON.parse", JSON.parse], ["Bun.JSON5.parse", Bun.JSON5.parse],
                       ["Bun.JSONC.parse", Bun.JSONC.parse], ["Bun.YAML.parse", Bun.YAML.parse],
                       ["Bun.TOML.parse", Bun.TOML.parse], ["Bun.JSONL.parse", Bun.JSONL.parse]]) {
  try { f("{ not valid"); } catch (e) { console.log(n, e.constructor.name, "instanceof Error:", e instanceof Error); }
}

Before:

Bun.JSONC.parse  BuildMessage  instanceof Error: false

(everything else: SyntaxError / true)

After:

Bun.JSONC.parse  SyntaxError   instanceof Error: true

Cause

src/runtime/api/JSONCObject.rs threw log.to_js(global, "Failed to parse JSONC"), which materializes a BuildMessage. The sibling parsers (TOMLObject.rs, JSON5Object.rs, YAMLObject.rs) all build a SyntaxError via global.create_syntax_error_instance(...) from the first log message. TOML switched to this pattern in the parser rewrite; JSONC never did.

Fix

Mirror the TOML/JSON5 error path: match on bun_parsers::Error (StackOverflow -> stack overflow, Alloc -> OOM, everything else -> SyntaxError carrying the first log message text), and reject empty input up front with a SyntaxError.

The JSONC.parse JSDoc in packages/bun-types/bun.d.ts now documents the throw (@throws {SyntaxError}, same as TOML.parse).


[review] gate passed · iteration 0 · 3 files touched

fails on main (without fix)
ASAN without fix: BUILD FAILED (no junit output)
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/jsonc/json-test-suite.test.ts test/js/bun/jsonc/jsonc.test.ts
ninja: Entering directory `/workspace/bun/build/debug'
[1/41] cxx obj/src/jsc/bindings/webcore/streams/BunAsyncIterableSource.cpp.o
[2/41] cxx obj/src/jsc/bindings/webcore/streams/JSByteLengthQueuingStrategy.cpp.o
[3/41] cxx obj/src/jsc/bindings/webcore/streams/JSReadableByteStreamController.cpp.o
[4/41] cxx obj/src/jsc/bindings/webcore/streams/JSDirectStreamController.cpp.o
[5/41] cxx obj/src/jsc/bindings/webcore/streams/CrossRealmTransform.cpp.o
[6/41] cxx obj/unified/UnifiedSource-src_jsc_bindings_webcore-11.cpp.o
[7/41] cxx obj/unified/UnifiedSource-src_jsc_bindings_webcore-14.cpp.o
[8/41] cxx obj/src/jsc/bindings/webcore/HTTPParsers.cpp.o
[9/41] cxx obj/src/jsc/bindings/webcore/streams/JSCountQueuingStrategy.cpp.o
[10/41] cxx obj/src/jsc/bindings/bindings.cpp.o
[11/41] cxx obj/src/jsc/bindings/webcore/streams/BunStreamConsumers.cpp.o
[12/41] cxx obj/src/jsc/bindings/webcore/streams/JSReadableStream.cpp.o
[13/41] cxx obj/src/jsc/bindings/webcore/streams/BunSt
... (truncated)

release without fix: all passed
bun test v1.4.0-canary.1 (9138cab77)

test/js/bun/jsonc/jsonc.test.ts:
(pass) Bun.JSONC exists [0.05ms]
(pass) Bun.JSONC.parse handles basic JSON [0.08ms]
(pass) Bun.JSONC.parse handles comments [0.03ms]
(pass) Bun.JSONC.parse handles a comment after a scalar on the same line [0.04ms]
(pass) Bun.JSONC.parse handles trailing commas [0.03ms]
(pass) Bun.JSONC.parse handles arrays with trailing commas [0.02ms]
(pass) Bun.JSONC.parse handles complex JSONC [0.05ms]
(pass) Bun.JSONC.parse handles nested objects [0.03ms]
(pass) Bun.JSONC.parse handles boolean and null values [0.03ms]
(pass) Bun.JSONC.parse throws on invalid JSON [0.05ms]
(pass) Bun.JSONC.parse throws a SyntaxError on invalid input [2.72ms]
(pass) Bun.JSONC.parse SyntaxError names the actual error, not a preceding warning [0.17ms]
(pass) Bun.JSONC.parse handles empty object [0.07ms]
(pass) Bun.JSONC.parse handles empty array [0.03ms]
(pass) Bun.JSONC.parse throws on deeply nested arrays instead of crashing [6.46ms]
(pass) Bun.JSONC.parse throws on deeply nested objects instead of crashing [5.66ms]
(pass) Bun.JSONC.parse handles pathological inputs in linear time [47.35ms]
(pass) Bun.JSONC.parse matches JSON.
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/jsonc/json-test-suite.test.ts test/js/bun/jsonc/jsonc.test.ts
bun test v1.4.0 (4809a056c)

test/js/bun/jsonc/jsonc.test.ts:
(pass) Bun.JSONC exists [3.12ms]
(pass) Bun.JSONC.parse handles basic JSON [2.58ms]
(pass) Bun.JSONC.parse handles comments [2.21ms]
(pass) Bun.JSONC.parse handles a comment after a scalar on the same line [2.55ms]
(pass) Bun.JSONC.parse handles trailing commas [3.20ms]
(pass) Bun.JSONC.parse handles arrays with trailing commas [2.09ms]
(pass) Bun.JSONC.parse handles complex JSONC [3.27ms]
(pass) Bun.JSONC.parse handles nested objects [2.32ms]
(pass) Bun.JSONC.parse handles boolean and null values [2.35ms]
(pass) Bun.JSONC.parse throws on invalid JSON [4.44ms]
(pass) Bun.JSONC.parse throws a SyntaxError on invalid input [17.76ms]
(pass) Bun.JSONC.parse SyntaxError names the actual error, not a preceding warning [4.38ms]
(pass) Bun.JSONC.parse handles empty object [4.47ms]
(pass) Bun.JSONC.parse handles empty array [1.86ms]
(pass) Bun.JSONC.parse throws on deeply nested arrays instead of crashing [17.77
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 748ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/41] cxx obj/src/jsc/bindings/webcore/streams/BunAsyncIterableSource.cpp.o
[2/41] cxx obj/src/jsc/bindings/webcore/streams/JSCountQueuingStrategy.cpp.o
[3/41] cxx obj/src/jsc/bindings/webcore/streams/CrossRealmTransform.cpp.o
[4/41] cxx obj/src/jsc/bindings/webcore/streams/JSReadableStreamAsyncIterator.cpp.o
[5/41] cxx obj/src/jsc/bindings/webcore/streams/BunStreamConsumers.cpp.o
[6/41] cxx obj/src/jsc/bindings/webcore/streams/BunStreamSource.cpp.o
[7/41] cxx obj/src/jsc/bindings/webcore/streams/JSByteLengthQueuingStrategy.cpp.o
[8/41] cxx obj/src/jsc/bindings/webcore/streams/JSDirectStreamController.cpp.o
[9/41] cxx obj/src/jsc/bindings/webcore/HTTPParsers.cpp.o
[10/41] cxx obj/src/jsc/bindings/webcore/streams/JSReadableByteStreamController.cpp.o
[11/41] cxx obj/src/jsc/bindings/webcore/streams/JSReadableStream.cpp.o
[12/41] cxx obj/unified/UnifiedSource-src_jsc_bindings_webcore-3.cpp.o
[13/41] cxx obj/src/jsc/bindings/webcore/streams/JSStreamsRuntime.cpp.o
[14/41] cxx obj/src/jsc/bindings/webcore/strea
... (truncated)
diff hotspot
src/runtime/api/JSONCObject.rs            | 41 +++++++++++++++++++++++++++----
 test/js/bun/jsonc/json-test-suite.test.ts |  2 +-
 test/js/bun/jsonc/jsonc.test.ts           | 27 ++++++++++++++++++++
 3 files changed, 64 insertions(+), 6 deletions(-)

gate history · 3 passed · 0 rejected · iteration 0

evidence per changed file
file                                       reads  edits  tests
src/runtime/api/JSONCObject.rs                 3      5      0
test/js/bun/jsonc/json-test-suite.test.ts      2      2      0
test/js/bun/jsonc/jsonc.test.ts                1      3      0

@coderabbitai

coderabbitai Bot commented Jul 22, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

Bun.JSONC.parse now rejects empty input, preserves explicit resource-error handling, and reports parser-specific syntax messages. Tests update JSONTestSuite classification and cover error types and message selection.

JSONC Error Handling

Layer / File(s) Summary
Runtime parse validation and error mapping
src/runtime/api/JSONCObject.rs
Empty input throws an unexpected-end syntax error, while other parse failures use the first recorded parser message with a generic fallback.
Parser regression coverage
test/js/bun/jsonc/*.test.ts
Tests expect empty input to throw and verify SyntaxError instances with JSONC parse messages across malformed inputs.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the primary change: invalid Bun.JSONC.parse input now throws SyntaxError instead of BuildMessage.
Description check ✅ Passed The description explains the change, cause, fix, behavior, tests, and validation results, although it uses different headings from the repository template.

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Jul 22, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 10:21 PM PT - Aug 5th, 2026

@robobun, your commit 86d7423 is building: #89449

Comment thread src/runtime/api/JSONCObject.rs Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — the follow-up commit addresses the warning-vs-error message ordering and adds a test for it.

What was reviewed:

  • Error path now mirrors TOMLObject.rs/JSON5Object.rs/YAMLObject.rs exactly (StackOverflow→stack overflow, Alloc→OOM, else→SyntaxError from log).
  • bun_parsers::Error variants — remaining catch-all arm covers only syntax-class errors (SyntaxError, ParserError, UTF8Fail, UnexpectedSyntax, JSONStringsMustUseDoubleQuotes).
  • Empty-input check is scoped to the host fn, so internal tsconfig/package.json callers keep lenient {}; whitespace-only inputs still route through the parser (covered by n_single_space / " " test case).
  • .d.ts and docs don't document the old ""→{} behavior, so nothing else needs updating.
Extended reasoning...

Overview

Changes the error type thrown by Bun.JSONC.parse on invalid input from BuildMessage (not an Error subclass) to SyntaxError, aligning it with JSON.parse and every sibling Bun parser (Bun.JSON5, Bun.TOML, Bun.YAML, Bun.JSONL). Also makes Bun.JSONC.parse("") throw SyntaxError instead of returning {}, matching JSON.parse(""). Touches one ~30-line host function in src/runtime/api/JSONCObject.rs, moves one JSONTestSuite entry from the "valid JSONC" to the "invalid" bucket, and adds two targeted tests to jsonc.test.ts.

Security risks

None. This is error-type reshaping on an already-rejecting parse path; no new input is accepted, no allocation sizing changes, and the underlying parser is untouched.

Level of scrutiny

Low-to-moderate. It's a user-facing behavior change (error class + empty-string semantics), but a small mechanical one that copies the exact structure already shipped for TOML/JSON5/YAML in the same directory. The empty-string change is scoped to the public host function so internal config-file callers (tsconfig, package.json) retain their lenient behavior. Test coverage is strong: the new SyntaxError test iterates six invalid inputs including "" and " ", the JSONTestSuite n_structure_no_data.json case moved to the throwing bucket, and the whole 318-case corpus continues to pass.

Other factors

My earlier inline comment (a duplicate-key warning could mask the real error message because log.msgs.first() was taken unconditionally) was addressed in 9138cab: the code now prefers the first Kind::Err entry with a fallback to first(), and a regression test asserts the message for '{"a":1,"a":2,' does not mention the duplicate key. I checked bun_parsers::Error — the catch-all Err(_) arm only covers syntax-class variants, so mapping them all to SyntaxError is correct. The LogJsc import removal is clean (no longer used after dropping log.to_js). Grepped packages/bun-types and docs/ — neither documents the old ""→{} behavior, so no doc drift.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/js/bun/jsonc/jsonc.test.ts`:
- Around line 128-153: Strengthen the assertions in the Bun.JSONC.parse error
tests by checking the specific fatal diagnostic text for representative
malformed inputs, rather than only the shared prefix. Update the malformed-input
table to pair inputs with expected messages, and make the
duplicate-key-plus-truncation test assert the expected truncation diagnostic
while still excluding the preceding “Duplicate key” warning.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5830fdae-0d8e-4ea1-bbb9-a6012a3018d8

📥 Commits

Reviewing files that changed from the base of the PR and between 410abd3 and 9138cab.

📒 Files selected for processing (3)
  • src/runtime/api/JSONCObject.rs
  • test/js/bun/jsonc/json-test-suite.test.ts
  • test/js/bun/jsonc/jsonc.test.ts

Comment thread test/js/bun/jsonc/jsonc.test.ts

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — brings Bun.JSONC.parse in line with the TOML/JSON5/YAML sibling parsers by throwing SyntaxError instead of BuildMessage.

What was reviewed:

  • Error-path shape mirrors TOMLObject.rs/JSON5Object.rs (StackOverflow → stack overflow, Alloc → OOM, else → SyntaxError from first log msg); the Kind::Err-first filter from my earlier comment is in place and covered by the inline-snapshot test.
  • Empty-input rejection is scoped to the host function so internal tsconfig/package.json callers keep lenient {}; n_structure_no_data.json reclassified accordingly and whitespace-only (" ", n_single_space.json) still routes through the parser and throws.
  • Unused LogJsc import dropped; bun_parsers::Error catch-all covers the remaining variants (SyntaxError/ParserError/UTF8Fail/UnexpectedSyntax).
Extended reasoning...

Overview

Changes the error type thrown by Bun.JSONC.parse on invalid input from BuildMessage (not an Error) to SyntaxError, matching JSON.parse and every other Bun.*.parse sibling. Also makes Bun.JSONC.parse("") throw instead of returning {}. Touches one ~70-line host-function file and two test files (+63/-6 net).

Security risks

None. No new input paths — only the error object constructed on the existing failure path changes. The empty-input check reads source.contents.is_empty() and throws; no allocation or parsing happens on that branch.

Level of scrutiny

Low-medium. The implementation is a near-verbatim copy of the TOMLObject.rs / JSON5Object.rs / YAMLObject.rs error path (verified by grep), plus the Kind::Err-preference tweak from my earlier review. The one behavioral judgment call — rejecting empty input — is well-justified (matches JSON.parse and Bun.JSON5.parse), scoped to the public API only (internal config-file callers still get lenient {} from parse_jsonc itself), and pinned by moving n_structure_no_data.json into N_INVALID in the JSONTestSuite corpus.

Other factors

  • All three prior review threads (mine on warning-vs-error message selection, CodeRabbit on assertion strength) are resolved; the fix commits landed and the duplicate-key-plus-truncation case is now pinned via toMatchInlineSnapshot.
  • The bun_parsers::Error match handles StackOverflow and Alloc(_) explicitly and catches the rest (SyntaxError, ParserError, UTF8Fail, UnexpectedSyntax) in the fallback arm — same coverage as the TOML sibling.
  • New tests cover: SyntaxError instance/name/message-prefix across six malformed inputs including "" and " "; the warning-before-error case; and the JSONTestSuite n_structure_no_data reclassification. PR evidence shows tests fail on main (release) and pass on the PR (ASAN + release).
  • The now-unused LogJsc import was removed.

@robobun

robobun commented Jul 22, 2026 •

Copy link
Copy Markdown
Collaborator Author

Rebased onto current main (includes the SIMD JSON parser rewrite from #33032; no conflicts, all 370 JSONC tests pass locally with the debug build, including the inline snapshot for the diagnostic message). Added @throws {SyntaxError} to the JSONC.parse JSDoc in packages/bun-types/bun.d.ts, matching TOML.parse; bun-types integration test passes.

Fail-before still holds: the two new tests fail on the released build and pass with this diff.

Previous build 77492 was red only on lanes unrelated to this diff (worker_threads stress SIGABRT, already reported for main-break triage, plus retries-passed flakes). Fresh CI run on the rebase: 89444.

Review requested. The one behavior callout for reviewers: Bun.JSONC.parse("") now throws like JSON.parse(""), while the internal tsconfig/package.json JSONC path stays lenient (empty file -> {}).

robobun and others added 6 commits August 6, 2026 05:10
… input

Aligns Bun.JSONC.parse with JSON.parse, Bun.JSON5.parse, Bun.YAML.parse,
Bun.TOML.parse, and Bun.JSONL.parse, all of which throw a real SyntaxError
(which is instanceof Error). Previously JSONC threw a BuildMessage, so a
'catch (e) { if (e instanceof SyntaxError) }' handler that worked for the
rest of the parser family missed JSONC.

Also makes Bun.JSONC.parse('') throw SyntaxError to match JSON.parse and
Bun.JSON5.parse. The internal JSONC parser (tsconfig.json, package.json)
keeps its lenient empty-file -> {} behavior.
…yntaxError message

parse_jsonc emits duplicate-key warnings before the fatal error, so
log.msgs.first() could surface the warning text instead of the real
parse error in the thrown SyntaxError message.
@robobun
robobun force-pushed the farm/71c173a2/jsonc-syntaxerror branch from 4809a05 to 71d34a5 Compare August 6, 2026 05:16
@robobun
robobun requested a review from alii as a code owner August 6, 2026 05:16
Comment thread src/runtime/api/JSONCObject.rs Outdated
Comment thread src/runtime/api/JSONCObject.rs Outdated
@robobun
robobun requested a review from dylan-conway August 6, 2026 05:16
Comment thread src/runtime/api/JSONCObject.rs Outdated
Comment thread src/runtime/api/JSONCObject.rs Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code looks correct and all prior review feedback (warning-vs-error message selection, comment-cop) is addressed — the error path now mirrors TOMLObject.rs / JSON5Object.rs exactly, plus the Kind::Err filter. Deferring to a maintainer only for the flagged public-API behavior change: Bun.JSONC.parse("") now throws where it previously returned {}.

What was reviewed:

  • Error-path shape checked against TOMLObject.rs:31-46 and JSON5Object.rs:61-74 — same StackOverflow/Alloc/SyntaxError structure and message format.
  • Empty-input check is on source.contents (raw bytes before parsing), so whitespace-only still reaches the parser and errors there — covered by the " " case in the new test.
  • n_structure_no_data.json moved from N_VALID_JSONC to N_INVALID; corpus count assertion (318) unchanged since it's a move, not an add.
Extended reasoning...

Overview

The PR changes Bun.JSONC.parse to throw SyntaxError instead of BuildMessage on invalid input, aligning it with the five sibling parsers (JSON.parse, Bun.JSON5.parse, Bun.YAML.parse, Bun.TOML.parse, Bun.JSONL.parse). It also makes Bun.JSONC.parse("") throw rather than return {}. Files touched: src/runtime/api/JSONCObject.rs (~35 lines net), two test files, and one JSDoc line in bun.d.ts.

Security risks

None. This is error-type/message plumbing on an in-process text parser. No new input surface, no allocation driven by untrusted sizes, no privilege boundaries.

Level of scrutiny

Medium. The mechanical change (BuildMessage → SyntaxError) is a straightforward bug fix that copies the exact pattern from TOMLObject.rs and JSON5Object.rs — I verified the match structure, error variants, and message format are byte-for-byte parallel. The Kind::Err filter is a small, well-justified deviation (JSONC's parser emits duplicate-key warnings that TOML/JSON5 don't). Tests are solid: a table of malformed inputs asserting instanceof SyntaxError, an inline snapshot pinning the diagnostic for the warning-precedes-error case, and the JSONTestSuite n_structure_no_data case moved to the must-reject list.

Other factors

Two rounds of my own review feedback were applied and resolved (the Kind::Err filter, and collapsing multi-line comments to satisfy comment-cop). CodeRabbit's assertion-strength concern was also addressed via the inline snapshot. All inline threads are resolved. CI is running on the latest commit.

The reason I'm not approving outright: the author explicitly flagged the Bun.JSONC.parse("") behavior change as "the one behavior callout for reviewers." It's well-justified (JSONTestSuite classifies empty input as must-reject, VS Code's jsonc-parser rejects it, JSON.parse("") and Bun.JSON5.parse("") both throw, and whitespace-only input already threw), and the internal tsconfig/package.json path keeps its lenient behavior. But it is a user-observable breaking change on a public API, and per REVIEW.md's API-design guidance that's a maintainer call, not mine.

@dylan-conway
dylan-conway enabled auto-merge (squash) August 6, 2026 05:44
@dylan-conway
dylan-conway merged commit a663772 into main Aug 6, 2026
56 checks passed
@dylan-conway
dylan-conway deleted the farm/71c173a2/jsonc-syntaxerror branch August 6, 2026 06:12
springmin pushed a commit to springmin/bun that referenced this pull request Aug 6, 2026
… input (oven-sh#35066)

## What

`Bun.JSONC.parse` now throws a `SyntaxError` on invalid input, matching
`JSON.parse`, `Bun.JSON5.parse`, `Bun.YAML.parse`, `Bun.TOML.parse`, and
`Bun.JSONL.parse`. Previously it threw a `BuildMessage`, which is not an
`Error`, so `catch (e) { if (e instanceof SyntaxError) }` (or `e
instanceof Error`) missed JSONC alone out of the whole parser family.

Also: `Bun.JSONC.parse("")` now throws `SyntaxError` to match
`JSON.parse("")` and `Bun.JSON5.parse("")`. The old behavior was
accidental: `Bun.JSONC.parse("")` returned `{}` while `Bun.JSONC.parse("
")` threw, because the internal parser's empty-file special case (there
so an empty `tsconfig.json` / `package.json` parses as `{}`) leaked
through the public API and whitespace-only input missed it. Empty input
is invalid in every JSON dialect: the official JSONTestSuite lists it as
a must-reject case (`n_structure_no_data`, moved to the invalid list in
this PR), and VS Code's `jsonc-parser` reports a parse error on it. The
internal tsconfig/package.json path keeps its lenient empty-file -> `{}`
behavior (the check is in the `Bun.JSONC.parse` host function, not the
shared parser).

## Repro

```js
for (const [n, f] of [["JSON.parse", JSON.parse], ["Bun.JSON5.parse", Bun.JSON5.parse],
                       ["Bun.JSONC.parse", Bun.JSONC.parse], ["Bun.YAML.parse", Bun.YAML.parse],
                       ["Bun.TOML.parse", Bun.TOML.parse], ["Bun.JSONL.parse", Bun.JSONL.parse]]) {
  try { f("{ not valid"); } catch (e) { console.log(n, e.constructor.name, "instanceof Error:", e instanceof Error); }
}
```

Before:
```
Bun.JSONC.parse  BuildMessage  instanceof Error: false
```
(everything else: `SyntaxError` / `true`)

After:
```
Bun.JSONC.parse  SyntaxError   instanceof Error: true
```

## Cause

`src/runtime/api/JSONCObject.rs` threw `log.to_js(global, "Failed to
parse JSONC")`, which materializes a `BuildMessage`. The sibling parsers
(`TOMLObject.rs`, `JSON5Object.rs`, `YAMLObject.rs`) all build a
`SyntaxError` via `global.create_syntax_error_instance(...)` from the
first log message. TOML switched to this pattern in the parser rewrite;
JSONC never did.

## Fix

Mirror the TOML/JSON5 error path: match on `bun_parsers::Error`
(`StackOverflow` -> stack overflow, `Alloc` -> OOM, everything else ->
`SyntaxError` carrying the first log message text), and reject empty
input up front with a `SyntaxError`.

The `JSONC.parse` JSDoc in `packages/bun-types/bun.d.ts` now documents
the throw (`@throws {SyntaxError}`, same as `TOML.parse`).

<!-- robobun:evidence:begin -->

---

**[review]** gate passed · iteration 0 · 3 files touched

<details><summary>fails on main (without fix)</summary>

```console
ASAN without fix: BUILD FAILED (no junit output)
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/jsonc/json-test-suite.test.ts test/js/bun/jsonc/jsonc.test.ts
ninja: Entering directory `/workspace/bun/build/debug'
[1/41] cxx obj/src/jsc/bindings/webcore/streams/BunAsyncIterableSource.cpp.o
[2/41] cxx obj/src/jsc/bindings/webcore/streams/JSByteLengthQueuingStrategy.cpp.o
[3/41] cxx obj/src/jsc/bindings/webcore/streams/JSReadableByteStreamController.cpp.o
[4/41] cxx obj/src/jsc/bindings/webcore/streams/JSDirectStreamController.cpp.o
[5/41] cxx obj/src/jsc/bindings/webcore/streams/CrossRealmTransform.cpp.o
[6/41] cxx obj/unified/UnifiedSource-src_jsc_bindings_webcore-11.cpp.o
[7/41] cxx obj/unified/UnifiedSource-src_jsc_bindings_webcore-14.cpp.o
[8/41] cxx obj/src/jsc/bindings/webcore/HTTPParsers.cpp.o
[9/41] cxx obj/src/jsc/bindings/webcore/streams/JSCountQueuingStrategy.cpp.o
[10/41] cxx obj/src/jsc/bindings/bindings.cpp.o
[11/41] cxx obj/src/jsc/bindings/webcore/streams/BunStreamConsumers.cpp.o
[12/41] cxx obj/src/jsc/bindings/webcore/streams/JSReadableStream.cpp.o
[13/41] cxx obj/src/jsc/bindings/webcore/streams/BunSt
... (truncated)

release without fix: all passed
bun test v1.4.0-canary.1 (9138cab)

test/js/bun/jsonc/jsonc.test.ts:
(pass) Bun.JSONC exists [0.05ms]
(pass) Bun.JSONC.parse handles basic JSON [0.08ms]
(pass) Bun.JSONC.parse handles comments [0.03ms]
(pass) Bun.JSONC.parse handles a comment after a scalar on the same line [0.04ms]
(pass) Bun.JSONC.parse handles trailing commas [0.03ms]
(pass) Bun.JSONC.parse handles arrays with trailing commas [0.02ms]
(pass) Bun.JSONC.parse handles complex JSONC [0.05ms]
(pass) Bun.JSONC.parse handles nested objects [0.03ms]
(pass) Bun.JSONC.parse handles boolean and null values [0.03ms]
(pass) Bun.JSONC.parse throws on invalid JSON [0.05ms]
(pass) Bun.JSONC.parse throws a SyntaxError on invalid input [2.72ms]
(pass) Bun.JSONC.parse SyntaxError names the actual error, not a preceding warning [0.17ms]
(pass) Bun.JSONC.parse handles empty object [0.07ms]
(pass) Bun.JSONC.parse handles empty array [0.03ms]
(pass) Bun.JSONC.parse throws on deeply nested arrays instead of crashing [6.46ms]
(pass) Bun.JSONC.parse throws on deeply nested objects instead of crashing [5.66ms]
(pass) Bun.JSONC.parse handles pathological inputs in linear time [47.35ms]
(pass) Bun.JSONC.parse matches JSON.
... (truncated)
```

</details>

<details><summary>passes on PR (with fix)</summary>

```console
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/jsonc/json-test-suite.test.ts test/js/bun/jsonc/jsonc.test.ts
bun test v1.4.0 (4809a05)

test/js/bun/jsonc/jsonc.test.ts:
(pass) Bun.JSONC exists [3.12ms]
(pass) Bun.JSONC.parse handles basic JSON [2.58ms]
(pass) Bun.JSONC.parse handles comments [2.21ms]
(pass) Bun.JSONC.parse handles a comment after a scalar on the same line [2.55ms]
(pass) Bun.JSONC.parse handles trailing commas [3.20ms]
(pass) Bun.JSONC.parse handles arrays with trailing commas [2.09ms]
(pass) Bun.JSONC.parse handles complex JSONC [3.27ms]
(pass) Bun.JSONC.parse handles nested objects [2.32ms]
(pass) Bun.JSONC.parse handles boolean and null values [2.35ms]
(pass) Bun.JSONC.parse throws on invalid JSON [4.44ms]
(pass) Bun.JSONC.parse throws a SyntaxError on invalid input [17.76ms]
(pass) Bun.JSONC.parse SyntaxError names the actual error, not a preceding warning [4.38ms]
(pass) Bun.JSONC.parse handles empty object [4.47ms]
(pass) Bun.JSONC.parse handles empty array [1.86ms]
(pass) Bun.JSONC.parse throws on deeply nested arrays instead of crashing [17.77
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 748ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/41] cxx obj/src/jsc/bindings/webcore/streams/BunAsyncIterableSource.cpp.o
[2/41] cxx obj/src/jsc/bindings/webcore/streams/JSCountQueuingStrategy.cpp.o
[3/41] cxx obj/src/jsc/bindings/webcore/streams/CrossRealmTransform.cpp.o
[4/41] cxx obj/src/jsc/bindings/webcore/streams/JSReadableStreamAsyncIterator.cpp.o
[5/41] cxx obj/src/jsc/bindings/webcore/streams/BunStreamConsumers.cpp.o
[6/41] cxx obj/src/jsc/bindings/webcore/streams/BunStreamSource.cpp.o
[7/41] cxx obj/src/jsc/bindings/webcore/streams/JSByteLengthQueuingStrategy.cpp.o
[8/41] cxx obj/src/jsc/bindings/webcore/streams/JSDirectStreamController.cpp.o
[9/41] cxx obj/src/jsc/bindings/webcore/HTTPParsers.cpp.o
[10/41] cxx obj/src/jsc/bindings/webcore/streams/JSReadableByteStreamController.cpp.o
[11/41] cxx obj/src/jsc/bindings/webcore/streams/JSReadableStream.cpp.o
[12/41] cxx obj/unified/UnifiedSource-src_jsc_bindings_webcore-3.cpp.o
[13/41] cxx obj/src/jsc/bindings/webcore/streams/JSStreamsRuntime.cpp.o
[14/41] cxx obj/src/jsc/bindings/webcore/strea
... (truncated)
```

</details>

<details><summary>diff hotspot</summary>

```
src/runtime/api/JSONCObject.rs            | 41 +++++++++++++++++++++++++++----
 test/js/bun/jsonc/json-test-suite.test.ts |  2 +-
 test/js/bun/jsonc/jsonc.test.ts           | 27 ++++++++++++++++++++
 3 files changed, 64 insertions(+), 6 deletions(-)
```

</details>

**gate history** · 3 passed · 0 rejected · iteration 0

<details><summary>evidence per changed file</summary>

```
file                                       reads  edits  tests
src/runtime/api/JSONCObject.rs                 3      5      0
test/js/bun/jsonc/json-test-suite.test.ts      2      2      0
test/js/bun/jsonc/jsonc.test.ts                1      3      0
```

</details>

<!-- robobun:evidence:end -->

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants