Skip to content

js_parser: reject a trailing comma in a parenthesized expression - #47139

Open
robobun wants to merge 4 commits into
mainfrom
robobun/81727f79/reject-paren-trailing-comma
Open

robobun wants to merge 4 commits into
mainfrom
robobun/81727f79/reject-paren-trailing-comma

Conversation

@robobun

@robobun robobun commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • var x = (a,); runs and prints as x = a. Node, JavaScriptCore, tsc and bun lint refuse it: SyntaxError: Unexpected token ')'. A parser review found it, not a user.
  • Cause: parse_paren_expr (src/js_parser/parse/mod.rs:455) accepts a comma before ), for arrow parameters and async(...) arguments. Its comma-expression exit does not check for it.

Fix

  • The loop records the ) after a comma. The comma-expression exit reports Unexpected ) there and stops, as b || (a,) already does.
  • (a,) => a, async(a,) and (a,): T => a stay valid. The transpiler cache version is 44.
  • Verified: test/bundler/transpiler/transpiler.test.js (10 new tests, stock bun fails 4). Also the transpiler, repl and cache suites.
  • Self-reviewed: 19 concerns raised, 17 addressed. Not taken: wait for js_parser: reject parenthesized arrow parameters #41624, file an issue for the TypeScript defect.

Background

  • The decision this needs. esbuild, the source of this parser, accepts (a,). This PR is stricter on purpose, where node, JavaScriptCore and tsc refuse. If the parser must follow esbuild here, close it.
  • Considered a lookahead for => at the comma: each valid (a, b,) => {} pays a second lex.

Downsides

  • A file or REPL input with (a,) stops loading: 0 of 60,881 files in this repo, 1 of 50,678 in 537 npm packages that target bun.
  • Valid JavaScript pays 8 to 12 instructions per parenthesized list (+0.008% on typescript.js, spread 0.02%) and 16 to 32 stack bytes per nesting level.
  • TypeScript: valid c ? (a,): T => (d) : e => f is an error now. main prints another program for it, also without the comma.
Notes

Where it came from and what it waits for

The check

  • close_paren_after_comma is a local of parse_paren_expr, next to comma_after_spread. The loop sets it and leaves when a comma is directly before ).
  • Only the comma-expression exit reads it, after log_expr_errors and after the Unexpected "..." check. The arrow function exit and the async(...) exit do not.
  • The error is Unexpected ) with the range of the ), and the parser stops. x = (L) and x = 0 || (L) give the same message, length and count for 26 of 26 lists, 5 columns apart.
  • An earlier error in the list comes first. x = ({ a = 1 },) gives Unexpected "=", then Unexpected ), as x = b || ({ a = 1 },) does on main. x = (...a,) and x = (a: T,) stop at their own error.
  • Bun.Transpiler transformSync, transform, scan and scanImports, bun file.js, bun build and the REPL use this parser.
  • bun check - a TypeScript type checker builtin to Bun #44361 recorded the same fact in this function for its tolerant mode. Add bun lint and bun format #44805 removed that mode.

REPL

The REPL puts ( ) around an input that starts with {. When that does not parse, it gives the input to JavaScriptCore as it is written. A replMode row in the new tests holds the first step. With replMode, the position of an error is a position in the text with the ( ), for each error in such an input. That is so on main too.

Input main this PR
bun repl -p '{ a: 1 },' prints the object, exit 0 SyntaxError: Unexpected token ',', exit 1
bun repl -p '(1, 2,)' 2, exit 0 SyntaxError: Unexpected token ')', exit 1

TypeScript between ? and :

In TypeScript a : after ) can start a return type. For a body in parentheses, the parser takes a : e => that follows for the return type of that body. Then the : of the conditional is gone. esbuild 0.21.5 does the same: it refuses x = c ? (a) => (b) : e => f.

.ts input tsc main this PR
x = c ? (a) => (b) : e => f valid Expected ":" but found ";" same as main
x = c ? (a): T => (d) : e => f valid prints c ? a : (T) => (d) => f same as main
x = c ? async (a,): T => (d) : e => f valid prints c ? async(a) : (T) => (d) => f same as main
x = c ? (a,): T => (d) : e => f valid prints c ? a : (T) => (d) => f Unexpected ) at the first )
x = c ? (a, b,): T => { return f(a b) } : d error at b Expected ")" but found "b" Unexpected ) at the first )
x = c ? (a,) : e => f TS1109 at the ) prints c ? a : (e) => f Unexpected ) at the )
  • In rows four and five the parser tries the arrow function, the try fails, and the list is then an expression. A list that ends with a comma is not an expression any more.
  • 25,200 programs of the form c ? HEAD: T => BODY : ELSE (6 heads, 5 return types, 20 bodies, 14 else branches, 3 places), 23,220 of them valid for tsc 6.0.2. main prints 20,700 as tsc reads them, and each of those prints the same with this PR. main prints 432 as another program: 288 have a head with a trailing comma and are an error now, 144 have no such comma and stay. 2,088 are an error on both builds. For 1,152 of them the error is now Unexpected ).
  • This PR does not change how the body is read. Five it.failing tests in transpiler.test.js hold the valid forms with the output of tsc.
  • A narrower repair was measured and left out: take the list for parameters when it ends with a comma and : T => follows. It changes no verdict, only messages. x = c ? (a,) : e => f would then report Expected ":" at the end, where tsc points at the ). Heads with typed parameters have the same fault on main and would keep it.

Suites

  • TypeScript's commaOperatorWithoutOperand.ts (in test/cli/check/typescript-go/bundle.zst) expects TS1109 at the ) of (ANY, ); and four more lines. The transpiler accepted 5 of 5. It refuses 5 of 5 now, at the same column.
  • 130 inputs with a comma in each list position, node against Bun.Transpiler: 65 -> 0 disagree.
  • 18,118 generated programs (js, jsx, ts, tsx): 6,804 are refused now, each with one Unexpected ). V8, JavaScriptCore and tsc refuse each of them. 4,394 are accepted by both builds and print the same text. 0 are accepted only by the PR. scan agrees with transformSync on each row. Agreement with V8: 5,350 -> 9,285 of 9,360. Agreement with tsc: 5,346 -> 8,211 of 8,750. This set has no arrow function body in parentheses before an arrow function. The table above has that family.
  • Inputs that were errors before: 1,114 of 6,920 generated ones change their errors. Each holds a (…,) and a second mistake. Examples: (a, b,) = 1 (Invalid assignment target -> Unexpected ), which is what node reports) and x = ({ a = 1 },) (one error -> two).

Measurements

Release builds of main 99ee03e and of this diff on it, before the rebase. valgrind and perf are not on the machine. Instruction counts come from perf_event_open (instructions:u) and from a gdb single-step count. babel.min.js is not on the machine, so typescript.js takes its place.

  • Instructions for one parse_paren_expr call, callees included, main vs PR (gdb single-step): (a, b) => {} js 14,182 -> 14,190, ts 18,195 -> 18,195. (a, b,) => {} js 14,262 -> 14,270, ts 18,250 -> 18,251. (a) js 2,755 -> 2,766, ts 2,798 -> 2,806. () => {} js 3,806 -> 3,808, ts 3,824 -> 3,824. Six parameters: js +12, ts -8. async(a, b): js +10, ts +33.
  • Item loop, comma to next item: P<false, false> 19 -> 19 instructions, P<true, false> 20 -> 19. ) compares on that path: 1 -> 1 (gdb trace over llvm-objdump).
  • 20 x transformSync(typescript.js) (9.1 MB, loader js): 18,944,973,753 -> 18,946,448,195 instructions:u (+0.008%), run-to-run spread 0.012% and 0.020% (10 interleaved runs). 300 x src/js/node/http2.ts: +0.003%, spread 0.04%. 100 x bun-install.test.ts: +0.019%, spread 0.05% and 0.07%.
  • A 1 MiB file of one repeated statement, 20 x transformSync: x = (a, b) => {}; js +0.104%, ts -0.000%. x = (a + b) * c; js +0.157%, ts +0.108%.
  • parse_paren_expr x4: 3064/3050/6746/6758 -> 3165/3159/5694/5660 bytes. No new function. .text -1,024 bytes, .rodata +0 (llvm-nm -S, llvm-size).
  • parse_paren_expr frame: 0x188/0x198/0x418/0x408 -> 0x198/0x1a8/0x428/0x428. Deepest ((((a)))) on an 8 MiB stack: js 7,445 -> 7,338, ts 4,712 -> 4,627.
  • Cache: a 7.9 KiB file with (a,) that main cached: exit 0 -> exit 1 with the PR binary. With the version field of that entry set to 44 by hand, the PR binary runs the file, so the version change is needed. 12 cached modules: 12 cache files written on the first run after the upgrade, 0 on the second.
  • Files that stop loading: this repo with node_modules, 0 of 60,881 (454 MB). 537 published packages that target bun, 1 of 50,678: src/__tests__/PieServer.test.js:1155 of @websocketpie/server-bun 1.2.6. It holds ( actual, expected, 'message', );, an assertion that lost its function name. bun test reports Unexpected ) there until the name is back. String and template literals in test/ that hold source text: 0 of 479, the new tests aside.

Not changed

Verified

  • test/bundler/transpiler/transpiler.test.js on the debug build: 268 pass, 0 fail. With stock bun 1.4.3-canary, 4 of the 10 new tests fail. One new block holds the forms that must stay valid, and five it.failing rows hold the TypeScript forms above.
  • Also pass on the debug build: test/cli/run/transpiler-cache.test.ts (24), test/js/bun/repl/repl.test.ts (130), test/bundler/esbuild/ts.test.ts and dce.test.ts.
  • test/bundler/transpiler/ and test/js/bun/transpiler/ (37 files) on the debug build: 5,355 pass. The 32 failures are 5 s timeouts of tests that start processes, 31 of them in jsx-production.test.ts. That file has the same result without this diff.
  • 3 tests of test/bundler/bundler_edgecase.test.ts fail on the debug build with and without this diff: aot/EntryPointNamedLikeBuiltinIsALocalFile, aot/EntryPointIsExternalPackage and DeepImportDiamondDAG.

`x = (a,)` ran, and the printer wrote it as `x = a`. Node, JavaScriptCore
and tsc refuse it. `parse_paren_expr` now records the `)` that follows a
comma and reports `Unexpected )` there when the list is a comma
expression. This is the error that `b || (a,)` already gets.

Arrow function parameters and the arguments of `async(...)` keep their
trailing comma. esbuild accepts `(a,)`, so the parser departs from
esbuild here on purpose.

The runtime transpiler cache version is 44, so that an entry written for
a file with `(a,)` is not served.
@robobun

robobun commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: fix is in this PR (#47139). The diff is green and ready for review.

How it was reproduced

// comma.js
var a = 1;
var x = (a,);
console.log("RAN", x);
  • bun comma.js on main prints RAN 1.
  • node v26.3.0 refuses the file: SyntaxError: Unexpected token ')'.
  • new Bun.Transpiler({ loader: "js" }).transformSync("x = (a,);") returns x = a; on main.
  • With this PR, bun comma.js reports error: Unexpected ) at comma.js:2:12.

CI

No failure comes from this diff. test/bundler/transpiler/transpiler.test.js has no failure in builds 134248 and 134348.

  • The failed test-aot-drawn jobs run test/aot/* tests that also fail on main (builds 134067 and 134092).
  • Build 134348: the darwin x64 test job stopped because its machine lost the build directory. The log shows spawn .../bun-profile ENOENT, then uv_os_get_passwd returned ENOENT in the test runner. No test failed in that job before that.
  • Build 134248: the same lane had one timeout in test/js/node/http/node-http-uaf.test.ts.
  • The other failures passed on a retry.

@robobun

robobun commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 6:27 AM PT - Oct 28th, 2026

❌ @robobun, your commit 106ca44 has 4 failures in Build #134348 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 47139

That installs a local version of the PR into your bun-47139 executable, so you can run:

bun-47139 --bun

@coderabbitai

coderabbitai Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 37368ac and 98fec54.

Files selected for processing: 2
  • src/js_parser/parse/mod.rs
  • src/jsc/RuntimeTranspilerCache.rs
Files with no reviewable changes: 2
  • src/jsc/RuntimeTranspilerCache.rs
  • src/js_parser/parse/mod.rs

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 0 remain after this review.

Walkthrough

The parser now reports Unexpected ) for parenthesized expressions that end in a comma, while retaining the arrow-function path. The cache format version changes, and tests cover parser, scanning, JavaScript compatibility, and Bun runtime behavior.

Changes

Parenthesized trailing-comma parsing

Layer / File(s) Summary
Track and reject expression trailing commas
src/js_parser/parse/mod.rs, src/jsc/RuntimeTranspilerCache.rs
The parser records the closing-parenthesis location after a comma and reports Unexpected ) for non-arrow expressions. The runtime transpiler cache format version changes from 43 to 44.
Validate parser and runtime behavior
test/bundler/transpiler/transpiler.test.js
Tests cover expression-list errors and locations, valid arrow parameters and call arguments, TypeScript cases, error ordering across transformation and scanning APIs, JavaScript parsing comparisons, and Bun runtime behavior.

Possibly related PRs

  • oven-sh/bun#46209: Both PRs change parse_paren_expr decisions for TypeScript parenthesized syntax and preserve arrow-function parsing paths.

Suggested reviewers: jarred-sumner

Priority: Low

Merge Risk: Minimal · up to 106ca

Merge Risk: ⚪ Minimal · up to 106ca

The change is ready to merge subject to normal checks. It intentionally loses support for a documented TypeScript conditional-arrow case.

Pre-merge checks: Passed 4
Check name Status Explanation
Title check Passed The title clearly and concisely identifies the main parser change: rejecting trailing commas in parenthesized expressions.
Description check Passed The description explains the problem, implementation, preserved valid cases, compatibility impact, cache update, testing, and known limitations. It does not use the template headings exactly, but it p…
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.
Full details: Description check

Explanation

The description explains the problem, implementation, preserved valid cases, compatibility impact, cache update, testing, and known limitations. It does not use the template headings exactly, but it provides the required information and is complete.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@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.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Comment thread src/js_parser/parse/mod.rs Resolved
With `replMode`, `{a: 1},` is parsed as `({a: 1},)`. The position of an
error is a position in that text, for each error in such an input. The
test for the trailing comma now checks the message only.
Comment thread src/js_parser/parse/mod.rs Resolved
Comment thread src/jsc/RuntimeTranspilerCache.rs Resolved

@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 review found no issues

No high-confidence issues detected in this change.

Still open from earlier reviews (1):

  • Unresolved: 1 minor or pre-existing.

The comment at the trailing comma check and the cache version note had
two lines each. The second line of each is gone. No code changes.

@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 review found no issues

No high-confidence issues detected in this change.

This branch has not been deployed

No deployments
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.

1 participant