Repository navigation
Conversation
…dEvaluation
The runtime transpiler (target: bun) enables minify_syntax, which folds
expressions like (0, class {}), true && class {}, [class {}][0],
({k: class {}}).k, and (true ? class {} : 0) down to a bare class {}.
Printing that into a binding position (const A = class {}) triggers
NamedEvaluation, so the class/function picks up the binding name instead
of staying anonymous per spec.
Guard each fold site with is_anonymous_named() the same way we already
guard for has_value_for_this_in_call(): emit (0, expr) instead of a bare
anonymous function/class/arrow. For the object-literal property case the
property key is the spec name, so skip the fold instead of wrapping.
|
Warning Review limit reached
Next review available in: 14 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (4)
Comment |
(class {}) || 0, (class {}) ?? 0, and [] ? class {} : 0 all fold the
anonymous class into a NamedEvaluation position via the return e_.left
arms and the CouldHaveSideEffects ternary branch (where simplify_unused_expr
returning None seeds join_with_comma with E::Missing, which collapses to
the bare branch).
There was a problem hiding this comment.
Both of my earlier findings are addressed in ce4639e — the return e_.left paths in BinLogicalOr/BinNullishCoalescing are now wrapped, the EIf CouldHaveSideEffects branch seeds 0 via needs_comma_wrap before falling back to E::Missing, and the new H/I/J/L cases cover them. I didn't find further gaps this pass, but this touches ~10 fold sites in the parser hot path and there are outstanding comment-cop flags, so leaving for a human sign-off.
What was reviewed:
- Re-checked all guarded fold sites (comma,
&&/||/??both operands, ternary both arms × both side-effect paths,[x][0],{k:x}.k,{}.k ??= x) againstis_anonymous_named(). - Verified
needs_comma_wraphoist also covers the pre-existingis_call_targetcase on theCouldHaveSideEffectspath. - Checked the
({}).x ??= class {}test expectation against spec (MemberExpression LHS → no NamedEvaluation →""is correct).
Extended reasoning...
Overview
The PR guards constant-folding sites in src/js_parser/{fold.rs, visit/visit_binary.rs, visit/visit_expr.rs} so that folding a wrapper expression ((0, x), true && x, [x][0], cond ? x : y, ({k:x}).k, ({}).k ??= x) down to a bare anonymous class/function/arrow does not expose it to NamedEvaluation at the enclosing binding site. It mirrors the existing is_call_target && has_value_for_this_in_call() pattern with is_anonymous_named(), emitting (0, expr) instead of a bare expr. Three runtime tests were added to test/bundler/transpiler/runtime-transpiler.test.ts.
My previous review (on commit 3758bcc) flagged two unguarded sibling paths: the return e_.left arms of BinLogicalOr/BinNullishCoalescing, and the CouldHaveSideEffects branch of the EIf fold where simplify_unused_expr → None → E::Missing → join_with_comma returned the arm bare. Commit ce4639e addresses both — the left-operand arms now wrap via join_with_comma(ZERO, e_.left), and the ternary CouldHaveSideEffects path chains .or_else(|| needs_comma_wrap.then(...)) ahead of the E::Missing fallback. The test file gained H ([] ? class {} : 0), I ((function(){}) || 0), J ((()=>{}) ?? 0), and L ((class{}) ?? 0) covering exactly those cases.
Security risks
None. This is AST-level constant folding; inputs are already-parsed expression nodes and the change only decides whether to keep a (0, …) comma wrapper.
Level of scrutiny
High. visit_binary.rs and visit_expr.rs run on every file the runtime transpiler touches, and the fix pattern is repeated across ~10 sibling sites — the class of bug this PR fixes is exactly "missed one parallel arm", so it deserves careful human eyes to confirm the enumeration is now complete (I did not find further unguarded fold-to-bare sites, but this is a whack-a-mole shape). The needs_comma_wrap hoist in the EIf handler also slightly changes behavior for the is_call_target case on the CouldHaveSideEffects path (now wrapped where it previously wasn't) — that reads as a correct incidental fix, but is worth a maintainer glance.
Other factors
There are four outstanding comment-cop bot flags on the added inline comments. The flagged comments are short (3-4 lines, same style as the neighboring // "(0, this.fn)()" => "(0, this.fn)()" examples) and read as legitimate why-comments rather than workaround justifications, but they are unaddressed automated feedback and the author should decide whether to trim them. Given the outstanding bot flags plus the breadth of parser sites touched, deferring rather than approving.
There was a problem hiding this comment.
Both prior findings are addressed — the left-operand return e_.left sites in BinLogicalOr/BinNullishCoalescing and the ternary CouldHaveSideEffects path are now guarded, with matching test cases (I/J/L/H). No new issues found. Deferring only because this is parser constant-folding that runs on every file; worth a maintainer glance at the approach (guard-per-fold-site vs. handling at the binding position).
What was reviewed:
- Traced
is_anonymous_named()(arrow / unnamed fn / unnamed class only) andprefill::data::ZERO→ENumber(0.0), sojoin_with_commanever short-circuits to bare expr. - Checked
({}).x ??= class {}against the spec — LHS is not an IdentifierRef, so no NamedEvaluation; test expectationX: ""is correct. - The guards are strictly additive (mirror the existing
is_call_target && has_value_for_this_in_call()shape); non-anonymous operands are unaffected.
Extended reasoning...
Overview
Guards ~8 constant-folding sites in the JS parser (BinComma, BinLogicalAnd/Or, BinNullishCoalescing, BinNullishCoalescingAssign|BinLogicalOrAssign, EIf both arms × both side-effect branches, EIndex on EArray × 2, EObject property access) so that folding a wrapper down to a bare anonymous class/function/arrow doesn't expose it to NamedEvaluation at the enclosing binding. Emits (0, expr) instead, or skips the fold for the object-literal case where the property key is the spec-assigned name. ~80 lines Rust + 3 test.concurrent blocks.
Security risks
None. Pure AST-transform correctness; no I/O, no untrusted input handling changes.
Level of scrutiny
High — this is the runtime transpiler's constant-folding path, executed on every file bun run loads. That said, every change is a strictly conservative guard added to an existing fold: when the operand is not an anonymous fn/class/arrow, behavior is byte-identical. The (0, expr) wrap is the same mechanism the pre-existing is_call_target && has_value_for_this_in_call() guard already uses at each of these sites, so there's a known-good template right next to every hunk.
Other factors
- My two earlier findings (left-operand
||/??folds; ternaryCouldHaveSideEffects+simplify_unused_expr → None → E::Missingpath) were fixed in ce4639e and covered by new test inputsI,J,L,H. - Verified
prefill::data::ZEROisExprData::ENumber, sojoin_with_commacannot short-circuit viais_missing()on the new sites. - Verified the
({}).x ??= class {}test expectation against the spec: logical-assignment NamedEvaluation only fires whenIsIdentifierRefof the LHS is true; a MemberExpression LHS skips it, so.name === ""is correct and the added!e_.right.is_anonymous_named()guard preserves that. - Tests cover all three anonymous forms, both operands, and include a negative test confirming direct
const A = class {}still gets"A"and named classes still fold. - Outstanding comment-cop bot lints appear addressed by 99bed96 (all added comments are one-line
// "in" => "out"examples matching neighboring style), but they haven't auto-resolved.
Not approving solo because parser folding is load-bearing for every file and a maintainer should sign off on scattering the guard across 8 sites vs. centralizing it.
|
Status: ready for review. Pass-after verified locally (host disk recovered): CI: builds #80517 and #80805 were both fleet-wide infra outages (169 waiting_failed, 20-25 expired, build-cpp sibling errored with nothing to link). My Rust code compiles clean on the CI darwin-aarch64 lane that did run. Re-roll is spent; needs a maintainer to re-trigger or merge once CI recovers. On the design question raised in review: the guard is unconditional (not keyed on |
0b0044f to
99bed96
Compare
There was a problem hiding this comment.
The prior sibling-site findings look addressed in ce4639e and the guards now cover left-operand ||/?? and the ternary CouldHaveSideEffects path. Deferring to a human on this one — it's ~10 fold-site edits in the parser hot path, the author couldn't run bun bd test locally (relying on CI #80805), and it's worth a maintainer confirming the guards should be unconditional rather than gated on minify_keep_names (they also affect bun build --minify output size, not just the runtime transpiler).
Checked: is_anonymous_named() covers EArrow/unnamed EFunction/unnamed EClass; join_with_comma won't short-circuit the new (0, ...) wraps since prefill::data::ZERO isn't EMissing; the ({}).x ??= class {} test expectation of "" matches spec (NamedEvaluation for ??= requires IsIdentifierRef on the LHS, which a MemberExpression fails); BinLogicalAnd's unguarded return e_.left is unreachable for anonymous fn/class since they're always truthy.
Extended reasoning...
Overview
Guards ~10 constant-folding sites in src/js_parser/{fold.rs, visit/visit_binary.rs, visit/visit_expr.rs} so that folding a wrapper expression ((0, x), true && x, null ?? x, cond ? x : y, [x][0], ({k:x}).k, ({}).k ??= x) down to a bare anonymous class/function/arrow emits (0, x) instead — preventing NamedEvaluation from assigning the binding name where the spec says .name should be "" (or the property key, in the object-literal case). Adds three test.concurrent subprocess tests covering 15 wrapper forms plus a guard test that direct const A = class {} and named classes still fold normally.
Security risks
None. Pure AST-transform correctness change; no I/O, no untrusted input parsing beyond what already exists.
Level of scrutiny
High. This is the JS parser's constant-folding path, exercised on every file the runtime transpiler touches (minify_syntax is force-on for target.is_bun()). A wrong guard here could regress correctness or output size across all of bun run and bun build --minify. The change is mechanically consistent (mirrors the pre-existing is_call_target && has_value_for_this_in_call() pattern at each site), but the number of touched sites and the earlier round of missed siblings argue for a human pass.
Other factors
- Prior review feedback addressed: my earlier inline findings (unguarded left-operand
||/??returns, ternaryCouldHaveSideEffectsarms) are now covered — verified againstto_boolean/to_null_or_undefinedin scan_side_effects.rs andjoin_with_comma'sis_missing()short-circuit. - Local verification gap: the author reports
bun bdlink failing locally and is relying on CI (#80805, still building at last update). REVIEW.md requires pass-after confirmation; a maintainer should confirm CI is green before merge. - Design question: the guards are unconditional, so
bun build --minifywill now emit(0, class {})where it previously emittedclass {}. That's a strict spec-correctness improvement and matches the existingthis-binding pattern, but a maintainer may want to weigh whether it should instead key offminify_keep_names(which exists inparser.rs) — esbuild is the reference here per REVIEW.md. - Test quality: tests follow harness conventions (
test.concurrent,bunEnv/bunExe, drain stdout/stderr/exited concurrently, assert stderr before exit code), and the guard test proves the fold isn't over-broad. Fail-before was confirmed withUSE_SYSTEM_BUN=1.
|
The comma case was independently re-reported, so confirming this is still live on current main (checked with a build at b7a0431, which already includes #36730): const o = { a: (0, function () {}), b: (0, class {}), c: (0, () => {}) };
const d = (0, function () {});
let e; e = (0, function () {});
console.log(JSON.stringify([o.a.name, o.b.name, o.c.name, d.name, e.name]));
// node: ["","","","",""]
// bun: ["a","b","c","d","e"]Every wrapper form in the test added here ( The branch now conflicts with main in |
|
The const B = [class {}][0];
const f = [function () {}][0];
const g = [0, class {}][1];
console.log(JSON.stringify([B.name, f.name, g.name]));
// node: ["","",""]
// bun: ["B","f","g"]
|
|
The object-literal case was independently re-reported too. Still live on main at 2f5c180 ( const a = ({ aa: async () => {} }).aa;
function f() { return ({ bb: () => {} }).bb; }
const c = ({ "c-c": function () {} })["c-c"];
const d = ({ dd: class {} }).dd;
console.log(JSON.stringify([a.name, f().name, c.name, d.name]));
// node v26.3.0: ["aa","bb","c-c","dd"]
// bun: ["a","","c","d"]This PR's
|
|
The const B = [() => null][0];
let C = [function () {}][0];
var D = [class {}][0];
console.log(JSON.stringify([B.name, C.name, D.name]));
// node: ["","",""]
// bun: ["B","C","D"]This branch covers it ( |
|
The object literal site of this PR is now split out as #42585. #40833 (merged to main on 2026-09-13) made the standard decorator lowering emit When this branch is rebased, drop its |
|
Closing as part of a cleanup of stale pull requests. This PR has had no new commits since 2026-07-25, it conflicts with main, and its last CI run failed. This is not a judgment on the fix itself. If the problem still reproduces on a current build, reopen this PR after a rebase or open a new one against main. |
|
Continued in #42691, against current main. It carries the comma, |
What
bun run(no flags) gives anonymous classes/functions a.namethe spec leaves empty:Why
The runtime transpiler forces
minify_syntax = truefortarget.is_bun(), which enables constant folding at these sites:BinComma:(0, expr)->exprBinLogicalAnd/BinLogicalOr/BinNullishCoalescing:true && expr->exprEIf:true ? expr : _->exprEIndexonEArray:[expr][0]->exprmaybe_rewrite_property_accessonEObject:({k: expr}).k->exprWhen
expris an anonymous class/function/arrow, printing the folded result into a binding position (const A = class {}, default parameter, class field, object property) triggers NamedEvaluation and the binding name sticks. The(0, expr)idiom exists precisely to defeat this, so folding it away breaks real code (DI/registry keyed onconstructor.name).The object-literal case is worse: the spec assigns the property key (
"prop") as the name, so folding replaces a correct name with the wrong one.Fix
Guard each fold site with
is_anonymous_named(), mirroring the existingis_call_target && has_value_for_this_in_call()pattern that already protectsthisbinding. For comma/logical/ternary/array, emit(0, expr)instead of a bare anonymous class/function/arrow. For the object-literal property access, skip the fold (the property key name must be preserved).Named classes/functions still fold normally since
is_anonymous_named()returns false for them.Tests
Added to
test/bundler/transpiler/runtime-transpiler.test.ts: all wrapper forms acrossconst, default parameter, and class field positions, plus a guard test that directconst A = class {}still gets name"A"and(0, class Named {})still folds.